Webhooks
Build with

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 sandbox endpoints before enabling production.

Request format
Build with

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
Build with

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.
workflow.run.completedA workflow decision run finished — carries the outcome (approve/decline/review). See Decisioning.
workflow.actionA webhook action node fired mid-graph.
workflow.run.failedA decision run could not finish (engine fault — not a decline).
entity.createdA completed verification created a monitored entity.
entity.importedAn entity was created via the direct-create or bulk-import API.
identity.resolvedAn entity was linked to a global identity.
event.flaggedA monitored event scored to a non-ALLOW decision.
screening.matchA sanctions / PEP / adverse-media / crypto-wallet screen returned a non-clear result (the payload's type field says which — WALLET for wallet hits).
alert.createdTransaction monitoring opened an alert.
alert.updatedA repeat firing rolled up into an existing open alert.
entity.reverification_dueAn entity's perpetual-KYC renewal is due.
case.overdueAn investigation case blew past its SLA.
key_person.completedA KYB key person resolved (their KYC finished, or a reviewer attested them).

The data payloads for the compliance events (entity.created, identity.resolved, event.flagged, screening.match, alert.*) are documented with each feature — see Identity Hub API and Event monitoring. The workflow.* events are documented under Decisioning.

Payload structure
Build with

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",
    "workflowId": "wf_AbC123dEf456",
    "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://trust.myaza.co/api/kyc/verifications/ver_01j9xyz456/media/selfie",
      "livenessVideo": "https://trust.myaza.co/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.

workflowId is the workflow that drove the verification (null for SDK mounts configured with plain props). When the workflow has a decision graph, the pass/fail result above arrives first, and the approve / decline / review decision follows in a separate workflow.run.completed event.

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
Build with

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://trust.myaza.co/api/kyc/verifications/ver_01j9xyz456/media/selfie \
  --output selfie.jpg

The URLs are scoped to the secret key's organization and environment — a sandbox 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
Build with

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
Build with

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
Build with

  • 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 sandbox before enabling production endpoints.