// Communication & Integration · 04
Webhooks
Don't call us, we'll call you. An HTTP POST when something happens.
How it fits together
- 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 readExpandCollapse
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.