Skip to content

// Communication & Integration · 04

Webhooks

Providerleads toHTTP POSTYour endpoint

Don't call us, we'll call you. An HTTP POST when something happens.

How it fits together

Webhooks structure«external»Payment providerretries if no 2xxat least onceYour endpointverify HMAC, raw bodyreject if older than 5 minstore event id onceMessage queueBackground workerdedupe on event idre-fetch if order matters1 POST event2 200 OK3 enqueue4 process
uses / callscreates· numbered steps replay in order
  • Payment provider uses / calls Your endpoint (1 POST event)
  • Your endpoint uses / calls Payment provider (2 200 OK)
  • Your endpoint creates Message queue (3 enqueue)
  • Message queue uses / calls Background worker (4 process)

Spot it

  • Polling an API every minute for changes
  • A payment provider must tell you it settled
  • Status updates lag behind the poll interval

It costs

You expose a public endpoint and rely on the sender's retries for delivery.

In code

public sealed class PaymentWebhookHandler(IWebhookQueue queue, IEventLog events, string secret){    public async Task<int> HandleAsync(        string eventId,        long timestamp,        string signatureHex,        byte[] rawBody,        CancellationToken ct)    {        var age = DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeSeconds(timestamp);        if (age.Duration() > TimeSpan.FromMinutes(5)) return 400;​        var prefix = Encoding.UTF8.GetBytes($"{timestamp}.");        var key = Encoding.UTF8.GetBytes(secret);        var signed = prefix            .Concat(rawBody)            .ToArray();        var mac = HMACSHA256.HashData(key, signed);        var expected = Encoding.UTF8.GetBytes(Convert.ToHexString(mac));        var provided = Encoding.UTF8.GetBytes(signatureHex.ToUpperInvariant());        if (!CryptographicOperations.FixedTimeEquals(expected, provided)) return 401;​        if (await events.SeenAsync(eventId, ct)) return 200;        // Enqueue before recording: a crash in between means a redelivery, never a lost event.        await queue.EnqueueAsync(eventId, rawBody, ct);        await events.RecordAsync(eventId, ct);        return 200;    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Instead of you polling the provider, the provider calls a URL you registered whenever an event occurs. It is push over plain HTTP, common for payments, source control and SaaS integrations.

The classic mistake is doing the work inside the request. Providers time out after a few seconds and retry, so slow handlers get duplicate deliveries. Verify, persist, acknowledge with a 2xx, then process asynchronously. And check the signature every time, since anyone can POST to a public URL.

Delivery is at least once and not necessarily in order, so dedupe on the event id and treat the payload as a hint: re-fetch current state from the provider API when order matters. Inside your system, a webhook usually becomes a message on a queue.

Ask for it

  • HMAC signature
  • shared secret
  • event id
  • retry schedule
  • replay window
  • 2xx acknowledgement

Add a POST /webhooks/payments endpoint: verify the HMAC signature on the raw body, reject timestamps older than 5 minutes, store the event id under a unique index, enqueue the payload to a broker (Kafka, RabbitMQ, SQS, Azure Service Bus), return 200 at once, and process it in a background consumer.