Integrations
Webhooks
Webhooks Overview

Webhooks Overview

Catalog of all inbound webhooks Eziseller accepts from third parties (Meta, Razorpay, Shiprocket) and how we verify, dispatch, and acknowledge them. Audience: new dev with Node/Express experience.

1. Overview

Eziseller receives push notifications from external providers for events that can't be polled cheaply: incoming WhatsApp/Instagram messages, Meta template/verification status changes, and Razorpay payment captures. All webhooks land on the Express backend, are signature-verified (HMAC SHA-256 with a provider-specific secret), dispatched to a handler that writes to Postgres via Prisma, and acknowledged with HTTP 200. Most inbound traffic by volume is Meta (WhatsApp + Instagram); Razorpay is low-volume but payment-critical. Shiprocket is poll-based today — we have no inbound Shiprocket webhook.

2. Architecture

Express mounts /api/* behind CORS + maintenance middleware; /webhooks/meta is mounted outside /api on purpose — Meta's webhook console does not tolerate arbitrary prefixes and must not be blocked by the maintenance middleware.

3. Webhook endpoints

PathSourceMethodVerificationPurpose
GET /webhooks/metaMetaGEThub.verify_token vs WHATSAPP_VERIFY_TOKEN / INSTAGRAM_VERIFY_TOKEN (plus per-user DB tokens)Subscription handshake — echo hub.challenge
POST /webhooks/metaMeta (WhatsApp + Instagram)POSTHMAC SHA-256 via X-Hub-Signature-256, secret chosen by body.object (WHATSAPP_APP_SECRET or INSTAGRAM_APP_SECRET)Messages, statuses, template updates, business verification
POST /api/whatsapp-business-webhookLegacy MetaPOSTHMAC SHA-256 with WHATSAPP_APP_SECRETDeprecated — superseded by /webhooks/meta. Kept for old tenant configs.
GET /api/whatsapp-business-webhookLegacy MetaGETSame token flow as aboveDeprecated handshake
POST /api/instagram-dm/webhookMeta (Instagram DM)POSTHMAC SHA-256 with INSTAGRAM_APP_SECRETNarrow Instagram DM path (separate service)
GET /api/instagram-dm/webhookMetaGETINSTAGRAM_VERIFY_TOKENHandshake
POST /api/webhooks/whatsappLegacyPOSTWhatsAppWebhookService.verifySignatureLegacy unified route (pre-multi-tenant)
POST /api/webhooks/instagramLegacyPOSTInstagramWebhookService.verifySignatureLegacy unified route
POST /api/public/catalog/webhooks/razorpayRazorpayPOSTCurrently none (see gotchas) — only handles event === 'payment.captured'Fallback for payments where browser closed before /checkout finished
POST /api/test-webhook/*, POST /api/webhook-simulator/*Dev toolsPOSTNoneLocal simulation — not for production traffic

Shiprocket has no inbound webhook — tracking is pulled via GET /courier/track/awb/:awb (see shipping.ts:L755).

4. Key flows

4.1 Meta webhook (canonical path)

4.2 Razorpay payment.captured fallback

The normal happy path creates the order synchronously in POST /api/public/catalog/checkout; the webhook is only for browser-abandonment recovery.

5. Signature verification pattern

All Meta endpoints follow the same shape (meta-webhooks.ts:L205-L256):

  1. Read X-Hub-Signature-256 header (format sha256=<hex>).
  2. Pick app secret based on body.object (whatsapp_business_accountWHATSAPP_APP_SECRET; instagram/pageINSTAGRAM_APP_SECRET).
  3. Compute HMAC-SHA256(secret, payload).
  4. Compare with crypto.timingSafeEqual.
  5. On NODE_ENV=development, verification is skipped entirely (returns true).

Razorpay's handler does not currently verify the X-Razorpay-Signature header against RAZORPAY_WEBHOOK_SECRET — see gotchas.

6. Key files

7. Env vars & config

VarRequiredPurposeWhat breaks
WHATSAPP_APP_SECRETprodHMAC secret for whatsapp_business_account payloadsEvery WhatsApp webhook returns 401
INSTAGRAM_APP_SECRETprodHMAC secret for instagram / page payloadsEvery Instagram webhook returns 401
WHATSAPP_VERIFY_TOKENprodSubscription handshake for WhatsAppMeta can't (re)subscribe the endpoint
INSTAGRAM_VERIFY_TOKENprodSubscription handshake for InstagramSame, for Instagram
RAZORPAY_WEBHOOK_SECRETprodShould gate /webhooks/razorpay — currently not enforcedAnyone who knows the URL can POST fake payment events
NODE_ENVdevelopment disables Meta signature verificationDo not set in prod
FRONTEND_URLprodCORS origin for /api/* (not for /webhooks/meta)

8. Gotchas & troubleshooting

  • Signature verified against re-stringified body, not raw bodyverifyWebhookSignature passes JSON.stringify(req.body) through express.json() output (meta-webhooks.ts:L100-L104). This works only while Node's JSON.stringify happens to produce byte-identical output to Meta's. Any whitespace/key-order difference silently fails verification. Fix: capture raw body via express.raw({ type: 'application/json' }) on this route and HMAC the Buffer directly. Signature verify is skipped in dev so the bug is invisible locally.
  • /webhooks/meta must stay outside /api → The maintenance-mode middleware (app.use('/api', ...) in server.ts:L82) would return 503 to Meta, which treats 5xx as retryable and eventually disables the subscription.
  • Handlers swallow errors and return 200meta-webhooks.ts catches its own errors and returns 200 {success:false} (meta-webhooks.ts:L131-L134). Meta will not retry, so a DB outage during a webhook permanently loses that message. Check backend logs, not Meta's delivery dashboard.
  • No dedupe → Meta may redeliver the same entry.id / message id; we only dedupe implicitly via ConversationService.storeMessage on externalId. Other handlers (template status, business verification) use updateMany which is idempotent but will repeat side effects (notifications) on redelivery.
  • Dev signature skipNODE_ENV=development bypasses HMAC. Do not run staging with NODE_ENV=development.
  • Legacy routes still mounted/api/whatsapp-business-webhook, /api/webhooks/whatsapp, /api/webhooks/instagram. All three are superseded by /webhooks/meta. Don't add new logic to them.
  • Razorpay signature not checked → The handler reads req.body.event directly. In production, add HMAC check against RAZORPAY_WEBHOOK_SECRET before trusting the payload. Low risk only because the webhook just flags a notification, doesn't create orders.
  • Raw body for Razorpay too → If you add the signature check, you need express.raw() for that single route; adding it globally will break every other /api/* route that expects parsed JSON.

9. Extension points

  • Adding a new Meta event type: extend the change.field switch in processWhatsAppWebhook / processInstagramWebhook (meta-webhooks.ts:L302-L310).
  • Adding a new provider: create backend/routes/<provider>-webhook.ts, mount it outside /api if the provider requires a fixed path, and implement raw-body HMAC verification following the Meta pattern — do not copy the current JSON.stringify(body) approach.
  • Moving to queue-based handling: today handlers run inline before the 200 is sent. For heavier flows, push {body, headers} onto a queue after signature verify and 200 immediately; retain verification on the sync path because once we 200 we lose provider retries.
  • Proper dedupe: add a WebhookEvent table keyed by (provider, externalId) and short-circuit duplicates before dispatching handlers.

10. Related docs