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:

HeaderExamplePurpose
Content-Typeapplication/json
X-Myaza-Signaturesha256=<hmac_hex>HMAC-SHA256 of the raw body. Verify this.
X-Myaza-Eventverification.completedThe event type.
X-Myaza-Delivery<delivery_id>Unique per delivery; use it to deduplicate.
User-AgentMyaza-Webhooks/1.0Identifies the sender.

Event types

EventWhen it fires
verification.startedA verification was accepted and processing began.
verification.completedVerification succeeded — identity confirmed.
verification.failedCompleted but validations did not pass (e.g. face mismatch, insufficient credit).
verification.not_foundThe ID number was not found in the government database.
verification.errorA system error occurred during verification.
api_key.createdAn API key was created (security audit).
api_key.revokedAn API key was revoked (security audit).
credits.deductedCredit was deducted from the wallet for a verification.
credits.lowWallet balance dropped below the configured threshold.
credits.topped_upThe wallet was topped up.

Payload structure

All events share the same envelope; the data object varies by event type.

json
{
  "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:

json
{
  "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:

json
{
  "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):

shell
curl -H "Authorization: Bearer sk_live_..." \
  https://identity.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/selfie \
  --output selfie.jpg

The 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.

KeyDescription
selfieLiveness selfie still.
documentFrontFront of the ID document.
documentBackBack of the ID document, when captured.
livenessVideoRecording of the liveness challenge.
documentFrontVideoRecording captured while scanning the document front.
documentBackVideoRecording 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.

js
// 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
# 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', 200

Retries

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.

AttemptDelay after previous attempt
130 seconds
25 minutes
330 minutes
42 hours
524 hours

Best practices

  • Verify the signature before processing the payload.
  • Respond 2xx immediately, 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 (or verificationId).
  • Store the secret in an environment variable, never in source.
  • Test on staging before enabling production endpoints.