If you've integrated webhooks and noticed the same event arriving more than once, you haven't found a bug. You've encountered something inherent to webhooks in general. This guide explains why duplicate webhook deliveries happen and how to handle them correctly with idempotency.
Webhooks don't guarantee exactly-once delivery
Here's the core thing to understand: there is no exactly-once delivery guarantee for webhooks. Not at AhaSend, not at Stripe, not anywhere. This isn't a limitation anyone failed to solve. It's an inherent property of delivering messages over an unreliable network to an endpoint the sender doesn't control.
The reason comes down to a simple problem. When AhaSend sends your endpoint a webhook and your server processes it, we need confirmation that you received it. If that confirmation never reaches us, we have no way of knowing whether you processed the event or not. The safe choice is to retry, because the alternative, dropping the event, means you might miss something important like a delivery failure or a spam complaint.
So we retry. And occasionally that means your endpoint receives an event it has already seen.
How a duplicate actually happens
There are two common scenarios that lead to the same webhook being delivered more than once.
The first is an intermittent network issue. Your system receives the webhook and processes it correctly, but the acknowledgement back to AhaSend gets lost somewhere on the network. From our side, the delivery looks like it failed, so we retry. From your side, you've now received the same event twice.
The second is a timeout. AhaSend waits up to 10 seconds for your endpoint to respond. If your server takes longer than that, we close the connection and queue the event for a later retry, even though your application may have already finished processing it. When the retry arrives, that's a duplicate.
In both cases nothing has gone wrong with the system. The webhook delivery is behaving exactly as designed: prioritising that you receive every event over the convenience of never seeing one twice.
The solution: idempotency with the webhook-id header
The correct way to handle this is to make your webhook processing idempotent, which means processing the same event twice has the same effect as processing it once. The tool for this is the webhook-id header.
Every webhook AhaSend sends includes three security headers, one of which is webhook-id, a unique identifier for that specific event. Crucially, when AhaSend retries a delivery, the retry carries the same webhook-id as the original. That gives you a reliable key to recognise events you've already handled.
The pattern looks like this:
webhook_id = request.headers["webhook-id"]
# Skip events you've already handled
if already_processed(webhook_id):
return ok()
process_event(event)
# Record only after successful processing
mark_processed(webhook_id)The key idea: before doing any work, check whether you've already seen this webhook-id. If you have, acknowledge the request with a 200 and do nothing else. If you haven't, process the event and then record the ID so future retries are recognised as duplicates.
A few practical notes on implementing this well.
Store processed IDs in something durable, such as your database or Redis, not an in-memory set that disappears when your service restarts. A database unique constraint on the webhook ID is a clean way to enforce this, since a duplicate insert will simply fail and you can treat that as a signal you've seen the event before.
Record the ID only after you've successfully processed the event. If you record it first and then your processing fails, the retry will be treated as a duplicate and skipped, and you'll have lost the event.
Respond quickly. Since the timeout is 10 seconds, do the minimum synchronously, acknowledge the webhook, and offload heavier work to a background queue. This reduces timeout-driven retries in the first place.
This is the industry standard
This approach isn't specific to AhaSend. It's how robust webhook integrations are built everywhere. Stripe, for example, documents exactly the same pattern: their events can occasionally arrive more than once, and they instruct developers to handle duplicates using the event ID. Building your integration this way means it's resilient not just with AhaSend but with any well-designed webhook provider.
AhaSend follows the Standard Webhooks specification, so the headers and verification approach described here are consistent with a broad ecosystem of tools and libraries.
In short
Duplicate webhook deliveries are normal, expected, and not a sign of anything broken. They happen because webhook delivery prioritises never losing an event over never repeating one. Handle them by making your processing idempotent: check the webhook-id header before processing, skip events you've already seen, and record IDs durably only after successful processing.
This post covers the concept. For the full implementation details, including the exact header structure, signature verification, retry behaviour, and event types, head over to the webhooks API documentation. That's the place to go when you're ready to build a production-grade integration.