Webhooks

Introduction

Webhooks are a powerful way to listen to events from Fuze and trigger actions in your app.
You can use webhooks to receive real-time notifications based on the actions that you take in your Fuze account, such as new messages, new users, or changes to existing users.

Webhook setup

Create an HTTP server

You will need an HTTP server to host your webhook endpoint.
The assumption is that you already have some kind of HTTP server running. If not, some popular options include Apache, Nginx, and Node.js built-in HTTP server.

The HTTP server, on which you will be creating the webhook endpoint, should be accessible via the internet and accessible at a URL.
For the sake of this guide, let’s assume the HTTP server is accessible at - https://mycompany.com

Create a webhook endpoint

You need to set up a webhook endpoint on your server to receive events from Fuze.
This endpoint will be a URL that Fuze can send HTTP POST requests to, whenever an event occurs.

The URL of the server should be a subdomain of the domain you registered in your KYC documents.
This step is required to enforce security.

For example, if your domain is mycompany.com, you can use mycompany.com/webhook for registering the webhook.

Create a new route in your web framework that will handle incoming webhook requests.
The following example code shows how to create a new route in an Express.js application.

import express from 'express';

const app = express();
app.post('/webhook', (req, res) => {
  // Handle incoming webhook requests here
  // This will respond to any requests received on the URL -
  // https://mycompany.com/webhook
})

Register the endpoint

Use the Webhook Subscription API to register the webhook endpoint. The API requires the webhook URL and a secret key.
The secret key will be used to sign the webhook events sent to your endpoint. It is described in the Webhook security section.

Webhook security

Every webhook request sent to your server must be verified to ensure that it is coming from Fuze and not from a third party.
The following headers are present for every request sent by Fuze.

FUZE-TIMESTAMP

This is the current epoch time in seconds (i.e. the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970, the beginning of the Unix epoch, less adjustments made due to leap seconds). Ensure that the timestamp is not too old or in the future.

Example: 1671444764

FUZE-SIGNATURE

We generate a signature using the HMAC-SHA256 algorithm and the secret key you provided. The payload for generating the signature includes the request body and timestamp. This ensures that the request body is not tampered with and the request is coming from Fuze.

Example: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2g3h4i5j6k7l8m9n0

Signature verification

To verify the signature, you need to use the secret key that you provided to Fuze when registering the webhook endpoint.
Use the timestamp from the FUZE-TIMESTAMP header and the request body to generate a new signature using the same HMAC-SHA256 algorithm. Then, compare the newly generated signature with the FUZE-SIGNATURE header value.

Sample code

import * as crypto from 'node:crypto';

// This function generates the HMAC-SHA256 signature for the given payload
export const generateFuzeWebhookSignature = ({
  payload,
  timestamp,
  secret,
}: {
  payload: any;
  timestamp: number;
  secret: string;
}) => {
  const hmac = crypto.createHmac('sha256', secret);
  const hmacPayload = JSON.stringify({
    payload,
    timestamp,
  });
  hmac.update(hmacPayload);
  return hmac.digest('hex');
};


// Webhook endpoint
app.post('/webhook', (req, res) => {
  const timestamp = Number(req.header('FUZE-TIMESTAMP'));
  const receivedSignature = req.header('FUZE-SIGNATURE');

  const expectedSignature = generateFuzeWebhookSignature({
    payload: req.body,
    timestamp,
    secret: '<your-webhook-secret-here>',
  });

  // Match the signature with the one sent in the request before handling the event
  if (receivedSignature !== expectedSignature) {
    console.error('Signatures mismatch');
    res.status(400).send();
  }

  // Further processing
})

Webhook events

Fuze will send data on the webhook endpoint strictly as a REST API call with the following specifications -

  • method as POST
  • content-type as application/json
  • data in the request body

Your server would need to respond with an HTTP 200 status to let the Fuze server know that you have received the event successfully.
Otherwise, Fuze will retry to send the events as per the Retry policy.

Retry policy

The webhook retry workflow works as follows -

  1. Fuze expects an HTTP 200 response to the webhook request sent.
  2. If not, we retry upto 3 times sending the same event, with exponential backoff.
  3. If we fail to send even after 3 times, we email to the support emails registered for your organisation.
  4. We do not deliver events that are older than 24 hours.

We follow "atleast once" delivery semantics with exponential backoff retries. Therefore, in certain cases, the same event may be delivered multiple times.

Sample code

// Webhook endpoint
app.post('/webhook', (req, res) => {
  const timestamp = Number(req.header('FUZE-TIMESTAMP'));
  const receivedSignature = req.header('FUZE-SIGNATURE');

  const expectedSignature = generateFuzeWebhookSignature({
    payload: req.body,
    timestamp,
    secret: '<your-webhook-secret-here>',
  });

  // Match the signature with the one sent in the request before handling the event
  if (receivedSignature !== expectedSignature) {
    console.error('Signatures mismatch');
    res.status(400).send();
  }

  // Handle the incoming webhook events here
  handleWebhookEvent(data);

  // Send a 200 status
  res.send();
})

Event schema

A webhook event is a JSON object with the following structure.

{
  "event": {
    // Integer. Organisation ID.
    "orgId": 1,

    // String. Entity that triggered the webhook event.
    "entity": "Org",

    // Number. Number of retries that have been attempted for this event.
    "numRetries": 0,

    // Date. ISO 8601 format.
    "updatedAt": "2023-12-14T12:35:02.894Z",

    // Date. ISO 8601 format.
    "createdAt": "2023-12-14T12:35:02.894Z"
  },
  "data": {
    // Object. Event specific data.
    "key_1": "value_1",
    "key_2": "value_2"
  }
}

Sample event

{
    "event": {
        "orgId": 1,
        "entity": "Users",
        "numRetries": 1,
        "updatedAt": "2023-12-14T12:35:02.894Z",
        "createdAt": "2023-12-14T12:35:02.894Z"
    },
    "data": {
        "orgUserId": "barbara_allen",
        "orgId": 10,
        "kyc": true,
        "tnc": true,
        "userStatus": "ACTIVE",
        "userType": "CONSUMER"
    }
}
{
    "event": {
        "orgId": 2,
        "entity": "Orders",
        "numRetries": 0,
        "updatedAt": "2023-12-14T12:35:02.894Z",
        "createdAt": "2023-12-14T12:35:02.894Z"
    },
    "data": {
        "id": 29718,
        "orgId": 10,
        "clientOrderId": "5468bbb7-5e5f-425c-a6eb-b89e19a0298a",
        "orgUserId": "barbara_allen",
        "symbol": "ETH_AED",
        "price": 0,
        "averagePrice": 8456.1,
        "side": "BUY",
        "type": "MARKET",
        "quantity": 0.01,
        "quoteQuantity": 0,
        "filled": 0.01,
        "status": "COMPLETED",
        "rejectionReason": null,
        "createdAt": "2023-12-14T12:25:00.257Z",
        "updatedAt": "2023-12-14T12:25:02.529Z"
    }
}

Limitations

  • You cannot subscribe to a subset of events.
    We will deliver all the events for all the entities. You can filter out the events you are interested in and ignore the rest.
  • There is a timeout on the webhook response from the customer’s backend.
    Fuze assumes that the delivery failed if a response with an HTTP 200 status is not received within 1 second.