Authenticating Notification Webhooks

Tatum notification service supports HMAC to authenticate webhooks.

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

Steps

Step_1: Enable HMAC

Example request:

  • The param “hmacSecret” is provided by the user to authenticate webhook origin.
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

Step_2: Create a subscription

Example request:

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"
}

Step_3: Check the webhook

Newly fired webhooks by Tatum should contain a x-payload-hash in the header.

Example webhook:

// 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_4: Verify the 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 code:

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
*/