Webhook Specifications & Protocols
1. Executive Summary & Ingestion Topology
Webhooks represent the asynchronous nervous system of Debelu, handling incoming signals for payment captures, interbank payouts, and customer messaging.
Because webhooks execute over the open internet, the ingestion architecture adheres to three mandatory enterprise invariants:
- Never Trust the Origin without Cryptographic Proof: Every incoming byte is verified against an HMAC signature calculated over the raw binary request body before parsing.
- Immediate Acknowledgment & Asynchronous Processing: Handlers respond with
HTTP 200 OKwithin $500\text{ms}$ to satisfy provider delivery SLAs, pushing the payload into BullMQ queues for durable background processing. - Strict Idempotency & State Fencing: Duplicate or out-of-order webhook deliveries are quarantined and deduplicated via database unique indexes.
graph TD
subgraph External Event Senders
PAYSTACK[Paystack Payment Cloud]
META[Meta WhatsApp Business Cloud]
end
subgraph Security Perimeter
HMAC[Timing-Safe HMAC SHA-512 Verification]
CHALLENGE[Meta hub.challenge Verification Handshake]
end
subgraph Deduplication & Queueing Plane
DEDUP{processed_webhook_events Table}
QUEUE[BullMQ Background Queue]
end
subgraph Domain Execution & State Machine
ESCROW[Escrow Lock: PaymentService]
RECON[Payout Reconciliation Engine]
CHAT[WhatsApp Support Handler]
end
PAYSTACK -->|POST /paystack-webhook| HMAC
META -->|POST /api/whatsapp/webhook| CHALLENGE
HMAC -->|Signature Valid| DEDUP
CHALLENGE -->|Token Valid| DEDUP
DEDUP -->|Already Processed| ACK[HTTP 200 Fast Return]
DEDUP -->|First Arrival| QUEUE
QUEUE -->|charge.success| ESCROW
QUEUE -->|transfer.*| RECON
QUEUE -->|messages| CHAT2. Inbound Webhooks: Paystack Payments & Transfers
2.1 Supported Event Payloads
Event 1: charge.success (Payment Capture)
Dispatched when a student buyer successfully authorizes an order payment via card, USSD, or direct bank transfer:
{
"event": "charge.success",
"data": {
"id": 302910291,
"domain": "live",
"status": "success",
"reference": "checkout-a1b2c3d4e5f678901234567890123456",
"amount": 1500000,
"gateway_response": "Successful",
"paid_at": "2026-10-05T09:20:15.000Z",
"created_at": "2026-10-05T09:19:45.000Z",
"channel": "card",
"currency": "NGN",
"authorization": {
"authorization_code": "AUTH_8kx9102a",
"bin": "408188",
"last4": "4081",
"exp_month": "12",
"exp_year": "2028",
"channel": "card",
"card_type": "mastercard",
"bank": "Access Bank"
},
"customer": {
"id": 892019,
"email": "student@unilag.edu.ng"
},
"metadata": {
"userId": "10000000-0000-4000-8000-000000000001",
"orderId": "10000000-0000-4000-8000-000000000801",
"settlement": "platform_escrow"
}
}
}Event 2: transfer.success & transfer.failed (Vendor Payouts)
{
"event": "transfer.success",
"data": {
"amount": 3310000,
"currency": "NGN",
"domain": "live",
"id": 1928301,
"integration": 492010,
"reason": "Debelu Vendor Payout",
"reference": "payout-b2c3d4e5f6a178901234567890123456",
"source": "balance",
"status": "success",
"transfer_code": "TRF_9kx0192a8b",
"recipient": {
"recipient_code": "RCP_102938475",
"details": {
"account_number": "1234567890",
"bank_code": "058",
"bank_name": "Guaranty Trust Bank"
}
}
}
}2.2 Cryptographic Signature Verification
Paystack signs every webhook payload using the platform's secret key. Ingestion functions verify the signature using timing-safe comparison:
import crypto from 'node:crypto';
export function verifyPaystackSignature(
rawBodyBuffer: Buffer,
headerSignature: string,
secretKey: string
): boolean {
if (!headerSignature || !secretKey) return false;
const computedHash = crypto
.createHmac('sha512', secretKey)
.update(rawBodyBuffer)
.digest('hex');
if (computedHash.length !== headerSignature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(computedHash, 'utf8'),
Buffer.from(headerSignature, 'utf8')
);
}3. Inbound Webhooks: Meta WhatsApp Business API
Route: POST /api/whatsapp/webhook ([debelu-backend/src/routes/whatsappWebhookRoutes.ts](file:///c:/Users/frank/OneDrive/Desktop/Chisom/Debelu/New%20Debelu%20Marketplace/debelu-backend/src/routes/whatsappWebhookRoutes.ts)).
3.1 Verification Challenge Handshake (GET)
When configuring the webhook in the Meta Developer Portal, Meta sends an initial verification challenge:
router.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === process.env.WHATSAPP_VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
return res.sendStatus(403);
});3.2 Inbound Customer Message Event (POST)
{
"object": "whatsapp_business_account",
"entry": [{
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "2348000000000",
"phone_number_id": "PHONE_NUMBER_ID"
},
"contacts": [{
"profile": { "name": "Chisom" },
"wa_id": "2348012345678"
}],
"messages": [{
"from": "2348012345678",
"id": "wamid.HBgLMjM0...",
"timestamp": "1728114000",
"text": { "body": "Where is my order for UNILAG New Hall?" },
"type": "text"
}]
},
"field": "messages"
}]
}]
}4. Idempotency & State Fencing Engine
4.1 Deduplication Schema
CREATE TABLE public.processed_webhook_events (
id text PRIMARY KEY, -- Provider event ID or gateway reference
provider text NOT NULL, -- 'paystack' or 'whatsapp'
event_type text NOT NULL, -- 'charge.success', 'transfer.success'
payload jsonb NOT NULL,
processed_at timestamptz DEFAULT now()
);4.2 State Machine Fencing
To prevent network latency from allowing an earlier charge.pending or transfer.processing webhook to overwrite an entity that has already shifted to terminal charge.success or transfer.success:
-- Fencing condition evaluated in Postgres update trigger
IF v_existing_status = 'paid' AND p_incoming_status = 'pending' THEN
-- Ignore outdated backward state mutation silently
RETURN;
END IF;5. Dead-Letter Queues (DLQ) & Operator Replay Tooling
When a webhook fails execution due to transient database lock contention (40001) or downstream outages:
- Exponential Backoff: BullMQ retries the event 5 times with jitter ($1\text{s}, 5\text{s}, 25\text{s}, 125\text{s}, 600\text{s}$).
- Dead-Letter Queue (DLQ): If all 5 attempts exhaust, the payload shifts to
dead_letter_webhooksand emits a SEV-2 alert in Sentry. - Manual Operator Replay CLI: On-call engineers safely replay quarantined events using the administrative CLI:bash
fly ssh console -C "npm run replay:webhook -- --id <EVENT_ID>"