πŸͺWebhooks

API reference for webhook-related endpoints

Subscription

Managing Webhook Subscriptions

You can create a new webhook subscription or update an existing one using a PUT request to /webhooks. Whether you prefer to receive notifications for all entities through a single callback URL or require more granular configuration, you can achieve this by using the types parameter in the request body.

Retrieving Webhook Subscriptions

You can retrieve and existing webhook subscription using a GET request to /webhooks/{webhookId}.

To retrieve all active webhook subscriptions, make a GET request to /webhooks.

Removing Webhook Subscriptions

If you wish to remove a webhook subscription, you can do so by sending a DELETE request to /webhooks/{webhookId}.

Schema

This portion of our API is in active development and may be updated frequently. Please, reach out to us if something is not working or not working as expected

Every webhook event object has a discriminator webhookType field followed by all fields included in a respective entity schema:

Customer Updated Event
{
  "webhookType": "customer.updated",
  "eventId": "db63696c-10b2-42c1-8a07-ac9a15985744",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "externalId": "string",
  "chainId": "account-chain-id",
  "status": "initiating",
  "createdAt": "2024-01-11T18:34:38.367Z",
  "updatedAt": "2024-01-11T18:34:38.367Z"
}
Account Updated Event
{
  "webhookType": "account.updated",
  "eventId": "d403095e-df22-4946-9932-63b04ced15b4",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "chainId": "account-chain-id",
  "status": "initiating",
  "type": "card_account",
  "currencies": [
    "string"
  ],
  "createdAt": "2024-01-11T18:34:38.367Z",
  "updatedAt": "2024-01-11T18:34:38.367Z"
}
Card Updated Event
{
  "webhookType": "card.updated",
  "eventId": "7fe835d9-7ab5-4db0-a5fe-e37a86735dc5",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "type": "virtual",
  "name": "string",
  "network": "visa",
  "maskedPan": "string",
  "expirationDate": "mm/yyyy",
  "billingAddress": {
    "firstLine": "string",
    "secondLine": "string",
    "city": "string",
    "state": "string",
    "country": "string",
    "postCode": "string"
  },
  "status": "issuing",
  "createdAt": "2024-01-11T18:34:38.367Z",
  "updatedAt": "2024-01-11T18:34:38.367Z"
}
Authorisation Updated Event
{
  "webhookType": "authorisation.updated",
  "eventId": "64727de0-1245-4a12-a8a7-bbe8383d9cfd",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "cardId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": "pending",
  "amount": "decimal",
  "currency": "string",
  "merchantAmount": "decimal",
  "merchantCurrency": "string",
  "exchangeRate": "decimal",
  "merchant": {
    "id": "string",
    "name": "string",
    "mcc": 0,
    "country": "string",
    "city": "string"
  },
  "createdAt": "2024-01-11T18:34:38.368Z",
  "updatedAt": "2024-01-11T18:34:38.368Z"
}
Transaction Updated Event
{
  "webhookType": "transaction.updated",
  "eventId": "64727de0-1245-4a12-a8a7-bbe8383d9cfd",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "cardId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "authorisationId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": "processing",
  "direction": "debit",
  "amount": "decimal",
  "currency": "string",
  "merchantAmount": "decimal",
  "merchantCurrency": "string",
  "exchangeRate": "decimal",
  "merchant": {
    "id": "string",
    "name": "string",
    "mcc": 0,
    "country": "string",
    "city": "string"
  },
  "tokenAmount": "decimal",
  "token": "asset-chain-id",
  "fees": [
    {
      "type": "partner",
      "tokenAmount": "decimal"
    }
  ],
  "chainTransactions": [
    {
      "chainId": "transaction-chain-id",
      "createdAt": "2024-01-11T18:34:38.368Z"
    }
  ],
  "createdAt": "2024-01-11T18:34:38.368Z",
  "updatedAt": "2024-01-11T18:34:38.368Z"
}

Webhook retry policy

We consider a webhook successfully delivered when we receive a success status code (2xx) from your webhook URI.

If we receive any other status code (for instance, if your API is temporarily unavailable), we will start retrying. Our retry policy is similar to jittered exponential backoff. We will immediately perform some fast retries and then start waiting increasingly longer. We will keep retrying for up to 24 hours. If we continue to receive any other status codes than 2xx after retrying for 24 hours, we will discard the webhook.

We apply this retry policy for all supported entities.

Handling duplicated webhooks

OffBlocks can't guarantee that you only receive a single webhook notification for each update. As such, your integration should have logic that can handle receiving multiple webhooks for a given update. Similarly, due to networking limitations, we can't guarantee webhooks to be delivered in a correct chronological order. Your system needs to be able to handle out-of-order notifications.

For example, imagine OffBlocks sends a transaction.updated webhook with status processed, but doesn't receive a 200 response due to network issues from the recipient. In this case, OffBlocks sends an extra transaction.updated webhook with status processed as it can't confirm the previous one was received, regardless of the current status of the transaction. Your integration should be able to handle such possibilities.

Webhook Notification Verification

Every webhook notification we send contains an HTTP signature that you can verify to ensure it has been sent from us, a verified source. Although it is not required, we strongly recommend verifying notification signature, or you risk accepting fraudulent events.

Each webhook notification contains a set of headers for you to verify, following the signing procedure outlined in Request Signatures. There are a few steps required to verifying the webhook signatures. We'll go through each one below.

Retrieve Webhook Verification Key

To verify the signature, you need to grab a public key (encoded in PEM format). This is available through our REST API and can be retrieved by doing a GET call on the /webhooks/verification-key endpoint.

Extract Webhook Headers

On each webhook HTTP request, there are three headers that will be needed for verification.

  • Content-Digest: a sha-512 hash of serialised request content, or body.

  • Signature-Input: a Dictionary structured field containing the metadata for a signatures generated from components within the HTTP request (see IETF specification for more details)

  • Signature: a Dictionary structured field containing a message signatures generated from the signature context of the target message (see IETF specification for more details)

All webhook requests are signed using ECDSA with curve P-384 DSS and SHA-384 as a signing algorithm, while digest is always calculated using sha-512 hashing algorithm.

Verification

Now that you have the webhook body, webhook signature headers, and the public key, you can use these data points to do the verification.

We actively develop a Go library to support HTTP message signatures as well as contribute to a Node.js library. Reach out to us if you're using other technologies on your backend and we'll be happy to assist with any issues integrating HTTP message signatures.

For example, if you're using Go-based backend, you can use following code snippet to verify webhook request's digest and signature:

reader := req.Body

body, err := io.ReadAll(reader)
if err != nil {
	w.WriteHeader(http.StatusBadRequest)
	return
}

block, _ := pem.Decode([]byte(key))

pki, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
	w.WriteHeader(http.StatusBadRequest)
	return
}

pkpub := pki.(*ecdsa.PublicKey)
verifier := httpsig.NewVerifier(
	httpsig.WithVerifyEcdsaP384Sha384(keyId, pkpub),
)

digestor := httpsig.NewDigestor(httpsig.WithDigestAlgorithms(httpsig.DigestAlgorithmSha512))

err = digestor.Verify(body, req.Header)
if err != nil {
	w.WriteHeader(http.StatusBadRequest)
	return
}

err = verifier.Verify(httpsig.MessageFromRequest(req))
if err != nil {
	w.WriteHeader(http.StatusBadRequest)
	return
}

Last updated