Best Practices

A webhook handler that works in testing can still fail in production. Retries, network instability, and concurrent deliveries all create conditions that a naive handler won't survive. This page covers what a reliable handler looks like — and the common mistakes that cause silent failures.

Register once

Create the webhook only once during setup, not per resource or in your application logic. Avoid dynamic registration. Don't call the create webhook endpoint from your code.

Verify every request

Every incoming webhook should be verified before your handler processes it. PayMongo signs each request with a secret key associated with your endpoint. If the signature doesn't match, discard the request — PayMongo didn't send it.

To learn more about request verification, visit the Developer Tools Best Practices section.

Respond immediately, process later

Your handler has 30 seconds to acknowledge a delivery before PayMongo treats it as a timeout and retries. Don't do any meaningful work inside the response cycle.

The pattern is always the same — acknowledge first, process after:

app.post("/webhook", (req, res) => {
  res.sendStatus(200); // Acknowledge immediately

  queue.push(req.body); // Hand off to background worker
});

Slow database writes, external API calls, and business logic all belong in a background job. Keeping the handler lean is the single most effective way to prevent unnecessary retries.

Handle duplicate deliveries

PayMongo retries failed deliveries up to 12 times. This means your handler will occasionally receive the same event more than once — either due to a retry after a timeout, or a network hiccup that caused your 2xx not to reach PayMongo.

Use the event ID to deduplicate. Store the IDs of events you've already processed and skip any that arrive again.

const eventId = payload.data.id;

if (await db.processedEvents.exists(eventId)) {
  return res.sendStatus(200); // Acknowledge and skip
}

await db.processedEvents.insert(eventId);
// Continue processing

Idempotency here isn't just good practice — it's a hard requirement for any handler that touches financial data.

Use livemode to separate environments

Every event payload includes a livemode field. Use it to guard against test events reaching your production logic — or live events reaching your test environment.

if (payload.data.attributes.livemode !== expectedLivemode) {
  return res.sendStatus(200); // Acknowledge but do not process
}

Use separate webhook endpoints for your live and test environments rather than relying on this check as your only safeguard.

Only act on event types you expect

Subscribe only to the events your integration needs. If an unexpected event type reaches your handler, acknowledge it with an HTTP status code between 200 and 209, along with a JSON response, and skip it — never return an error for an unrecognized event type, as this will trigger a retry.

const handled = ["payment.paid", "payment.failed", "refund.succeeded"];

if (!handled.includes(payload.data.attributes.type)) {
  return res.sendStatus(200); // Acknowledge and ignore
}

Common pitfalls

Parsing the body before verification. Middleware that parses JSON before your verification step will alter the raw bytes and break signature checks. Make sure your verification runs against the raw body before any parsing.

Returning errors for unrecognized events. A 4xx or 5xx response triggers a retry. If your handler doesn't recognize an event, return 200 and move on — don't let unfamiliar events pile up in your retry queue.

Blocking on slow operations. Any operation that risks breaching the 30-second response window should be offloaded. This includes third-party API calls, sending emails, and complex database transactions.

Using the same endpoint for live and test. Mixing environments creates noise and risks test events triggering real business logic. Always register separate endpoints for each environment.

Not rotating compromised secrets. A leaked signing secret means anyone can forge a verified webhook. Treat it like a password — rotate it immediately if there's any doubt.