Skip to navigation

Verify and handle webhooks

Verify wamo webhook signatures and handle deliveries and retries reliably.
View as Markdown

wamo signs every webhook request according to the Standard Webhooks specification. Always verify the signature before trusting a request.

Request headers

Each request includes these headers:

HeaderDescription
webhook-idUnique identifier for the message. Stays the same across retries of the same event.
webhook-timestampUnix timestamp (seconds) of the delivery attempt.
webhook-signatureSpace-separated list of signatures, each in the form v1,<base64 signature>.
webhook-topicThe event type, for example transaction.status_changed. It is the same as the type field in the body.

Verify with a library

The simplest approach is an official Standard Webhooks library. Pass the raw request body, the headers, and your endpoint’s signing secret.

import express from "express";
import { Webhook } from "standardwebhooks";
const app = express();
const wh = new Webhook(process.env.WAMO_WEBHOOK_SECRET);
app.post("/webhooks/wamo", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = wh.verify(req.body.toString(), req.headers);
} catch (err) {
return res.status(400).send("Invalid signature");
}
if (event.type === "transaction.status_changed") {
// Handle the transaction status update
}
res.sendStatus(200);
});

Verify against the raw request body exactly as received. Parsing the JSON and serializing it again can change the bytes and make verification fail.

Verify manually

If you can’t use a library:

1

Build the signed content

Concatenate the webhook-id header, the webhook-timestamp header, and the raw body, separated by periods: {webhook-id}.{webhook-timestamp}.{body}.

2

Compute the expected signature

Remove the whsec_ prefix from your secret and base64-decode the rest. Compute an HMAC-SHA256 of the signed content with that key, then base64-encode the result.

3

Compare signatures

Split webhook-signature on spaces and strip the v1, prefix from each entry. The request is valid if any entry matches your expected signature. Use a constant-time comparison.

4

Check the timestamp

Reject requests whose webhook-timestamp is too far from the current time (for example, more than 5 minutes) to protect against replay attacks.

Respond quickly

wamo treats any 2xx response as a successful delivery. Any other status code, a timeout, or a connection error counts as a failure. Return a 2xx response as soon as you’ve verified and stored the event, and do slower processing asynchronously.

Retries and duplicates

Failed deliveries are retried automatically with exponential backoff. Delivery is at least once, so your endpoint can receive the same event more than once.

  • Use the webhook-id header as an idempotency key and skip events you’ve already processed. A retry of the same event has the same webhook-id.
  • Don’t rely on events arriving in order. For a transaction, COMPLETED and FAILED are final statuses: if a PENDING event arrives after one of them, ignore it.