Webhooks
Webhooks let you receive verification outcomes (and other events) in real time instead of polling. Myaza sends a signed HTTP POST to each endpoint you register whenever a subscribed event occurs.
Register and manage endpoints under Settings → Organization → Developers → Webhooks. Endpoints are environment-scoped — test your integration with staging endpoints before enabling production.
Request format
Each delivery is a POST with a JSON body and these headers:
| Header | Example | Purpose |
|---|---|---|
Content-Type | application/json | — |
X-Myaza-Signature | sha256=<hmac_hex> | HMAC-SHA256 of the raw body. Verify this. |
X-Myaza-Event | verification.completed | The event type. |
X-Myaza-Delivery | <delivery_id> | Unique per delivery; use it to deduplicate. |
User-Agent | Myaza-Webhooks/1.0 | Identifies the sender. |
Event types
| Event | When it fires |
|---|---|
verification.started | A verification was accepted and processing began. |
verification.completed | Verification succeeded — identity confirmed. |
verification.failed | Completed but validations did not pass (e.g. face mismatch, insufficient credit). |
verification.not_found | The ID number was not found in the government database. |
verification.error | A system error occurred during verification. |
api_key.created | An API key was created (security audit). |
api_key.revoked | An API key was revoked (security audit). |
credits.deducted | Credit was deducted from the wallet for a verification. |
credits.low | Wallet balance dropped below the configured threshold. |
credits.topped_up | The wallet was topped up. |
Payload structure
All events share the same envelope; the data object varies by event type.
{
"id": "evt_01j9abc123",
"event": "verification.completed",
"createdAt": "2026-04-27T12:00:00.000Z",
"data": {
"verificationId": "ver_01j9xyz456",
"requestId": "order_1001",
"externalId": "user_42",
"status": "verified",
"reason": null,
"reasonCode": null,
"idType": "bvn",
"country": "NG",
"idNumber": "12345678901",
"userData": {
"firstName": "JOHN",
"lastName": "DOE",
"dateOfBirth": "1990-01-01"
},
"facialMatch": { "match": true, "confidence": 85 },
"media": {
"selfie": "https://identity.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/selfie",
"livenessVideo": "https://identity.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/liveness-video"
},
"environment": "PRODUCTION",
"createdAt": "2026-04-27T12:00:00.000Z"
}
}On a non-success event (verification.failed, .not_found, .error) the data carries a human-readable reason and a stable reasonCode you can branch on — both null above:
{
"status": "failed",
"reason": "The document expired on 2020-01-01. A current, non-expired document is required.",
"reasonCode": "document_expired"
}See the full failure reason codes catalog.
An api_key.* event's data instead looks like:
{
"id": "evt_01j9abc789",
"event": "api_key.created",
"createdAt": "2026-04-27T12:00:00.000Z",
"data": {
"apiKeyId": "key_01j9abc000",
"name": "Mobile app production key",
"environment": "PRODUCTION",
"createdBy": "user@example.com"
}
}Captured media
Verification events carry a media object — a map of the images and videos captured during the flow. Each value is an absolute URL (not the bytes themselves). Fetch each one from your backend with a secret (sk_) key as a Bearer token (media is sensitive, so a publishable key returns 403 secret_key_required):
curl -H "Authorization: Bearer sk_live_..." \
https://identity.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/selfie \
--output selfie.jpgThe URLs are scoped to the secret key's organization and environment — a staging key cannot read production media — and do not expire. The object only contains the kinds that were actually captured (a number-only-ID flow has just selfie + livenessVideo), and is null when no media is associated with the event.
| Key | Description |
|---|---|
selfie | Liveness selfie still. |
documentFront | Front of the ID document. |
documentBack | Back of the ID document, when captured. |
livenessVideo | Recording of the liveness challenge. |
documentFrontVideo | Recording captured while scanning the document front. |
documentBackVideo | Recording captured while scanning the document back. |
Verifying the signature
Compute HMAC-SHA256(rawBody, endpointSecret) and compare it to the X-Myaza-Signature header using a constant-time comparison. Always use the raw request body — not a re-serialized JSON object — or the signature won't match.
Your endpoint secret is shown when you create the endpoint in the dashboard.
// Node.js / Express
const crypto = require('crypto');
app.post('/webhooks/myaza', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-myaza-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.MYAZA_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// process event.event / event.data …
res.status(200).send('OK');
});# Python / Flask
import hmac, hashlib
from flask import request, abort
@app.route('/webhooks/myaza', methods=['POST'])
def webhook():
sig = request.headers.get('X-Myaza-Signature', '')
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(), request.get_data(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
event = request.get_json()
return 'OK', 200Retries
A delivery that doesn't receive a 2xx response (or times out) is retried with exponential backoff. After 5 failed attempts the delivery is marked FAILED and can be retried manually from the dashboard.
| Attempt | Delay after previous attempt |
|---|---|
| 1 | 30 seconds |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 24 hours |
Best practices
- Verify the signature before processing the payload.
- Respond
2xximmediately, then do heavy work asynchronously to avoid timeouts and retries. - Be idempotent. The same delivery may arrive more than once — deduplicate on
X-Myaza-Delivery(orverificationId). - Store the secret in an environment variable, never in source.
- Test on staging before enabling production endpoints.