Websites13 min read

Payment webhooks: signature, idempotency and security (technical guide 2026)

Mohamed Bah·Fondateur, Kolonell
June 10, 2026
Share:
Payment webhooks: signature, idempotency and security (technical guide 2026)

Payment webhooks: signature, idempotency and security (technical guide 2026)

Websites

A payment webhook is the server-to-server call the provider (Orange Money, Wave, Stripe) sends to your backend when a payment changes status. It, not the browser redirect, is authoritative. A poorly secured webhook opens the door to fake payment confirmations and duplicate processing.

This guide covers the four pillars of a robust webhook: signature verification, idempotency, correct HTTP response, and retry handling.

Anatomy of a webhook payload

A webhook arrives via a POST request to your endpoint, with a JSON body and signature headers.

FieldTypeDescription
event_idstringUnique event identifier, the idempotency key
event_typestringType, for example payment.succeeded
order_idstringOrder reference on the merchant side
amountintegerAmount in FCFA
statusstringSUCCESS, FAILED, PENDING
created_attimestampEvent timestamp
signaturestringHMAC of the body, in the header

Headers to read

The signature travels in a dedicated header, often alongside a timestamp to block replay attacks.

HeaderRole
X-SignatureHMAC SHA-256 of the raw body
X-TimestampSend timestamp, for anti-replay window
Content-Typeapplication/json

HMAC signature verification

The signature proves the webhook truly comes from the provider and the body was not altered. The computation uses the raw body, never the re-serialized JSON.

  • Read the raw request body, byte for byte.
  • Read the X-Signature and X-Timestamp headers.
  • Compute HMAC SHA-256 of the body with your webhook secret.
  • Compare the computed signature to the received one in constant time.
  • Reject if the timestamp is older than 5 minutes (anti-replay).

Classic mistake

Many bugs come from a framework parsing the JSON before you read the raw body. Re-serializing changes whitespace and breaks the HMAC. Always read the raw body before any parsing.

Idempotency: process exactly once

Providers resend the same webhook several times (retries). Your logic must be idempotent: processing an event_id twice must never charge twice nor deliver twice.

StrategyMechanismBenefit
Unique key in databaseUnique index on event_idImmediate rejection of duplicates
Per-order lockLock on order_id during processingAvoids race conditions
Transactional statusAtomic PENDING to PAID transitionNo double processing

The simplest and most robust: a unique index on event_id. If the insert fails due to a duplicate, return 200 without reprocessing.

HTTP response and status codes

What you return decides whether the provider retries or not. A wrong response triggers needless retries or, worse, loses the event.

Returned codeProvider interpretationWhen to use it
200Received and processedSuccess, or already-processed duplicate
400Invalid requestInvalid signature, corrupted payload
401UnauthenticatedMissing or wrong signature
409ConflictProcessing already in progress (optional)
500Server errorInternal bug, triggers a retry

Need a professional website?

Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.

Return 200 as quickly as possible, then process in the background if the work is long. A webhook that responds in more than a few seconds risks a timeout on the provider side.

Retry strategy

If you respond with a 5xx error or do not respond, the provider retries on an exponential backoff.

AttemptIndicative delayBackend action
1immediateProcess or log the failure
2about 1 minuteCheck idempotency
3about 5 minutesCheck idempotency
4about 30 minutesAlert support if still failing
5 and beyondup to 24 hManual reconciliation

Concrete example

A customer pays a 40,000 FCFA order. The provider sends payment.succeeded. Your server reads the raw body, verifies the HMAC, checks that event_id is not in the database, inserts the event, moves the order from PENDING to PAID in a transaction, then returns 200. One minute later a duplicate retry arrives with the same event_id: the insert fails on the unique index, you return 200 immediately, no double delivery. Total: one effective processing, zero double charge.

Webhook security checklist

ControlExpected status
HMAC verification on raw bodymandatory
Constant-time signature comparisonmandatory
Anti-replay window on the timestampmandatory
Unique index on event_idmandatory
HTTPS-only endpointmandatory
Fast 200 response, async processingrecommended
Logging of every eventrecommended

FAQ

Why not rely on the browser redirect?

Because the customer may close the tab or lose connectivity before returning. Only the server-to-server webhook guarantees you are notified of the real status.

What content should the HMAC be computed on?

On the raw request body, byte for byte, before any JSON parsing. Re-serializing the JSON changes whitespace and invalidates the signature.

How do I avoid double processing?

A unique index on event_id in the database. If the event already exists, return 200 without reprocessing. It is the simplest and most reliable method.

What should I return if the signature is invalid?

A 401 or 400 code, without processing the event. Never return 200 for a webhook whose signature does not match.

How long does a provider retry?

Usually up to 24 hours with exponential backoff. After total failure, plan a manual reconciliation or a replay endpoint.

Let's talk about your project. Kolonell builds secure payment backends with signature verification and idempotency by default. WhatsApp +221 77 596 93 33.

Tags:#webhooks#payment#security#hmac#idempotency#backend#api#integration
Share:

Mohamed Bah

Fondateur, Kolonell

Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.