All updates

Headless Webhook API

Build custom headless webhook experiences from the API, SDKs, CLI, and MCP server.

Today, we're introducing the Headless Webhook API. Everything the webhook detail page does is now available in the API, every SDK, the CLI, and the MCP server:

What you can build with it

  • Automated recovery: list failed events on a webhook and replay them in a loop.
  • Delivery alerts: poll for failed events and page your team before customers notice.
  • Secret rotation on a schedule: rotate signing secrets from your secrets manager or CI.
  • Agent-driven debugging: connect the MCP server and ask your agent what failed.

Find what failed

List the events delivered to a webhook. Each one carries the delivery status for that endpoint: success, failed, attempting, or pending.

import { Resend } from 'resend';
const resend = new Resend('re_xxxxxxxxx');
const { data, error } = await resend.webhooks.events.list({
webhookId: '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
});

Events come back most recent first and paginate forward with after. Only events inside your plan's data retention window are returned.

{
"object": "list",
"has_more": false,
"data": [
{
"id": "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
"type": "email.delivered",
"created_at": "2026-08-22T15:28:00.000Z",
"status": "failed"
},
{
"id": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2",
"type": "email.sent",
"created_at": "2026-08-22T15:27:42.000Z",
"status": "success"
}
]
}

Inspect the payload

Retrieve Event gives you the exact payload we sent and when the next automatic retry is scheduled.

import { Resend } from 'resend';
const resend = new Resend('re_xxxxxxxxx');
const { data, error } = await resend.webhooks.events.get({
webhookId: '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
eventId: 'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3',
});

The payload is exactly what your endpoint received, so you can replay it locally against your handler.

{
"object": "webhook_event",
"id": "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
"type": "email.delivered",
"created_at": "2026-08-22T15:28:00.000Z",
"status": "failed",
"next_attempt_at": null,
"payload": {
"type": "email.delivered",
"created_at": "2026-08-22T15:28:00.000Z",
"data": {
"email_id": "571f1f42-1c2d-4b1f-8f8e-8b3b5b3b5b3b",
"from": "onboarding@resend.dev",
"to": ["delivered@resend.dev"],
"subject": "Welcome",
"created_at": "2026-08-22T15:27:59.000Z"
}
}
}

See what your endpoint returned

List Attempts shows what happened each time we tried, so you can tell a timeout from a 500 without digging through your own logs.

import { Resend } from 'resend';
const resend = new Resend('re_xxxxxxxxx');
const { data, error } = await resend.webhooks.events.attempts.list({
webhookId: '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
eventId: 'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3',
});

Each attempt carries the status code and response body your endpoint returned.

{
"object": "list",
"has_more": false,
"data": [
{
"id": "atmpt_1srOrx2ZWZBpBUvZwXKQmoEYga2",
"http_status_code": 500,
"response": "Internal Server Error",
"sent_at": "2026-08-22T15:33:12.000Z"
}
]
}

Replay it

Once your endpoint is healthy again, replay the event. This is the same action as the Replay button in the dashboard.

import { Resend } from 'resend';
const resend = new Resend('re_xxxxxxxxx');
const { data, error } = await resend.webhooks.events.replay({
webhookId: '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
eventId: 'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3',
});

A replay queues one delivery right away and doesn't change the automatic retry schedule already running for that event. The webhook must be enabled. If we auto-disabled it after repeated failures, re-enable it first, then replay.

Rotate the signing secret

If a signing secret leaks, or you rotate secrets as routine hygiene, you no longer need the dashboard to do it.

import { Resend } from 'resend';
const resend = new Resend('re_xxxxxxxxx');
const { data, error } = await resend.webhooks.rotateSigningSecret(
'4dd369bc-aa82-4ff3-97de-514ae3000ee0',
);

The response includes the new secret, so there's no second request.

{
"object": "webhook",
"id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
"signing_secret": "whsec_yyyyyyyyyy"
}

For 24 hours after rotating, payloads are signed with both the previous and the new secret, so you can roll out the new one to your handler without dropping events. Learn more about verifying webhook requests.

Conclusion

These endpoints are part of a larger move to bring the full functionality of the dashboard into the API, CLI, and MCP server.

If you have any questions, please reach out to us and we'll be happy to help.