Skip to main content

Overview

pawaPass sends webhook notifications to your configured URL when verification events occur. This allows you to track the entire lifecycle of a verification in real-time.

Setup

You can configure your webhook target URL and signing secret in the Backoffice (Production, Sandbox). Each environment is configured separately.

Events

Sent each time a verification’s status changes, from CREATED through STARTED, USER_DATA_COLLECTED, to a final state like APPROVED or DECLINED.This is the primary webhook for monitoring verification progress.
Sent when an agent manually corrects identity document data after a verification has concluded (e.g., fixing a typo in a name or date of birth).Does not fire during initial data collection.

Payload format

All webhook payloads follow the same structure:
{
  "notificationId": "a1b2c3d4-5678-9012-abcd-ef1234567890",
  "type": "VERIFICATION.STATUS_CHANGE",
  "object": "VERIFICATION",
  "createdAt": "2025-08-20T10:15:00.000Z",
  "data": {
    "id": "aB1cD2eF",
    "externalId": "user-zm-98765",
    "requirements": [
      { "type": "DOCUMENT_SCAN" },
      { "type": "FACE_SCAN" }
    ],
    "url": "https://app.pawapass.com/your-company/aB1cD2eF",
    "status": "APPROVED",
    "country": "ZM",
    "phoneNo": "+260971234567",
    "reviewResult": null,
    "metadata": {
      "reason": "kyc-onboarding",
      "channel": "mobile-app",
      "department": "compliance"
    },
    "successUrl": "https://your-website.com/success",
    "errorUrl": "https://your-website.com/error",
    "supportUrl": "https://your-website.com/support",
    "identityDocument": {
      "type": "NATIONAL_ID",
      "country": "ZM",
      "serialNo": "9876543/21/1",
      "nationalNo": "Z12345678",
      "firstName": "Tisa",
      "lastName": "Banda",
      "dateOfBirth": "1997-05-10",
      "placeOfBirth": "Lusaka",
      "nationality": "ZM",
      "expiryAt": "2032-05-09",
      "issuedAt": "2022-05-10",
      "issuingAuthority": "Republic of Zambia",
      "placeOfIssue": "Lusaka",
      "createdAt": "2025-08-20T09:31:00.000Z",
      "updatedAt": "2025-08-20T10:15:00.000Z"
    },
    "faceScan": {
      "ageEstimation": "OVER_25",
      "createdAt": "2025-08-20T09:30:00.000Z",
      "updatedAt": "2025-08-20T09:31:15.000Z"
    },
    "createdBy": "[email protected]",
    "expiresAt": "2025-08-27T09:00:00.000Z",
    "createdAt": "2025-08-20T09:00:00.000Z",
    "updatedAt": "2025-08-20T10:15:00.000Z"
  }
}

Field reference

FieldTypeDescription
countrystring | nullISO 3166-1 alpha-2 country code associated with this verification.
phoneNostring | nullPhone number associated with this verification.
reviewResultobject | nullReview outcome details. null when verification has not been reviewed or no review reason is available. Contains a reason field when present.

Review result reasons

ValueDescription
CAPTURE_ISSUEProblem with the document capture (e.g. blurry photo, glare)
DOCUMENT_VALIDITYThe document could not be validated (e.g. expired, tampered)
IMAGE_QUALITYThe submitted image did not meet quality requirements
GENERICGeneral decline reason

Age estimation values

The ageEstimation field on the face scan can be one of:
ValueAge
UNDER_8Under 8 years
OVER_8Over 8 years
OVER_13Over 13 years
OVER_16Over 16 years
OVER_18Over 18 years
OVER_21Over 21 years
OVER_25Over 25 years
OVER_30Over 30 years
UNKNOWNCould not be estimated

Signature verification

Webhooks include an X-SIGNATURE header containing a SHA256 HMAC signature of the raw request body, signed with your secret key. Verify this signature to confirm the webhook is authentic.
import { createHmac } from 'crypto';

function verifyWebhookSignature(
  requestBody: string,
  signature: string,
  secretKey: string
): boolean {
  const expected = createHmac('sha256', secretKey)
    .update(requestBody, 'utf-8')
    .digest('hex');
  return expected === signature;
}

// In your webhook handler:
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'];
  const isValid = verifyWebhookSignature(req.rawBody, signature, SECRET_KEY);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(req.rawBody);
  // Process the webhook...

  res.status(200).send('OK');
});

Best practices

Verify the signature

Always validate the X-SIGNATURE header before processing the webhook payload.

Return 200 OK quickly

Process the webhook asynchronously if needed. Any non-2xx response triggers retries.

Handle duplicate deliveries

Use the notificationId field to deduplicate. Webhooks may be retried.

Route by event type

Use the type field to determine how to process each event.

Secret rotation

Legacy integrations used a single shared secret for both API authentication and webhook signing. If you have not yet rotated your webhook secret, the legacy secret is still used for signing. Rotating the webhook secret does not affect your API key, and vice versa. These are now fully independent.
Only one webhook secret may be active at a time, so rotating it requires coordination with your application. To rotate:
  1. Prepare your application to accept the new secret.
  2. Generate a new secret in the Backoffice and update your application at the same time.
  3. Verify that incoming webhooks pass signature validation.
There may be a short window where some webhook deliveries fail signature validation on your side due to the timing mismatch. These will be retried automatically using the normal retry schedule (see below), so no notifications will be lost.

Retries

If your endpoint returns a non-2xx status code or times out, pawaPass will retry delivery using capped exponential backoff with jitter:
  • 55 attempts over approximately 1 week
  • Delay starts at 1 second, doubles each time (1s, 2s, 4s, 8s, …), capped at 4 hours
  • Each delay includes ±25% random jitter to prevent thundering herd
  • If all retries are exhausted, the notification is dropped
Use List verifications or Get verification to poll for missed updates.