Your application receives an email webhook, creates a support ticket, and returns HTTP 200. A minute later, another ticket appears for the same message. Or the delivery dashboard shows a successful webhook, but the ticket never reached the database. These are two sides of the same reliability problem: getting an HTTP notification is not the same as processing a business event exactly once.
Webhook automation has renewed interest as products such as Resend Inboxes make incoming mail and agent workflows easier to connect. The technical problem is older and broader than any one product. A provider delivers signed events over HTTP. Networks fail, clients retry, queues duplicate messages, workers crash, and downstream APIs time out. The design must assume duplicate delivery and separate successful receipt from completed business work. That is what makes email-driven automation dependable rather than a fragile demo.
Treat delivery, acceptance and business processing as separate events
Consider an email.received event. The provider sends a POST containing a JSON envelope. Your route checks its authenticity and stores the event. A worker later creates a support ticket and sends an acknowledgment. Four questions need different answers: Did the endpoint receive the POST? Was the sender authentic? Was the event saved durably? Did the ticket creation actually complete?
The 200 OK status code only indicates how your HTTP endpoint responded. It does not indicate that the ticket was created, the downstream job executed, or that the message was understood. A provider may also attempt to deliver the message again if your handler has completed processing the ticket but has not returned an acknowledgment in time. If your automation assumes that each webhook delivery is unique then this may result in duplicate tickets or duplicate email responses being sent.
Have a short ingestion process that validates the request, creates a unique event key and puts it onto the queue. Then acknowledge the provider. The expensive processing can be done separately. In production you need to have clear documentation about how durable your event store and queue are. If you acknowledge before persisting you may lose events if there is a restart.
Understand the payload and event identifiers
Resend's webhook payloads include a top-level event type and a data object for the relevant resource. For inbound email the sample email.received event includes fields such as an email identifier, sender, recipients, message ID and subject. The event schema is owned by the provider and can change. So you need to be aware of what family of events you are dealing with.
{
"type": "email.received",
"created_at": "2026-10-29T09:00:00Z",
"data": {
"email_id": "example-email-001",
"from": "Example <hello@example.invalid>",
"to": ["support@example.invalid"],
"subject": "Cannot sign in"
}
}
This is a reduced illustrative event shape, not a complete provider payload or a sample that should be signed and submitted to a production endpoint. The provider may have further fields. Note that the email.received field contains meta data only. You can get body content and attachments via the Receiving API. Resend Inboxes also provides distinct inbox.* event types; subscribe to and handle those explicitly rather than treating every inbox event as email.received. The event ID used for delivery deduplication may not be the same as the resource ID of the email. For Resend's signing protocol, the svix-id header provides a message identity that can be used as part of an idempotency key after signature verification. The data.email_id corresponds to the actual email.
Choose the key according to the operation. To prevent processing the same signed event delivery more than once, store a unique key derived from the provider, endpoint and event message ID. To prevent two distinct legitimate events for the same email from creating the same ticket, use a separate business uniqueness rule such as provider plus email ID plus operation. They solve related but different duplicate problems.
Verify the signature using the exact raw body
A webhook is an inbound request from the public internet. A client can POST plausible JSON without ever having received a real provider event. Validate the provider's signature before trusting payload fields or recording privileged actions. Resend documents three signature headers: svix-id, svix-timestamp, and svix-signature. Its SDK verification helper takes the original raw payload, those headers, and the webhook signing secret.
A minimal Next.js route illustrates the boundary:
import { NextRequest, NextResponse } from "next/server";
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(req: NextRequest) {
const payload = await req.text();
const secret = process.env.RESEND_WEBHOOK_SECRET;
if (!secret) return new NextResponse("Not configured", { status: 500 });
let event;
try {
event = resend.webhooks.verify({
payload,
headers: {
id: req.headers.get("svix-id") ?? "",
timestamp: req.headers.get("svix-timestamp") ?? "",
signature: req.headers.get("svix-signature") ?? "",
},
webhookSecret: secret,
});
} catch {
return new NextResponse("Invalid signature", { status: 400 });
}
// Persist an idempotency key and enqueue work DURABLY before returning 2xx.
// Persistence/queue logic is application-specific and omitted here.
// Fail closed until your own durable handler is implemented.
void event;
return new NextResponse("Durable ingestion not configured", { status: 503 });
}
In this example, we have deliberately returned an HTTP 503 response after validation as we have not included the database and queue adapter. This is not a complete webhook processor, and so setting the status to 200 would cause events to be lost if not ingested durably. The benefit of this is to show why req.text() should be called before parsing. If you call req.json() then stringify it, there could be issues with ordering of keys and whitespace meaning the signature doesn’t match. Don’t create your own signature algorithm as the provider will have a verifier. Also check timestamp freshness and secret rotation according to the provider's verification contract.
RESEND_API_KEY authorizes calls your application makes to Resend; the webhook signing secret authenticates callbacks Resend sends to you. They are different credentials. These should not be present in any browser code, logs or shared screenshots. If you are failing validation, ensure that the secret is in the correct endpoint and environment, rather than turning off the validation.
Make duplicate processing impossible at the persistence boundary
Webhook event verification, durable storage, worker execution, and reconciliation for duplicate deliveries and retries.

Deduplication at receipt and idempotency at the downstream operation solve different failure modes.
A Set scoped to the process may be used to ensure no duplication during development but will fail to do so on serverless instances, restarts and horizontally scaled workers. Use a database uniqueness constraint or equivalent durable transactional store. Validate the signed message. Derive the idempotency key. Insert the event record with a unique constraint. Atomically or via a recoverable outbox add to the queue of work to do. Confirm accepted deliveries.
For example, a PostgreSQL table can enforce one record per provider message ID:
CREATE TABLE webhook_events (
provider TEXT NOT NULL,
event_id TEXT NOT NULL,
event_type TEXT NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status TEXT NOT NULL DEFAULT 'pending',
PRIMARY KEY (provider, event_id)
);
INSERT INTO webhook_events (provider, event_id, event_type)
VALUES ('resend', $1, 'email.received')
ON CONFLICT (provider, event_id) DO NOTHING;
This prevents duplicate event rows, but it doesn't automatically ensure exactly one external side effect. A worker could raise a ticket and cause a crash whilst marking the event as complete. On retry, it might create another ticket. Prefer a downstream API with its own idempotency key, or store a business operation key such as ticket-for:resend:<email_id> in a table with a uniqueness constraint and a reconcilable state machine. Call the downstream service with an idempotency key when it supports one. If not, design your delivery mechanism to allow for lookups and reconciliation.
It's important to note that delivery needs to be at least once. It is your responsibility to ensure that this is handled safely. If delivery fails it should still be possible to see that it has failed.
Return the right HTTP response at the right time
An invalid signature should fail immediately. A valid signed event with a permanently unsupported event type can be safely logged and ignored. A temporarily unavailable database or queue is a reason not to acknowledge successfully before persistence. Classification using a slow machine learning / CRM system should be done after the request has been queued. Not while we are waiting for the HTTP response.
| Endpoint result | Typical meaning | Operator action |
|---|---|---|
| 2xx after durable acceptance | Provider may stop retrying | Process asynchronously and monitor completion |
| 400 invalid signature | Request cannot be trusted | Check secret, raw body, signing headers |
| 4xx for valid but unsupported event | Depends on provider retry policy | Avoid accidental retry storm; document behavior |
| 5xx before persistence | Temporary endpoint failure | Allow provider retry; repair dependency |
| Timeout | Provider saw no timely acknowledgment | Expect duplicate delivery and inspect logs |
Always refer to the documentation for retry policies from your provider. Resend will show any retries and manual replays, so there may be a legitimate reason why an event has been retried. Some systems will stop retrying specific client errors whereas others will retry widely. Do not assume that any single provider will implement a general retry policy. If you are deliberately returning a 5xx error ensure that the handler is safe to be run again.
Debugging issues with your signature
If you are having issues with your signature, and requests are succeeding in production but failing locally (or vice versa), compare the webhook signing secret for the environment, the size of the raw payload, the timestamp header and which endpoint is receiving the request. A reverse proxy can modify the body and/or headers.
A JSON middleware may consume the request stream before the verifier runs The replay of the captured payload without the original signed headers is not an exact replay..
Do not log signing secrets or full personal email bodies. To aid with debugging, you should log the Request ID, the number of bytes, the verification status, the event type after verification and the durable event key. If your production webhook has been exposed, you should rotate your secret and audit any events that have been accepted during this period.
Use the delivery dashboard and replay functionality provided by the provider as much as possible. You can also use our observability view within Resend to compare deliveries and responses. If you can see delivery has reached your endpoint, and the delivery has been verified but there is no ticket, then there is an issue downstream. You'll need to check the queue, worker and CRM integration. If you only replay the webhook, you won't be able to see if the worker has crashed unless you have logs for these downstream processes.
Handling out of order events
It is possible that the notifications for a particular resource will not be received in the order you expect in your database. For example, a message could trigger email.received, a thread update and a status change close together. If the second event is delivered before the first one, a worker that picks up an existing ticket may fail or create a duplicate. You should not rely on the delivery order of webhooks unless you are certain that your provider will deliver them in this way.
Use an upsert or a per-resource processing strategy for order-sensitive transitions. Re-fetch current resource state from the provider when the event only signals that something changed. Include a time stamp for the event. Do not assume the time stamp is authentic for unauthenticated messages. Decide whether to ignore old events, merge them or keep them for audit purposes. The answer may vary depending on the type of event.
A webhook payload may contain personally identifiable information, email addresses and message subjects. Only store the data required for the workflow. Apply data retention policies. Limit access to complete message content. Allow controls to be applied to email attachments and malicious HTML. Verification only confirms the notification originated from, it does not confirm the message content or attachment is safe.
Keep automation effects visible and recoverable
A queue is useful only if failures can be surfaced and repaired. Record event states such as pending, processing, completed and failed. Track attempt, last error category and next retry. Implement dead lettering for poison messages or recurring downstream errors. Provide an operator to safely re-execute the business process without reposting raw, unverified payloads to a public endpoint.
We have three deliberately difficult scenarios to test: sending the same verified event, failing the worker after the downstream ticket has been created and simulating the inability to access the queue. The first must not create duplicate processing records. The second needs reconciliation or downstream idempotency. The third must not return a success response if the event could be lost. Test valid but unknown event types and malformed JSON.
The Yoryantra JSON Validator can be used to inspect the shape of your sanitized event JSON. The HTTP Headers Parser can be used to inspect pasted signature headers. The API Error Response Formatter can be used to understand errors coming back from downstream APIs. None of these tools will validate your Resend signature or simulate the delivery infrastructure. Do not include any real signing secrets or customer information in the examples of problems you describe in your public content.
Replay an event without repeating the customer action
A support operator may need to reprocess a failed email.received event days after it arrived. A safe replay should retrieve the verified event from storage, check the current state and ask another worker to retry. No new signed headers should be created. Replay should be controlled, log who initiated it and give some insight into what may happen. If the ticket has already been created, the reconciliation process should find it and set the event to complete.
Distinguish a delivery replay from a business replay. The provider resending the HTTP notification is useful when the endpoint never accepted it. Re-running a failed business action is useful after acceptance succeeded but the worker failed. Both paths should converge on the same persistence and uniqueness rules. This can cause confusion and the issue may be worsened by replaying the incorrect step. Providing a timeline of events including delivery ID, acceptance time, number of attempts by the worker and external ticket ID will make it much easier to understand what has gone wrong, rather than just a red failed webhook tag.
A checklist for reliable webhooks
Ensure that the signing secret and raw body are handled appropriately in your environment. Ensure that the event key is stored securely. Only acknowledge the event. Ensure safe retries from the worker. Test a duplicate, a timeout, a malformed signature, and a downstream failure. Document what happens when two events refer to one email. Add monitoring for accepted-but-unprocessed events as well as rejected deliveries.
Successful webhook automation is not measured by how quickly the endpoint responds on the happy path. It is measured by whether a customer email produces the intended result once, even when the same event arrives again, the worker crashes, or the downstream service is unavailable.
