Guide

Webhooks

When a user approves a consent or verification, MID delivers the result to the callbackUrl you supplied. Each delivery is signed so you can trust it came from MID.

Delivery & headers

MID sends a POST with a JSON body and these headers:

HeaderDescription
X-Webhook-SignatureHex HMAC-SHA256 of the raw request body, keyed with your client secret
X-Client-IdThe client ID the event belongs to
Content-Typeapplication/json

Verify the signature

Compute the HMAC over the raw request body (before JSON parsing) and compare it to X-Webhook-Signature using a constant-time check. Reject anything that doesn't match.

Express handler (Node.js)
import crypto from 'crypto';

// Capture the raw body, e.g. express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })
app.post('/api/mid/webhook', (req, res) => {
  const signature = req.get('X-Webhook-Signature') || '';
  const expected = crypto
    .createHmac('sha256', process.env.MID_CLIENT_SECRET)
    .update(req.rawBody)
    .digest('hex');

  const ok = signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!ok) return res.status(401).json({ error: 'invalid signature' });

  const { sessionId, status, data } = req.body;
  // Reject stale events (replay protection)
  if (Math.abs(Date.now() - (req.body.timestamp || 0)) > 5 * 60 * 1000) {
    return res.status(401).json({ error: 'stale webhook' });
  }

  if (status === 'APPROVED') {
    // data.claims holds verified values for OAUTH requests
    fulfil(sessionId, data);
  }
  res.json({ received: true });   // respond 200 quickly
});

Use the raw body

Verify against the exact bytes MID sent. Re-serializing a parsed object can change key order or spacing and break the signature.

Payload schema

FieldTypeDescription
sessionIdstringThe consent/verification session
statusstringAPPROVED on success
timestampnumberDelivery time (ms) — use for replay protection
data.userIdstringThe approving user (phone)
data.authorizationCodestringOAUTH only — exchange at /v1/oauth/token
data.claimsobjectOAUTH only — verified claim values
data.claimsApprovedstring[]OAUTH only — approved claim keys
hashstringIntegrity hash of the session/transaction
Example payload
{
  "sessionId": "b0f2…",
  "status": "APPROVED",
  "timestamp": 1717171717000,
  "data": {
    "userId": "+2348012345678",
    "mobile": "+2348012345678",
    "authorizationCode": "mac_…",
    "claimsApproved": ["name", "nin", "liveSelfie"],
    "claims": {
      "name": { "value": "John Doe", "verified": true, "verificationStatus": "verified" }
    }
  },
  "hash": "…"
}

Best practices

Verify before you trust

Always check X-Webhook-Signature before acting on a payload.

Protect against replay

Reject events whose timestamp is more than 5 minutes old, and de-duplicate by sessionId.

Respond fast

Acknowledge with 200 quickly and do heavy work asynchronously.

Be idempotent

The same sessionId may arrive more than once — make handling safe to repeat.