Skip to main content

Webhook Integration

This guide provides instructions for setting up and configuring webhooks in our system. The webhook system supports multiple authentication types, allowing you to secure your webhook endpoints according to your security requirements.

Overview​

Webhooks allow you to receive real-time updates about specific events in your azakaw integration. When an event occurs, our system sends an HTTP POST request to your configured webhook endpoint with relevant information about the event.

Webhook Configuration​

Each webhook is associated with a tenant and includes several configurable properties:

Configuration Properties​

These are set by Azakaw when your webhook is provisioned. They are listed so you know what to supply and what to ask for when something needs changing.

PropertyDescription
IdUnique identifier for the webhook configuration
TenantIdYour tenant's unique identifier
RetryCountNumber of retry attempts after a failed delivery
RetryIntervalFixed wait between retry attempts. It does not grow between attempts.
WebhookBaseUrlBase URL of your endpoint, e.g. https://example.com/api
WebhookRelativePathPath appended to the base URL, without a leading slash, e.g. webhook/events. A leading slash produces a doubled separator in the final URL.
IsActiveWhether the webhook is currently enabled
AuthTypeWhich authentication method is used — see Authentication Types
AuthSettingsThe credentials for that method (API key, Basic username/password, or HMAC secret)
One endpoint, all events

A tenant has a single active webhook, and every event type is delivered to that one URL. There is no per-event subscription — branch on the Type field in the payload to tell them apart.

Authentication Types​

Every webhook uses exactly one authentication method, chosen when Azakaw provisions your webhook. They are mutually exclusive — a webhook configured for HMAC does not also send an API key, and vice versa.

Configured by Azakaw

There is no self-service API for webhook configuration. Contact your Azakaw representative with your endpoint URL and preferred authentication method, and they will provision it and hand over any generated credentials.

None​

No authentication headers are sent. Available for sandbox testing only — do not use it in production, where anyone who learns your endpoint URL can post arbitrary payloads to it.

API Key​

A static key is sent in a custom header on every delivery:

x-api-key: your-api-key

The header name is lower-case x-api-key, not Authorization. Compare the value against your stored copy in constant time, and reject the request with 401 if it does not match.

Basic Authentication​

Standard HTTP Basic, sent on every delivery:

Authorization: Basic base64(username:password)
ASCII credentials only

Credentials are encoded as ASCII. Usernames and passwords containing non-ASCII characters — accented Latin, Arabic, and so on — will not transmit correctly. Use ASCII-only credentials.

HMAC Signature​

The strongest option, and the one to prefer for production. Instead of sending a shared secret, Azakaw sends a signature derived from it. The secret itself never travels over the network, so capturing a request does not let an attacker forge future ones.

Two headers are added to every delivery:

X-Azakaw-Timestamp: 1756041600
X-Azakaw-Signature: v1=54b62fe6af3abf989969879eed7323ed5c1011713e78ef28bf119f965c9d2cb3
HeaderMeaning
X-Azakaw-TimestampUnix time in seconds at which the request was signed
X-Azakaw-Signaturev1= followed by lower-case hex HMAC-SHA256. The v1 prefix identifies the scheme so future versions can be introduced without breaking existing receivers.

How the signature is computed​

  1. Join the timestamp and the raw request body with a single . character:

    1756041600.{"Type":"RiskAssessmentCalculated","SubmissionId":"67890"}
  2. Compute HMAC-SHA256 over that string, using your webhook secret as the key.

  3. Hex-encode the result in lower case.

Verifying a delivery​

import { createHmac, timingSafeEqual } from 'crypto';

const SECRET = process.env.AZAKAW_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

function verify(rawBody, headers) {
const timestamp = headers['x-azakaw-timestamp'];
const received = (headers['x-azakaw-signature'] ?? '').replace(/^v1=/, '');

// Reject anything outside the tolerance window, so a captured request cannot be replayed.
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!timestamp || age > TOLERANCE_SECONDS) return false;

const expected = createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');

const a = Buffer.from(received);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
import hashlib, hmac, os, time

SECRET = os.environ["AZAKAW_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300

def verify(raw_body: bytes, headers: dict) -> bool:
timestamp = headers.get("X-Azakaw-Timestamp", "")
received = headers.get("X-Azakaw-Signature", "").removeprefix("v1=")

if not timestamp or abs(int(time.time()) - int(timestamp)) > TOLERANCE_SECONDS:
return False

signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()

return hmac.compare_digest(received, expected)
Sign the raw body, never re-serialized JSON

Compute the signature over the exact bytes you received. If your framework parses the JSON and you re-serialize it before verifying, key order and whitespace will differ and the signature will never match. In Express use express.raw() or bodyParser.json({ verify }); in Flask use request.get_data().

Secret rotation​

A webhook has a single active secret. Rotating it is a coordinated cutover — contact Azakaw to schedule one, and be ready to accept the new secret at the agreed time. Deliveries signed with the old secret will fail verification immediately after the switch.

Retry Mechanism​

In the event of a failed webhook request (e.g., if your server returns a 500 status code), the system will automatically retry the request based on the following configuration:

  • RetryCount: The maximum number of retry attempts
  • RetryInterval: The time between retry attempts

Webhook Request Body​

The body of the webhook request will contain important information depending on the event being triggered. Below are the structures for webhook events.

RiskAssessmentCalculated​

When the RiskAssessmentCalculated event is triggered, the body will contain the following fields:

{
"Type": "RiskAssessmentCalculated",
"RiskAssessmentId": "{RiskAssessmentId}",
"SubmissionId": "{SubmissionId}",
"Risk": "{RiskTypeId}",
"ClientRating": "{ClientRating}"
}

Properties​

PropertyDescriptionValues
TypeEvent type"RiskAssessmentCalculated"
RiskAssessmentIdUnique identifier for the risk assessmentString
SubmissionIdSubmission identifierString
RiskRisk level of the profile"Low", "Medium", "High"
ClientRatingCustomer segment rating"Retail", "Professional", "N/A"

Example​

{
"Type": "RiskAssessmentCalculated",
"RiskAssessmentId": "12345",
"SubmissionId": "67890",
"Risk": "Low",
"ClientRating": "Retail"
}

ProfileStatusUpdated​

When the ProfileStatusUpdated event is triggered, the body will contain the following fields:

{
"Type": "ProfileStatusUpdated",
"RiskAssessmentId": "{RiskAssessmentId}",
"SubmissionId": "{SubmissionId}",
"Status": "{Status}",
"ApprovedBy": {
"Id": "{userId}",
"Name": "{fullName}",
"Email": "{email}",
"Roles": [
"{role1}",
"{role2}"
]
}
}

Properties​

PropertyDescription
TypeEvent type ("ProfileStatusUpdated")
RiskAssessmentIdUnique identifier for the associated risk assessment
SubmissionIdSubmission identifier
StatusUpdated status (e.g., "Approved")
ApprovedByThe user who approved the profile
ApprovedBy.IdThe id of the user who approved
ApprovedBy.NameThe full name of the user who approved
ApprovedBy.EmailThe email of the user who approved
ApprovedBy.RolesThe user roles

Example​

{
"Type": "ProfileStatusUpdated",
"RiskAssessmentId": "1bf4f6ad-807d-45e4-8e8b-613e51203618",
"SubmissionId": "9dc1b390-4a35-4310-a85a-25e6bd7cd3f6",
"Status": "Approved",
"ApprovedBy": {
"Id": "8392d3fb-ad15-43ca-8b9e-a7cbcf411067",
"Name": "Noelani Alfreda Boone William",
"Email": "tazixog@yopmail.com",
"Roles": [
"MLRO"
]
}
}

NafathVerificationCompleted​

When the NafathVerificationCompleted event is triggered, the body will contain the following fields:

{
"Type": "NafathVerificationCompleted",
"TransId": "{TransId}",
"Status": "{Status}"
}

Properties​

PropertyDescriptionValues
TypeEvent type"NafathVerificationCompleted"
TransIdTransaction identifier from NafathString
StatusVerification status"COMPLETED", "REJECTED", "EXPIRED", "ERROR"

Example​

{
"Type": "NafathVerificationCompleted",
"TransId": "abc123-def456-ghi789",
"Status": "COMPLETED"
}

Notes​

  • This webhook is triggered when a user completes verification in their Nafath mobile app
  • You can use the TransId to retrieve full user details via the Nafath API
  • The webhook is sent regardless of whether the status is COMPLETED, REJECTED, or EXPIRED
  • For more information on Nafath integration, see Nafath Identity Verification