Webhooks are the moment a payment system hands responsibility to your code. They arrive over the public internet, sometimes twice, sometimes late. Designing for that is the difference between a reliable integration and a support queue.
What FeitPay guarantees
- At-least-once delivery. Every event is written to an outbox in the same transaction as the state change, then delivered with exponential backoff and jitter. After the maximum number of attempts, failed deliveries are kept in a dead-letter list you can replay.
- A stable identity. Each event carries an
event_id. Retries reuse it. - A verifiable signature. The
FeitPay-Signatureheader contains a timestamp and an HMAC-SHA256 overtimestamp + "." + raw_body, computed with your endpoint’s secret.
What your endpoint should do
import { verifyWebhook } from '@feitpay/webhooks';
// Rejects stale timestamps (300 s tolerance) and accepts the previous secret during rotation.
const event = await verifyWebhook({
payload: rawBody, // the exact bytes received
header: request.headers['feitpay-signature'],
secrets: [process.env.FEITPAY_WEBHOOK_SECRET, process.env.FEITPAY_WEBHOOK_SECRET_PREVIOUS].filter(Boolean),
});
if (await alreadyProcessed(event.event_id)) return ok();
Three habits matter most:
- Verify against the raw body. Re-serialised JSON will not match the signature.
- Deduplicate on
event_id. Treat a repeated event as success, not an error. - Confirm before irreversible actions. For high-value fulfilment, fetch the payment from the API and check its state before shipping.
Rotating secrets without downtime
When you rotate an endpoint secret, deliveries are signed with both the new and the previous secret for a short overlap window, so verifiers can switch over without dropping events. Secrets are shown once, encrypted at rest and never logged.