Button Publisher Integration Guide on
Android

Button Publisher Integration Guide

This guide walks you through how to add Button to your iOS application. In this guide, we'll:

  1. Add the Button SDK
  2. Configure User Attribution
  3. Create a Button Purchase Path
  4. Get Notified on User Activity via Webhooks (Loyalty Only)

Add the Button SDK

Add the Button SDK to your build.gradle file in the dependencies.

implementation 'com.usebutton:android-sdk:6+'

Import and configure the SDK in your Activity. This should be called when your application class is created.

import com.usebutton.sdk.Button

class MyApplication : Application() {
  override fun onCreate() {
    super.onCreate()

    // Debugging enabled (do not include in production)
    if (BuildConfig.DEBUG) {
      Button.debug().loggingEnabled = true
    }

    // Replace app-xxxxxxxxxxxxxxxx with your App ID from the Button Dashboard https://app.usebutton.com
    Button.configure(this, "app-xxxxxxxxxxxxxxxx")
  }
}
import com.usebutton.sdk.Button;

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    // Debugging enabled (do not include in production)
    if (BuildConfig.DEBUG) {
      Button.debug().setLoggingEnabled(true);
    }

    // Replace app-xxxxxxxxxxxxxxxx with your App ID from the Button Dashboard https://app.usebutton.com
    Button.configure(this, "app-xxxxxxxxxxxxxxxx");
  }
}

Proguard Rules

If your app is using Proguard (minifyEnabled true in your build.gradle) make sure that the proguard rules below are in effect in your rules file. This file is usually located in yourapp/proguard-rules.pro, or its location is specified in build.gradle, look for proguardFiles in your default or variant configuration (note: this can be multiple files).

-keepattributes Exceptions,InnerClasses,EnclosingMethod
-keep class com.usebutton.** { *; }
-keepclassmembers class * implements android.os.Parcelable {
    static ** CREATOR;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { public *; }

Configure User Attribution

Note: Passing a User ID is required for Loyalty Publishers.

Passing a user ID ensures subsequent Merchant activity (such as installs or purchases) is attributed to a user. This can either be your user ID, email or a stable hash of one. You can use this later to look up orders, activity and identify the user in Webhooks.

Set the user ID in your login handler.

Button.user().setIdentifier("YOUR_LOGGED_IN_USERS_ID")
Button.user().setIdentifier("YOUR_LOGGED_IN_USERS_ID");

If your app offers logout functionality, invoke the SDK's logout feature during your logout handler.

Button.clearAllData()
Button.clearAllData();

Create a Button Purchase Path

// Step 1 - Create a Purchase Path request
val url = "https://www.example.com/"
val request = PurchasePathRequest(url)

// Optionally associate a unique token (e.g. campaign Id)
// request.pubRef = "abc123"

// Step 2 - Fetch a Purchase Path object
Button.purchasePath().fetch(request) { purchasePath, throwable ->

    // Step 3 - Start Purchase Path flow
    purchasePath?.start(context)
}
// Step 1 - Create a Purchase Path request
String url = "https://www.example.com/";
PurchasePathRequest request = new PurchasePathRequest(url);

// Optionally associate a unique token (e.g. campaign Id)
// request.setPubRef("abc-123");

// Step 2 - Fetch a Purchase Path object
Button.purchasePath().fetch(request, new PurchasePathListener() {
    @Override
    public void onComplete(@Nullable PurchasePath purchasePath,
            @Nullable Throwable throwable) {

        // Step 3 - Start Purchase Path flow
        if (purchasePath != null) {
            purchasePath.start(context);
        }
    }
});

If Button can exchange the the given url for a fully attributed action, the fetch will complete with a PurchasePath. Starting a purchasePath will pass control to the Button SDK which will open the merchant app, install flow, or web checkout.

You can pass an optional pubRef (typically used for click IDs, campaign IDs, etc.), which will be passed back to you via webhooks.


Get Notified on User Activity via Webhooks (Loyalty Only)

In order to reward users for Merchant activity such as purchases, you can setup a service endpoint for Button to send requests to. You can configure this webhook – and test it – in your Button Dashboard.

Event States

There are 3 unique states to orders in Button's systems, presented in the event_type field in the webhook's root. The table below provides details on each state:

State Terminal? Mutable? Billable? Definition
tx-pending No Yes No Indicates that the transaction has been created but is not in a final state. It can still be adjusted or cancelled. Commission is not yet guaranteed for this transaction.
tx-validated Yes No Yes Indicates that the finalization period for a transaction has concluded. The transaction will never be modified beyond this point. Commission as stated in the Webhook is guaranteed. All earnings are final and billable to the Merchant.
tx-declined Yes No No Indicates that the transaction has been cancelled by the user or the Merchant. The transaction will never be modified beyond this point. The transaction will not be billable to the Merchant.

Event Objects

A transaction is a record of a commission owed or earned.

A transaction is created when a user links from a Publisher app or mobile website to a Merchant app or mobile website and completes a commissionable event. Commissionable events include: downloading a Merchant's App or making a purchase. We capture the transaction event types in the category field.

Transactions have an associated status that indicates a stage in the transaction life-cycle. A validated status indicates that the transaction is immutable and billable. All pending status transactions are subject to change.

There are a number of financial fields within the transaction. Button models all fields that contain a financial value as an an integer in the smallest currency unit (e.g. 100 for $1.00, -100 for $-1.00; 500 for $5.00, -500 for -$5.00).

  • account_id: The publisher's Account ID for this transaction. A Publisher will have a separate Account ID for each currency in which they drive a Merchant transaction.
  • advertising_id: The device's advertising ID (IDFA on iOS and Google AID on Android). Only available when this value was reported.
  • amount: The commission your organization is owed or has earned.
  • attribution_date: The date Button used for selecting commission rates.
  • btn_ref: Button tracking token associated with this transaction.
  • button_id: The ID of the button. Value is either static or a Button ID (btn-xxx).
  • button_order_id: Button generated Order ID that is globally unique. Only available for new-user-order and existing-user-order events.
  • campaign_id: a Button configuration value distinguishing the Merchant Campaign ID tied to this transaction.
  • category: The type of event that generated this commission. This will be one of the following values:
    • new-user-order: Commission driven by an order event. No relation to user segment.
    • existing-user-order: Deprecated. Commission driven by an order event. No relation to user segment.
    • app-install: Commission driven by the installation of the Merchant application.
  • commerce_organization: The Button organization ID for the Merchant that received the billable transaction.
  • created_date: When the transaction was first created in Button's system (ISO-8601).
  • currency: The currency of the order or commission, as a three-letter ISO currency code (ISO 4217).
  • customer_order_id: The customer-facing order ID reported by the Merchant. Only available for new-user-order and existing-user-order events and when reported by the merchant. Note: This field represents the order identifier communicated to the customer.
  • event_date: When the transaction was processed by Button's system (ISO-8601).
  • id: Button generated Transaction ID that is globally unique and persists throughout the lifecycle of a transaction.
  • modified_date: When the transaction was last modified (ISO-8601).
  • order_currency: The currency of the order. (only available for new-user-order and existing-user-order events).
  • order_id: Merchant reported ID in their own namespace. Only available for new-user-order and existing-user-order events and when reported by the merchant.
  • order_click_channel: The channel in which the user completed the transaction. Values include: app, webview, unknown.
  • order_line_items: An array of the order's line item details. Only available for new-user-order and existing-user-order events.
    • identifier: The unique identifier of your line item.
    • total: The total price of all items bought in a particular line item (e.g. if 3 bananas were purchased for $3.00 each, total would be 900). If provided, amount will be ignored and set to round (total / quantity). Must be net of any discounts or taxes and represent the value a Merchant expects to commission on. Must be an integer > 0.
    • amount: The line item order value, in smallest decimal currency units. Will be an integer. Example: 1234 for $12.34, -1234 for -$12.34.
    • sku (optional): The Stock Keeping Unit of the line item. A Merchant-specific identifier for the line item. String with a maximum length of 255.
    • quantity: The number of individual units purchased. If present, will be an integer > 0.
    • publisher_commission: The total commission generated by the line item. Will be an integer >= 0. A publisher_commission equal to 0 means no commission was earned for this line item. This field will only appear on transactions where created_date is after 2018-07-18T23:00:00Z.
    • gtin (optional): The Global Trade Item Number of the number. Supported values: UPC and EAN. String with 14 numeric digits.
    • category (optional): The topmost (or most general) level category of the line item. Example: For an item with category Electronics > Computers > Laptops this field would have the value Electronics.
    • subcategory1 (optional): The first subcategory of the line item. Example: For an item with category Electronics > Computers > Laptops this field would have the value Computers.
    • subcategory2 (optional): The second subcategory of the line item. Example: For an item with category Electronics > Computers > Laptops this field would have the value Laptops.
    • description: Text describing the line item.
    • attributes: A key/value store for strings. Defaults to an empty object. Only available when this value was reported.
  • order_purchase_date: The Merchant reported date and time that the user made the purchase. Only available when this value was reported.
  • order_total: The total value of the order. Includes total value of all items purchased, whether or not they qualified for a commission. Includes discounts, if applied. Only available for new-user-order and existing-user-order events.
  • pub_ref: The publisher reference value set when a Button or Link was fetched. Only visible to the Publisher, and only if it is configured.
  • publisher_customer_id: The publisher's internal customer ID. Only visible to the Publisher, and only if it is configured.
  • publisher_organization: The Button organization ID for the Publisher application that drove the transaction.
  • status: The current state of the transaction, which will be one of the following values. More detail here.
    • pending: A transaction has been created, but it is subject to change. app-install events skip this state.
    • validated: A transaction is final and immutable and will be paid out.
    • declined: A transaction has been canceled (user or Merchant cancels an entire order).
  • validated_date: When the transaction moved to validated status. (ISO-8601).

Don't be surprised if Button adds some additional fields to help with debugging (Ex. 'campaign_id')


{
  "id": "hook-XXX",
  "event_type": "tx-validated",
  "data": {
    "posting_rule_id": null,
    "order_currency": "USD",
    "modified_date": "2017-01-01T20:00:00.000Z",
    "created_date": "2017-01-01T20:00:00.000Z",
    "order_line_items": [
        {
            "identifier": "sku-1234",
            "total": 6000,
            "amount": 2000,
            "quantity": 3,
            "publisher_commission": 1000,
            "sku": "sku-1234",
            "gtin": "00400000000001",
            "category": ["Clothes"],
            "description": "T-shirts",
            "attributes":{
                "size": "M"
            }
        }
    ],
    "button_id": "static",
    "campaign_id": "camp-XXX",
    "rate_card_id": "ratecard-XXX",
    "order_id": null,
    "customer_order_id": "abcdef-123456",
    "account_id": "acc-XXX",
    "btn_ref": "srctok-XXX",
    "currency": "USD",
    "pub_ref": "publisher-reference-string",
    "status": "validated",
    "event_date": "2017-01-01T20:00:00Z",
    "order_total": 6000,
    "advertising_id": null,
    "publisher_organization": "org-YYY",
    "commerce_organization": "org-XXX",
    "amount": 1000,
    "button_order_id": "btnorder-XXX",
    "publisher_customer_id": "11111111-2222-3333-4444-999999999999",
    "order_purchase_date": "2017-01-01T20:00:00.000Z",
    "id": "tx-XXX",
    "order_click_channel": "app",
    "category": "new-user-order",
    "validated_date": "2017-01-06T19:02:09Z",
    "attribution_date": "2017-01-06T19:02:09Z"
  },
  "request_id": "attempt-XXX"
}

Filter Webhook Payload for "Order" Events

Button’s webhooks fire requests on all commissionable events, including orders and app-installations. So, when a commissionable event occurs, you will receive a webhook from Button. Depending on how and what you're using Button webhooks for, you'll want to appropriately filter out webhook requests.

For example, if you’re rewarding users on purchases they make, you’ll want to filter out the webhook requests representing app-installs by checking the value of the category field in the request payload. If the value is app-install, ignore the request. If the value is new-user-order or existing-user-order, process the request into user rewards.

Validate Payload Request Data Needed for User Rewards

When validating the data in the webhook request payload, we recommend you verify the availability of specific fields you need instead of validating against a specific JSON structure. This is because the JSON structure may change as we enhance the data reported to us by Merchants (e.g. "Jet now reports UPC data with each line-item"). This approach will ensure that your webhooks don’t error when such an update is made.

Acknowledgment

To acknowledge that you have successfully received a webhook, you should respond with a 2XX status code.

Unsuccessful webhooks due to connection issues or non-4XX error will be re-sent for up to 3 days. Retries follow an exponential back-off strategy for the first hour. After that, retries happen once an hour.

To maintain the proper receipt order of messages, new webhooks will only be sent once previous ones are acknowledged. Therefore, if you don't acknowledge a webhook, you won't be bombarded by new webhooks, but you also won't receive recent information.

Error handling

Button's transaction webhooks enforce ordering on a single transaction, but sends webhooks for independent transactions concurrently to maximize throughput. This means that for a webhooks concerning a specific transaction, before the "next" event that occurred, we require confirmation that the "current" webhook was properly accepted. However, webhooks for other transactions can still send as normal. We limit the total number of transactions webhooks we are trying to send in parallel for to an endpoint at any one time.

For this reason, it’s important to respond to a webhook request with a 2XX level HTTP response code, unless a real error occurred requiring subsequent webhook for that transaction to be paused until this error is resolved.

Below is a list of suggested response codes and what they signify to Button:

  • 200 or 204 - Webhook accepted and successfully processed.
  • 202 - Webhook accepted but processing was unsuccessful.
  • 400 - Webhook is invalid and cannot be processed. Button will not attempt to re-send this webhook.
  • 401 - Webhook signature is invalid. Button will attempt to re-send this webhook.

Retry Logic

If Button receives a 400 response for a webhook, we will not attempt to re-send that webhook.

If Button receives a 3XX, 4XX (other than 400), or 5XX response for the webhook, we will retry this webhook with an exponential back-off strategy for the first hour. After that, retries happen once an hour up to 3 days.

Button limits the number of webhooks that it tries to send to an endpoint at any one time. If the number of failing webhooks in the retry state for an endpoint exceeds the number that Button attempts to send, Button will not send any new webhooks to that endpoint until at least one of the webhooks in the retry state succeeds or exceeds its retry limit.

We recommend logging 202 and 4XX responses and flagging them up to our Customer Service team for further investigation.

Delivery Guarantees

Button offers at-least-once delivery guarantees for webhooks. Button will retry webhook delivery until acknowledgment is received. In rare occasions, we might deliver the same webhook more than once. This can happen when one of the servers becomes unavailable to process a message acknowledgement. If that occurs, the webhook delivery might be retried.

While this is rare, it is important that your application does not reprocess duplicate webhooks. You can use id in the webhook field to dedupe, as we will never send 2 different webhooks with the same id value.

Security

To receive Webhooks from Button, you must expose a public route on the open internet. Because sensitive business-logic often sits behind a webhook handler, it's important to verify that a request received in your webhook endpoint is actually from Button.

To do this, Button cryptographically hashes the payload we send with a Webhook Secret. Each Webhook configured in the Dashboard will have its own secret. Then, on the receiving end, the server may compute the same hash given a copy of the Secret and the request payload. Equating this computed hash with the one Button sent guarantees a few things (assuming your Secret has never been compromised):

  1. The request is guaranteed to be from Button
  2. The contents of the request are guaranteed to be exactly as Button sent them (that is, no malicious computer could have intercepted the request and altered the contents of the payload)

To generate an equivalent hash, you must:

  1. Capture the raw request body before any parsing has occurred. This should be a UTF-8 encoded string.
  2. Supply your Webhook Secret and the string from (1) to your language's HMAC implementation, using SHA-256 as the hashing algorithm.
  3. Digest the result from (2) as a hex-encoded string.

Note: It's a good idea to read your Webhook Secret from the server's environment rather than checking it into version control.

Notable Distinctions

  • All amounts come through in cents (no decimals). (e.g. a $10.00 order would be "order_total":"1000")
  • order_total is the total order value, amount is the commission amount (e.g. if the commission is 10%, a $10 order would generate a $1 commission - "order_total":"1000" with "amount":"100")

Client Libraries

Button's client libraries include helper methods for validating incoming webhook requests:

Alternatively, you can integrate one of the following examples directly into your project.

Example Implementation

For generic use in the popular web frameworks Express, Sinatra, and Flask for Node.js, Ruby, and Python, respectively:

var express = require('express');
var app = express();

var bodyParser = require('body-parser');
var crypto = require('crypto');

var port = process.env.PORT || 5000;

// You can find your Webhook's Secret in the Dashboard at https://app.usebutton.com/webhooks on the page for a specific Webhook.
var WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET' // Do not publicly expose this

app.use(bodyParser.json({ verify: verify, type: 'application/json' }));

app.post('/webhook', function(req, res) {
  // Here is the webhook data
  var data = req.body.data;

  res.sendStatus(200);
})

app.listen(port, function() {
  console.log('Listening on port: ' + port)
})

function verify(req, res, buf, encoding) {
  if (req.headers['x-button-signature'] !== signature(buf)) {
    throw new Error('Invalid Webhook Signature');
  }
}

function signature(requestBody) {
  return crypto.createHmac('sha256', WEBHOOK_SECRET)
    .update(requestBody)
    .digest('hex');
}
require 'sinatra'

require 'openssl'
require 'json'

# You can find your Webhook's Secret in the Dashboard at https://app.usebutton.com/webhooks on the page for a specific Webhook.
WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET' # Do not publicly expose this

post '/webhook' do
  request_body = request.body.read
  raise 'Invalid Webhook Signature' if request.env["HTTP_X_BUTTON_SIGNATURE"] != signature(request_body)

  json = JSON.parse(request_body)

  # Here is the webhook data
  data = json['data']

  status 200
end

def signature request_body
  OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    WEBHOOK_SECRET,
    request_body
  )
end

import os
import hmac
import hashlib

from flask import Flask, request, abort

app = Flask(__name__)

# You can find your Webhook's Secret in the Dashboard at https://app.usebutton.com/webhooks on the page for a specific Webhook.
WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET' # Do not publicly expose this

@app.route('/webhook', methods=['POST'])
def webhook():
    computed_signature = signature(request.data)
    sent_signature = request.headers.get('X-Button-Signature').encode('utf8')

    if not hmac.compare_digest(computed_signature, sent_signature):
        abort(403)

    # Here is the webhook data
    data = request.json['data']

    return 'ok', 200

def signature(request_body):
    return hmac.new(WEBHOOK_SECRET, request_body, hashlib.sha256).hexdigest()

if __name__ == "__main__":
    app.run()

The full source code for each of these sample apps can be found here.