Skip to main content
All articles
Technical4 min read

Getting your webhook integration right

Five mistakes that show up in almost every integration, and the fifteen-line handler that avoids all of them.

A payment does not complete when you call it. The buyer goes through their bank, clears an authentication, comes back — or does not. The result reaches you by webhook, and that notification is the one to trust.

Which means the quality of your webhook handler decides the reliability of your entire payment flow. Here are the five mistakes we see most often.

1. Verifying the signature after parsing the JSON

This is the most frequent and the most silent mistake. The signature is computed over the raw body of the request. If your framework parses the JSON before you touch it, and you then re-serialise the object to verify, you get different bytes — a space, a key order, a reformatted number — and the signature no longer matches.

Configure your route to keep the raw body, verify the signature against it, and only parse afterwards.

2. Subscribing to everything

The API lets you declare an explicit list of events. The temptation is to tick them all "just in case".

Every event you subscribe to is an event your code must know how to ignore cleanly. A switch with no default branch, a log that swells, an alert that fires for nothing: the short list is the safe list. Subscribe to what you handle, and nothing more.

3. Not deduplicating

The same notification can arrive twice. That is not a defect, it is a guarantee: we would rather deliver twice than lose once.

Every delivery carries an event id. Store it — a table with a unique constraint is enough — and return immediately if you have seen it before. Without that, one lost acknowledgement makes you ship the same order twice.

4. Doing the heavy work inside the request

Sending an email, generating a PDF invoice, calling three internal services: if all of that happens before your response, your handler takes seconds. We then treat the delivery as failed and retry. You redo the work. The cycle spirals, and the endpoint ends up suspended.

Acknowledge with a 200 as soon as you have written the event somewhere, and do the rest in the background.

5. Trusting the redirect over the webhook

That is not a webhook problem, it is the problem webhooks solve. A buyer who closes the tab after paying will never see your return page. If your order is only confirmed there, it never will be — while the money did arrive.

The handler, in fifteen lines

javascript
export async function POST(request) {
  const raw = await request.text();               // raw body, first
  if (!verifySignature(raw, request.headers)) {
    return new Response('bad signature', { status: 401 });
  }

  const event = JSON.parse(raw);
  const known = await db.events.findByEventId(event.id);
  if (known) return new Response('ok');           // already handled

  await db.events.insert({ eventId: event.id, payload: event });
  await queue.push('handle-payment-event', event.id);

  return new Response('ok');                      // acknowledged, rest follows
}

Verify, deduplicate, acknowledge, delegate. The rest of your business logic lives in the background task, where it can fail and be retried without any consequence for the delivery.

Replaying a failed delivery

Your server was down for maintenance, a delivery failed: nothing is lost. The portal's delivery log shows every delivery — its attempt count, your server's last response (HTTP code included) and the exact body of the event. Replay is one click away — or one API call, if you want it in your own monitoring.

Two habits make replays harmless. First, the deduplication from point 3: a replayed event carries the same identifier, your handler recognises it and exits. Second, never fix an incident by patching your database by hand *and then* replaying the event — you would apply the same effect twice. Replay first, verify after.

An endpoint that keeps failing ends up suspended, to protect you as much as us. Recovery follows the same path: fix, test with a test event, re-enable, then replay the missed deliveries in chronological order.

Securing the endpoint beyond the signature

The HMAC signature authenticates the content; it does not replace basic hygiene around it.

  • HTTPS only — the API refuses to register a plain-HTTP endpoint anyway.
  • Check the timestamp: every delivery is dated. Reject anything older than a few minutes and you close the door on replaying a captured request.
  • Minimal responses: your endpoint has nothing to say. An empty 200 is enough; a detailed error body mostly informs an attacker.
  • One secret per environment: the sandbox signing secret and the production one are distinct. A test event signed with the sandbox key must never be accepted by your production endpoint.

None of these rules costs more than a few lines. Together they make a webhook endpoint exposed to the Internet as safe as the rest of your integration.

One last piece of advice

Send yourself a test event before opening production. The API offers it on every declared endpoint, and the delivery log tells you what your server answered. Discovering a signature problem then costs five minutes; discovering it in production costs a day of investigation.

Written by ChariPay team.

Read next

A question about your integration?

Our team supports merchants and developers alike, from the first test through to go-live.