Skip to main content
All articles
Technical5 min read

Getting your webhook integration right

Getting your webhook integration right: the five mistakes found 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. The Webhooks guide in the API docs gives the exact computation: an HMAC-SHA256 over the X-CHARI-TIMESTAMP timestamp and the raw body, sent in X-CHARI-SIGNATURE. The simplest way to check that your route really keeps the raw body is to send yourself a test event: Start in test mode opens the sandbox as soon as you sign up, with no approval to wait for.

2. Subscribing to everything

The API lets you declare an explicit list of events (see the webhooks reference). 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, in the Chari-Event-Id header. 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. It is payment idempotency, applied on the receiving side.

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 eventId = request.headers.get('Chari-Event-Id');
  const known = await db.events.findByEventId(eventId);
  if (known) return new Response('ok');           // already handled

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

  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, from the portal.

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. The test event and the delivery log are walked through step by step in testing your webhooks locally.

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.

Next step

Start in test mode takes three fields — name, work email, company — then a password chosen from the activation link sent by email. You then create your test key from the portal, declare your endpoint and pay with the test card to receive a first signed event — free, with no time limit. The payment API in Morocco guide places webhooks within the full integration.

Written by ChariPay team.

Read next

Technical5 min read

Never charge twice — idempotency explained

Payment idempotency explained: how the Idempotency-Key header and externalId stop you charging a customer twice when the network drops mid-call.

Guide4 min read

Sandbox to production: the go-live list

Moving from sandbox to production: the checklist before you switch keys — signed webhooks, idempotency, secrets, the classic oversights.

Ready to try it yourself?

Create your account and take test-mode payments today: it is free, with no approval to wait for. A question? Our team supports merchants and developers alike.

  • Free, no commitment
  • Test mode from sign-up
  • No approval to wait for