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:
failed events and page your team before customers notice.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"}]}
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"}}}
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"}]}
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.
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.
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.