> ## Documentation Index
> Fetch the complete documentation index at: https://developer.verilock.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time event notifications with signed payloads.

Webhooks deliver real-time HTTP POST notifications when events occur in your account, eliminating the need to poll for status changes.

## Events

| Event                     | Trigger                                                                    |
| ------------------------- | -------------------------------------------------------------------------- |
| `session.completed`       | Verification session finished processing                                   |
| `session.approved`        | Session approved -- identity verified                                      |
| `session.declined`        | Session declined -- failed checks or suspicious data                       |
| `session.expired`         | Session expired before completion                                          |
| `aml.completed`           | AML screening finished processing                                          |
| `aml.match_found`         | Potential matches found against watchlists or PEP databases                |
| `aml.monitoring.match`    | Continuous monitoring detected new AML matches                             |
| `transaction.flagged`     | Transaction flagged for manual review                                      |
| `transaction.blocked`     | Transaction automatically blocked by risk rules                            |
| `biometric.completed`     | Biometric authentication finished processing                               |
| `database.validated`      | Database validation completed                                              |
| `face.duplicate_found`    | Face search detected a duplicate identity                                  |
| `credential.created`      | Reusable KYC credential issued                                             |
| `credential.shared`       | Credential shared with another organization                                |
| `credential.verified`     | Shared credential verified by another organization                         |
| `credential.revoked`      | Credential revoked                                                         |
| `video.completed`         | Video recording or live agent call finished                                |
| `video.reviewed`          | Video KYC reviewed and decision made                                       |
| `liveness.failed`         | Active liveness challenge failed                                           |
| `risk.threshold_exceeded` | Session risk score exceeded configured threshold                           |
| `ambient.alert`           | Ambient verification detected a risk change (warning or critical severity) |

## Payload Structure

```json session.completed theme={null}
{
  "event": "session.completed",
  "data": {
    "session_id": "ses_a1b2c3d4e5f6",
    "status": "approved",
    "risk_score": 12,
    "decision": "approved",
    "completed_at": "2026-03-15T14:30:00Z"
  },
  "timestamp": "2026-03-15T14:30:01Z",
  "webhook_id": "wh_evt_9f8e7d6c5b4a"
}
```

| Field        | Type   | Description                                   |
| ------------ | ------ | --------------------------------------------- |
| `event`      | string | Event type identifier                         |
| `data`       | object | Event-specific payload (varies by event type) |
| `timestamp`  | string | ISO 8601 timestamp                            |
| `webhook_id` | string | Unique delivery ID -- use for idempotency     |

## Signature Verification

Every webhook includes an `X-Verilock-Signature` header with an HMAC-SHA256 signature.

<Warning>
  **Always verify signatures** before processing webhook data. Your webhook secret is available in **Dashboard > Settings > Webhooks**.
</Warning>

<CodeGroup>
  ```php PHP theme={null}
  $payload   = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
  $secret    = getenv('VERILOCK_WEBHOOK_SECRET');

  $expected = hash_hmac('sha256', $payload, $secret);

  if (!hash_equals($expected, $signature)) {
      http_response_code(401);
      exit('Invalid signature');
  }

  $event = json_decode($payload, true);

  match ($event['event']) {
      'session.completed'    => handleSession($event['data']),
      'aml.match_found'      => handleAmlMatch($event['data']),
      'transaction.blocked'  => handleBlocked($event['data']),
      'ambient.alert'        => handleAmbientAlert($event['data']),
      default                => null,
  };

  http_response_code(200);
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import json

  def verify_webhook(request):
      payload = request.body
      signature = request.headers.get('X-Verilock-Signature', '')
      secret = os.environ['VERILOCK_WEBHOOK_SECRET']

      expected = hmac.new(
          secret.encode(), payload, hashlib.sha256
      ).hexdigest()

      if not hmac.compare_digest(expected, signature):
          return HttpResponse(status=401)

      event = json.loads(payload)
      # Process event...
      return HttpResponse(status=200)
  ```

  ```javascript Node.js theme={null}
  import crypto from 'crypto';

  app.post('/webhooks/verilock', (req, res) => {
    const payload = JSON.stringify(req.body);
    const signature = req.headers['x-webhook-signature'];
    const secret = process.env.VERILOCK_WEBHOOK_SECRET;

    const expected = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');

    if (!crypto.timingSafeEqual(
      Buffer.from(expected), Buffer.from(signature)
    )) {
      return res.status(401).send('Invalid signature');
    }

    // Process event...
    res.status(200).send('OK');
  });
  ```
</CodeGroup>

## Retry Policy

Failed deliveries (non-2xx response within 10 seconds) are retried with exponential backoff:

| Attempt   | Delay | Elapsed |
| --------- | ----- | ------- |
| 1st retry | 10s   | \~10s   |
| 2nd retry | 60s   | \~70s   |
| 3rd retry | 5min  | \~6min  |

After 3 failed retries, the delivery is marked as failed. View and retry failed deliveries from your dashboard.

## Best Practices

<CardGroup cols={3}>
  <Card title="Respond immediately" icon="bolt">
    Return `200` as soon as you receive the webhook. Process events asynchronously via a job queue.
  </Card>

  <Card title="Idempotency" icon="fingerprint">
    Store processed `webhook_id` values and skip duplicates to ensure exactly-once processing.
  </Card>

  <Card title="Verify signatures" icon="shield-check">
    Always validate `X-Verilock-Signature` using timing-safe comparison before processing any data.
  </Card>
</CardGroup>
