Configure Webhooks on
All Platforms

Configure Webhooks

Button allows you to configure webhooks so you can get real-time updates to your system when users push Buttons in your app. To do so, you will need to configure the destination of the webhook and what events you want to receive. Then, Button will send you an HTTP request when any of those events are created.

It's a very good idea to not inherently trust any request received by your webhook endpoint. See Security for details.

For full source code of sample apps in Node.js, Ruby, and Python that securely receive Button webhooks, see here. You can also find these in the Example Implementation section, below.


Create or Edit a Webhook

To create a new webhook or edit an existing one, you can do so on the Dashboard.


Format

Webhooks will be sent with an HTTP POST to your configured URL.

The body of the message:

{
    "id": "hook-1c1d7937f9715e3e",
    "event_type": "tx-validated",
    "data": {},
    "request_id": "attempt-4843253923fd59a8",
}
  • id: the unique ID for the webhook
  • event_type: the type of event that triggered the webhook. This field also indicates what object will be in the data field. List of event states.
  • data: this is the main payload of the message. Description of the different event objects.
  • request_id: since messages can be sent multiple times, this is the unique ID per request

The headers will include

  • Content-Type: application/json
  • User-Agent: Button Webhooks X.X.X
  • X-Button-Signature: XXXXXXXXX

Event States

  • tx-pending: A transaction has entered the pending state. Object transaction.
  • tx-validated: A transaction has entered the validated state. Object transaction.
  • tx-declined: A transaction has entered the declined state. Object transaction.

Event Objects

Transaction

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

A transaction is created when a user moves from one app to another and takes an action like installing the app, or purchasing an item (API details for a Commerce app to report an order). This is so that we can credit the Publisher app for driving the user, and debit the Commerce app as they received something they wanted (a new user or an order). Since there various events that can drive a transaction, we capture that in the category field. Also, transactions have a status associated with them, to allow you to identify the current state the of the life-cycle the transaction is in.

  • amount: The amount your organization is owed or earned, in the smallest decimal currency units.
  • currency: The currency of the amount, as a three-letter ISO currency code (ISO 4217).
  • category: The type of event that has lead to the transactions.
    • new-user-order: An order for a user who just installed the Merchant application.
    • existing-user-order: An order for a user who already had the Merchant application installed.
    • app-install: An installation of the Merchant application.
  • created_date: When the transaction was first created.
  • modified_date: When the transaction was last modified.
  • validated_date (optional): When the transaction moved to validated status.
  • event_date: When the order or app install occurred.
  • btn_ref: Button's tracking token associated with this transaction.
  • pub_ref: The publisher token set when a Button or Link was fetched (only visible to the Publisher).
  • account_id: Account ID.
  • order_id (optional): Order ID reported by the Merchant. (only available for new-user-order and existing-user-order events and Merchants)
  • button_order_id (optional): Order ID generated by Button. (only available for new-user-order and existing-user-order events)
  • order_currency (optional): The currency of the order. (only available for new-user-order and existing-user-order events)
  • order_total (optional): The total attributable order value of the order, in the smallest decimal currency units. (only available for new-user-order and existing-user-order events)
  • order_line_items (optional): 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.
    • amount: The line item order value, in smallest decimal currency units. Will be an integer >= 0. Example: 1234 for $12.34.
    • quantity (optional): The number of units of this line item. Will be an integer > 1
    • description (optional): Text describing the line item.
    • attributes (optional): A key/value store for strings. Defaults to an empty object.
  • publisher_organization: The organization ID of the application that drove the user to another application.
  • publisher_customer_id (optional): The publisher's internal customer ID (only available for the publisher organization, and only if it is configured)
  • button_id: The ID of the button.
  • commerce_organization: The organization ID of the application that the user was driven into.
  • status: The current state of the transaction.
    • pending: A transaction has been created, but it is subject to change. app-install events skip this state.
    • validated: A transaction is final and will be paid on a later date.
    • declined: A transaction has been canceled (this can be from an order being canceled).
  • advertising_id (optional): The device's advertising ID (IDFA on iOS and Google AID on Android). Only available to Merchants, when this value was reported.

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

An example webhook object:

{
  "event_type": "tx-validated",
  "data": {
    "amount": 1000,
    "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",
            "amount": 2000,
            "quantity":3,
            "description": "T-shirts",
            "attributes": {
                "size": "M"
            }
        }
    ],
    "button_id": "btn-59722058ab439774",
    "order_id": "order-1",
    "account_id": "acc-67a18cc120e5235d",
    "btn_ref": "srctok-713c963c1c30c55f",
    "currency": "USD",
    "pub_ref": "publisher-token",
    "status": "validated",
    "event_date": "2017-01-01T20:00:00Z",
    "order_total": 6000,
    "advertising_id": "02840796-66B3-4C96-AF72-84393B4925BF",
    "publisher_organization": "org-1418faaf0504783b",
    "commerce_organization": "org-0a94e4380a410aab",
    "button_order_id": "btnorder-4db429a36440c00e",
    "publisher_customer_id": "6815467b-93ca-47ce-97ae-bcf8b4292a87",
    "id": "tx-070dd05f5db6e4ec",
    "category": "new-user-order",
    "validated_date": "2016-06-01T19:02:09Z"
  },
  "id": "hook-7486e4aab57bb23e",
  "request_id": "attempt-5c302d097767a327"
}

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

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.

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.


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.


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.