Skip to content

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:

  1. 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.
  2. Immediate Acknowledgment & Asynchronous Processing: Handlers respond with HTTP 200 OK within $500\text{ms}$ to satisfy provider delivery SLAs, pushing the payload into BullMQ queues for durable background processing.
  3. Strict Idempotency & State Fencing: Duplicate or out-of-order webhook deliveries are quarantined and deduplicated via database unique indexes.
mermaid
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| CHAT

2. 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:

json
{
  "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": "[email protected]"
    },
    "metadata": {
      "userId": "10000000-0000-4000-8000-000000000001",
      "orderId": "10000000-0000-4000-8000-000000000801",
      "settlement": "platform_escrow"
    }
  }
}

Event 2: transfer.success & transfer.failed (Vendor Payouts) ​

json
{
  "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:

typescript
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:

typescript
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) ​

json
{
  "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 ​

sql
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:

sql
-- 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:

  1. 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}$).
  2. Dead-Letter Queue (DLQ): If all 5 attempts exhaust, the payload shifts to dead_letter_webhooks and emits a SEV-2 alert in Sentry.
  3. 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>"

Released under Proprietary Enterprise License.