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.
| Property | Description |
|---|---|
| Id | Unique identifier for the webhook configuration |
| TenantId | Your tenant's unique identifier |
| RetryCount | Number of retry attempts after a failed delivery |
| RetryInterval | Fixed wait between retry attempts. It does not grow between attempts. |
| WebhookBaseUrl | Base URL of your endpoint, e.g. https://example.com/api |
| WebhookRelativePath | Path appended to the base URL, without a leading slash, e.g. webhook/events. A leading slash produces a doubled separator in the final URL. |
| IsActive | Whether the webhook is currently enabled |
| AuthType | Which authentication method is used — see Authentication Types |
| AuthSettings | The credentials for that method (API key, Basic username/password, or HMAC secret) |
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.
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)
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
| Header | Meaning |
|---|---|
X-Azakaw-Timestamp | Unix time in seconds at which the request was signed |
X-Azakaw-Signature | v1= 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
-
Join the timestamp and the raw request body with a single
.character:1756041600.{"Type":"RiskAssessmentCalculated","SubmissionId":"67890"} -
Compute
HMAC-SHA256over that string, using your webhook secret as the key. -
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)
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
| Property | Description | Values |
|---|---|---|
| Type | Event type | "RiskAssessmentCalculated" |
| RiskAssessmentId | Unique identifier for the risk assessment | String |
| SubmissionId | Submission identifier | String |
| Risk | Risk level of the profile | "Low", "Medium", "High" |
| ClientRating | Customer 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
| Property | Description |
|---|---|
| Type | Event type ("ProfileStatusUpdated") |
| RiskAssessmentId | Unique identifier for the associated risk assessment |
| SubmissionId | Submission identifier |
| Status | Updated status (e.g., "Approved") |
| ApprovedBy | The user who approved the profile |
| ApprovedBy.Id | The id of the user who approved |
| ApprovedBy.Name | The full name of the user who approved |
| ApprovedBy.Email | The email of the user who approved |
| ApprovedBy.Roles | The 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
| Property | Description | Values |
|---|---|---|
| Type | Event type | "NafathVerificationCompleted" |
| TransId | Transaction identifier from Nafath | String |
| Status | Verification 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
TransIdto 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