> ## Documentation Index
> Fetch the complete documentation index at: https://docs.masterid.co.ug/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive webhooks

> Verify signatures, process events durably and handle retries for MasterID webhooks.

MasterID delivers event notifications to a public HTTPS endpoint that you configure in the Developer Console. Your receiver must verify each payload using the one-time signing secret, accept events durably and deduplicate by event ID. Webhooks are an alternative to polling for final verification and collection results.

## Setup and constraints

1. Open the Developer Console and add one public HTTPS webhook URL.
2. Securely store the one-time signing secret immediately. Secrets cannot be recovered after the first display is closed.
3. To replace the URL or signing secret, delete the current webhook and add another.

> [!WARNING]
>
> Redirects and private network destinations are not supported.

## Events

| Event | Description |
| - | - |
| `verification.completed` | A verification has reached a final status. Payload includes `sessionId`, `status` and `charged`. |
| `collection.completed` | A collection has reached a final status. (The inner data shape is not documented beyond the envelope.) |

## Request headers

| Header | Description |
| - | - |
| `X-MasterID-Event-ID` | Unique event identifier. Deduplicate using this value. |
| `X-MasterID-Timestamp` | Unix timestamp in seconds. Reject events older than 5 minutes. |
| `X-MasterID-Signature` | Hex HMAC-SHA256 of `timestamp + '.' + rawBody`. |

## Verify the signature

**1. Read the raw request body**

Read the request body as raw bytes or a string before any JSON parsing. Parsing first can change whitespace and invalidate the signature.

**2. Compute the expected signature**

Compute the HMAC-SHA256 using your signing secret over the string `timestamp + '.' + rawBody`. Produce a lowercase hex digest.

**3. Compare in constant time**

Compare the computed signature against the `X-MasterID-Signature` header using a constant-time comparison function. Reject the request if they do not match.

**4. Check the timestamp**

Reject events with a timestamp older than 5 minutes relative to your server time.

**5. Parse JSON after verification**

Only parse the request body as JSON after the signature and timestamp have passed validation.

**6. Deduplicate**

Track processed `X-MasterID-Event-ID` values and skip duplicates.

**7. Accept durably**

Persist or enqueue the event durably, then return any HTTP `2xx` response. Do not return a success status before you have accepted the event.

## Retry schedule

MasterID retries failed deliveries up to five total attempts.

| Attempt | Approximate delay after previous attempt |
| - | - |
| 1 | Immediate (first delivery) |
| 2 | \~1 minute |
| 3 | \~2 minutes |
| 4 | \~4 minutes |
| 5 | \~8 minutes plus scheduler delay |

API History records each attempt's HTTP status. If callbacks are unavailable, poll saved references as a recovery path.

> [!NOTE]
>
> Completion notifications and background retries require the published release.

## Receiver examples

Each example reads the signing secret from the environment, performs constant-time comparison, enforces the 5-minute timestamp window, deduplicates by event ID and returns `200` after durable acceptance.

```javascript Node.js (Express with express.raw) theme={null}
const crypto = require('node:crypto');
const express = require('express');

const SIGNING_SECRET = process.env.MASTERID_WEBHOOK_SECRET;
if (!SIGNING_SECRET) throw new Error('MASTERID_WEBHOOK_SECRET is required');

const app = express();
// Use raw body parser so the signature matches the exact bytes MasterID sent
app.use('/webhooks/masterid', express.raw({ type: 'application/json' }));

const processedEventIds = new Set(); // Use Redis in production

function constantTimeCompare(a, b) {
  if (a.length !== b.length) return false;
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return result === 0;
}

app.post('/webhooks/masterid', (req, res) => {
  const rawBody = req.body.toString('utf8');
  const eventId = req.headers['x-masterid-event-id'];
  const timestamp = req.headers['x-masterid-timestamp'];
  const signature = req.headers['x-masterid-signature'];

  if (!eventId || !timestamp || !signature) {
    return res.status(400).send('Missing webhook headers');
  }

  const now = Math.floor(Date.now() / 1000);
  const ts = parseInt(timestamp, 10);
  if (Number.isNaN(ts) || now - ts > 300) {
    return res.status(400).send('Stale timestamp');
  }

  const expected = crypto
    .createHmac('sha256', SIGNING_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

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

  if (processedEventIds.has(eventId)) {
    return res.status(200).send('Already processed');
  }

  let payload;
  try {
    payload = JSON.parse(rawBody);
  } catch {
    return res.status(400).send('Invalid JSON');
  }

  // Persist or enqueue the event before acknowledging
  console.log('Event accepted:', payload.event, eventId);
  processedEventIds.add(eventId);

  return res.status(200).send('Accepted');
});

app.listen(3000, () => console.log('Webhook receiver on :3000'));
```

```python Python (Flask with request.get_data) theme={null}
import os, time, hmac, hashlib
from flask import Flask, request, jsonify

SIGNING_SECRET = os.environ.get("MASTERID_WEBHOOK_SECRET")
if not SIGNING_SECRET:
    raise RuntimeError("MASTERID_WEBHOOK_SECRET is required")

app = Flask(__name__)
processed_event_ids = set()  # Use Redis in production

def constant_time_compare(a: str, b: str) -> bool:
    return hmac.compare_digest(a, b)

@app.route("/webhooks/masterid", methods=["POST"])
def masterid_webhook():
    raw_body = request.get_data(as_text=True)
    event_id = request.headers.get("X-MasterID-Event-ID")
    timestamp = request.headers.get("X-MasterID-Timestamp")
    signature = request.headers.get("X-MasterID-Signature")

    if not event_id or not timestamp or not signature:
        return "Missing webhook headers", 400

    now = int(time.time())
    try:
        ts = int(timestamp)
    except ValueError:
        return "Invalid timestamp", 400
    if now - ts > 300:
        return "Stale timestamp", 400

    expected = hmac.new(
        SIGNING_SECRET.encode("utf-8"),
        f"{timestamp}.{raw_body}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    if not constant_time_compare(expected, signature):
        return "Invalid signature", 401

    if event_id in processed_event_ids:
        return "Already processed", 200

    try:
        payload = request.get_json(force=True)
    except Exception:
        return "Invalid JSON", 400

    # Persist or enqueue the event before acknowledging
    print("Event accepted:", payload.get("event"), event_id)
    processed_event_ids.add(event_id)

    return "Accepted", 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=3000)
```

## Integration checklist

> **Public HTTPS endpoint**
>
> Your URL must be reachable from the internet over HTTPS with a valid certificate.

> **Raw body preservation**
>
> Read the exact raw body before JSON parsing to preserve the signature input.

> **Constant-time compare**
>
> Use `crypto.timingSafeEqual` or `hmac.compare_digest` to avoid timing attacks.

> **Timestamp tolerance**
>
> Reject events with a timestamp older than 5 minutes to prevent replay attacks.

> **Deduplicate events**
>
> Track `X-MasterID-Event-ID` and skip duplicates. Delivery is at least once.

> **Durable acceptance**
>
> Write or queue the event before returning HTTP 2xx. Do not trust repeated events as new operations.

## Fallback path

If your webhook receiver is unavailable, poll saved references as a recovery path:

* [Run a verification](https://docs.masterid.co.ug/guides/verifications.md) and poll `GET /verifications/{sessionId}`.
* [Collect mobile money payments](https://docs.masterid.co.ug/guides/payment-collection.md) and poll `GET /collections/{reference}`.


## Related topics

- [Webhook Events](https://docs.masterid.co.ug/api-reference/webhooks/events.md)
- [API Overview](https://docs.masterid.co.ug/api-reference/overview.md)
- [MasterID Integration API](https://docs.masterid.co.ug/index.md)
- [Run a verification](https://docs.masterid.co.ug/guides/verifications.md)
- [Collect mobile money payments](https://docs.masterid.co.ug/guides/payment-collection.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.