Authenticating Notification Webhooks

Tatum notification service supports HMAC to authenticate webhooks.

HMAC allows you to authenticate the legitimacy of notifications sent to your URL.

Step_1: Enable HMAC

  1. Enable HMAC via the following REST API endpoint.

    • Where param “hmacSecret” is provided by the user to authenticate webhook origin.
      Example:
    curl --location --request PUT 'https://api-eu1.tatum.io/v4/subscription' \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: {API_KEY} ' \
    --data '{
      "hmacSecret": "c354b83b-d31b-4dda-9bab-d6a67715a1ed"
    }'
    
    // Response
    204 No Content
    
  2. Create a subscription via [the following REST API] endpoint.
    Example:

    curl --location 'https://api-eu1.tatum.io/v4/subscription' \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: {API_KEY} ' \
    --data '{
      "type": "ADDRESS_EVENT",
      "attr": {
        "chain": "TRON",
        "address": "TVf3RVEtzKtMfqQaCAWs9d4HKbC4bZGaWP",
        "url": "https://eo1tfamse2vfgpm.m.pipedream.net"
      }
    }'
    
    // Response
    {
        "id": "6644ce703ac81e0b215820cc"
    }
    
  1. Newly fired webhooks by Tatum should contain a x-payload-hash in the header.
    Example:

    // Example Webhook headers and body
    {
      "event": {
        "method": "POST",
        "url": "https://eo1tfamse2vfgpm.m.pipedream.net/",
        "headers": {
          "host": "eo1tfamse2vfgpm.m.pipedream.net",
          "content-length": "317",
          "accept": "application/json, text/plain, */*",
          "content-type": "application/json",
          "x-payload-hash": "WdhYQft+qP8LpYAdeOMncUzIZ7DSUWX9JVSjeGH3F4mCreUxtIpTl2VYigm+qUvkfSQ0lWmTrzADm4mGxSVcxA=="
        },
        "body": {
          "address": "TJG7iciLGjsib9qhe6U6F7M2vxYJuDjWNM",
          "amount": "20",
          "counterAddress": "TVf3RVEtzKtMfqQaCAWs9d4HKbC4bZGaWP",
          "asset": "TRON",
          "blockNumber": 44087791,
          "txId": "93442189d7bccbe009f8ab594831ff9d7d258cab712d74a404cd3dccdc4c6d69",
          "type": "native",
          "tokenId": null,
          "chain": "tron-testnet",
          "subscriptionType": "ADDRESS_EVENT"
        }
      }
    }
    
    

Step_2: Verify Webhook Authenticity

  1. Make sure HMAC is enabled. See Step_1 above.
  2. From the received webhooks, retrieve the x-payload-hash value from the header and the webhook's payload as JSON
    1. Example x-payload-hash: "x-payload-hash": "WdhYQft+qP8LpYAdeOMncUzIZ7DSUWX9JVSjeGH3F4mCreUxtIpTl2VYigm+qUvkfSQ0lWmTrzADm4mGxSVcxA=="
  3. Convert the webhook payload to stringify JSON without any spaces.
    • In JavaScript, you would do it like this JSON.stringify(webhook.event.body)
  4. Perform calculations on your side to create a digest using the HMAC Secret, the webhook payload, and the HMAC SHA512 algorithm.
  5. Compare x-payload-hash header value with calculated digest as a Base64 string. Both values must match to confirm that the webhooks are being received from Tatum.

Example:

const { createHmac } = require('node:crypto');

// Step 1: Enable HMAC
// Same HMAC Secret as the one used in the "Enable HMAC" endpoint
const hmacSecret = "c354b83b-d31b-4dda-9bab-d6a67715a1ed";

// Step 2: Get webhook payload and x-payload-hash
const xPayloadHash =
  "WdhYQft+qP8LpYAdeOMncUzIZ7DSUWX9JVSjeGH3F4mCreUxtIpTl2VYigm+qUvkfSQ0lWmTrzADm4mGxSVcxA==";
const webhook = {
  body: {
    address: "TJG7iciLGjsib9qhe6U6F7M2vxYJuDjWNM",
    amount: "20",
    counterAddress: "TVf3RVEtzKtMfqQaCAWs9d4HKbC4bZGaWP",
    asset: "TRON",
    blockNumber: 44087791,
    txId: "93442189d7bccbe009f8ab594831ff9d7d258cab712d74a404cd3dccdc4c6d69",
    type: "native",
    tokenId: null,
    chain: "tron-testnet",
    subscriptionType: "ADDRESS_EVENT",
  },
};

// Step 3: Convert webhook body to stringify JSON
const stringifyWebhook = JSON.stringify(webhook.body);

// Step 4: Calculate digest as a Base64 string using the HMAC Secret, the webhook payload, and the HMAC SHA512 algorithm.
const base64Hash = createHmac("sha512", hmacSecret)
  .update(JSON.stringify(webhook.body))
  .digest("base64");

// Step 5: Compare x-payload-hash value with calculated digest as a Base64 string
const checkValues = xPayloadHash == base64Hash;

console.log(`base64Hash: ${base64Hash}`);
console.log(`x-payload-hash and base64Hash are equal? ${checkValues}`);

// Output
/*
base64Hash: WdhYQft+qP8LpYAdeOMncUzIZ7DSUWX9JVSjeGH3F4mCreUxtIpTl2VYigm+qUvkfSQ0lWmTrzADm4mGxSVcxA==
x-payload-hash and base64Hash are equal? true
*/


const { createHmac } = await import("node:crypto");

// Step 1: Enable HMAC
// Same HMAC Secret as the one used in the "Enable HMAC" endpoint
const hmacSecret = "c354b83b-d31b-4dda-9bab-d6a67715a1ed";

// Step 2: Get webhook payload and x-payload-hash
const xPayloadHash =
  "WdhYQft+qP8LpYAdeOMncUzIZ7DSUWX9JVSjeGH3F4mCreUxtIpTl2VYigm+qUvkfSQ0lWmTrzADm4mGxSVcxA==";
const webhook = {
  body: {
    address: "TJG7iciLGjsib9qhe6U6F7M2vxYJuDjWNM",
    amount: "20",
    counterAddress: "TVf3RVEtzKtMfqQaCAWs9d4HKbC4bZGaWP",
    asset: "TRON",
    blockNumber: 44087791,
    txId: "93442189d7bccbe009f8ab594831ff9d7d258cab712d74a404cd3dccdc4c6d69",
    type: "native",
    tokenId: null,
    chain: "tron-testnet",
    subscriptionType: "ADDRESS_EVENT",
  },
};

// Step 3: Convert webhook body to stringify JSON
const stringifyWebhook = JSON.stringify(webhook.body);

// Step 4: Calculate digest as a Base64 string using the HMAC Secret, the webhook payload, and the HMAC SHA512 algorithm.
const base64Hash = createHmac("sha512", hmacSecret)
  .update(JSON.stringify(webhook.body))
  .digest("base64");

// Step 5: Compare x-payload-hash value with calculated digest as a Base64 string
const checkValues = xPayloadHash == base64Hash;

console.log(`base64Hash: ${base64Hash}`);
console.log(`x-payload-hash and base64Hash are equal? ${checkValues}`);

// Output
/*
base64Hash: WdhYQft+qP8LpYAdeOMncUzIZ7DSUWX9JVSjeGH3F4mCreUxtIpTl2VYigm+qUvkfSQ0lWmTrzADm4mGxSVcxA==
x-payload-hash and base64Hash are equal? true
*/