What are Webhooks

Webhooks allow apps to receive real-time events from TOTUS. Webhooks are required to be implemented in order to receive fulfillment information and are included as a security mechanism to make sure the right app receives the card data and is not intercepted by a bad actor.

To help orient you with the content in this guide, the following definitions explain webhook concepts:

Webhook: A single event message. TOTUS sends a webhook to an app's webhook subscription endpoint. A webhook contains a JSON payload in the body and metadata in the headers.

Webhook headers

The following headers are included in the webhook call.

X-TOTUS-Topic: `/fulfillment/complete`
X-TOTUS-Hmac-Sha256: `XWmrwMey6OsLMeiZKwP4FppHH3cmAiiJJAweH5Jo4bM=`
X-TOTUS-API-Version: `v1.0`
X-TOTUS-RequestId: `b54557e4-bdd9-4b37-8a5f-bf7d70bcd043`

The following header fields are used:

FieldUsage
X-TOTUS-TopicThe type of webhook call being made
X-TOTUS-Hmac-Sha256A hash of the content in the webhook and the API key used to create the original order
X-TOTUS-API-VersionThe version of the TOTUS API
X-TOTUS-RequestIdThe request-id from the POST of the order

The webhooks API provides "at least once" delivery of webhook events. This means that an endpoint might receive the same webhook event more than once. You can detect duplicate webhook events by comparing the X-TOTUS-RequestId header to previous events.

Verify the Webhook

Before you process the webhook, you need to verify that the webhook was sent from TOTUS. You can verify the webhook by calculating a digital signature.

Each webhook request includes a base64-encoded X-TOTUS-Hmac-Sha256 header, generated using the API Key and the data sent in the request.

# The following example uses Ruby and the Sinatra web framework to verify a webhook request:

require 'rubygems'
require 'base64'
require 'openssl'
require 'sinatra'
require 'active_support/security_utils'

API_KEY = 'my_API_KEY'

helpers do
  # Compare the computed HMAC digest based on the client secret and the request contents to the reported HMAC in the headers
  def verify_webhook(data, hmac_header)
    calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', API_KEY, data))
    ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
  end
end

# Respond to HTTP POST requests sent to this web service
post '/' do
  request.body.rewind
  data = request.body.read
  verified = verify_webhook(data, env["HTTP_X_TOTUS_HMAC_SHA256"])

  halt 401 unless verified

  # Process webhook payload
  # ...
end

# The following example uses PHP to verify a webhook request:

<?php

define('API_KEY', 'my_api_key');

function verify_webhook($data, $hmac_header)
{
  $calculated_hmac = base64_encode(hash_hmac('sha256', $data, API_KEY, true));
  return hash_equals($hmac_header, $calculated_hmac);
}

$hmac_header = $_SERVER['HTTP_X_TOTUS_HMAC_SHA256'];
$data = file_get_contents('php://input');
$verified = verify_webhook($data, $hmac_header);

error_log('Webhook verified: '.var_export($verified, true)); // Check error.log to see the result

if ($verified) {
  # Process webhook payload
  # ...
} else {
  http_response_code(401);
}

?>

# The following example uses Python and the Flask framework to verify a webhook request:

from flask import Flask, request, abort
import hmac
import hashlib
import base64

app = Flask(__name__)

API_KEY = 'my_api_key'

def verify_webhook(data, hmac_header):
    digest = hmac.new(API_KEY.encode('utf-8'), data, digestmod=hashlib.sha256).digest()
    computed_hmac = base64.b64encode(digest)

    return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    data = request.get_data()
    verified = verify_webhook(data, request.headers.get('X-TOTUS-Hmac-SHA256'))

    if not verified:
        abort(401)

    # Process webhook payload
    # ...

    return ('', 200)

Webhooks in Production

Setting the webhooks on an order basis allows the webhooks to call into the development machines (on a per-developer basis) and add additional data to the path. The issue with adding the ability to add webhook URLs in the order, if the API key is compromised, then the information can be sent to the bad actor.

To mitigate this scenario the webhook call in production is limited to the root domain that is set up in the account. The RootURL needs to match the URL setup in your account; otherwise, the POST for the order will fail. This ensures that the right endpoint is called. Only the RootURL needs to match what is stored in the account. The rest of the path can be changed per POST order call.

https://[RootURL]/fulfillment/A33433?partnerid=123&partnerid2=94333