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:
| Header | Description |
|---|---|
| X-Webhook-Signature | Hex HMAC-SHA256 of the raw request body, keyed with your client secret |
| X-Client-Id | The client ID the event belongs to |
| Content-Type | application/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
| Field | Type | Description |
|---|---|---|
| sessionId | string | The consent/verification session |
| status | string | APPROVED on success |
| timestamp | number | Delivery time (ms) — use for replay protection |
| data.userId | string | The approving user (phone) |
| data.authorizationCode | string | OAUTH only — exchange at /v1/oauth/token |
| data.claims | object | OAUTH only — verified claim values |
| data.claimsApproved | string[] | OAUTH only — approved claim keys |
| hash | string | Integrity 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.