---
title: "Headless Webhook API"
slug: headless-webhook-api
description: "Build custom headless webhook experiences from the API, SDKs, CLI, and MCP server."
created_at: "2026-09-16"
updated_at: "2026-09-16"
image: https://cdn.resend.com/posts/headless-webhook-api.jpg
humans: ["gabriel-miranda"]
---

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](/docs/mcp-server):

- [List Events](/docs/api-reference/webhooks/list-events): every event delivered to a webhook, with its delivery status.
- [Retrieve Event](/docs/api-reference/webhooks/get-event): the exact payload we sent to your endpoint.
- [List Attempts](/docs/api-reference/webhooks/list-event-attempts): list all attempts with the status code and body your endpoint returned.
- [Replay Event](/docs/api-reference/webhooks/replay-event): queue another delivery of a webhook event.
- [Rotate Signing Secret](/docs/api-reference/webhooks/rotate-signing-secret): get a new secret without opening the dashboard.

<YouTube videoId="MzueKFU5Geo" />

## 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.

<video
  src="https://cdn.resend.com/posts/headless-webhooks-api.mp4"
  autoPlay
  loop
  muted
  playsInline
  className="extraWidth"
/>

## Find what failed

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

<CodeTabs codeHeight={250}>

```nodejs
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

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

```php
$resend = Resend::client('re_xxxxxxxxx');

$events = $resend->webhooks->events->list(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0'
);
```

```python
import resend

resend.api_key = 're_xxxxxxxxx'

events = resend.Webhooks.list_events(
    webhook_id='4dd369bc-aa82-4ff3-97de-514ae3000ee0'
)
```

```ruby
require 'resend'

Resend.api_key = 're_xxxxxxxxx'

events = Resend::Webhooks.list_events('4dd369bc-aa82-4ff3-97de-514ae3000ee0')
```

```go
import "github.com/resend/resend-go/v4"

client := resend.NewClient("re_xxxxxxxxx")

events, err := client.Webhooks.ListEvents("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
```

```rust
use resend_rs::{list_opts::ListOptions, Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let _events = resend
    .webhooks
    .list_events(
      "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
      ListOptions::default(),
    )
    .await?;

  Ok(())
}
```

```java
import com.resend.*;
import com.resend.core.exception.ResendException;
import com.resend.services.webhooks.model.ListWebhookEventsResponseSuccess;

public class Main {
    public static void main(String[] args) throws ResendException {
        Resend resend = new Resend("re_xxxxxxxxx");

        ListWebhookEventsResponseSuccess events = resend.webhooks().listEvents(
            "4dd369bc-aa82-4ff3-97de-514ae3000ee0"
        );
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DI

var resp = await resend.WebhookEventListAsync(
    new Guid( "4dd369bc-aa82-4ff3-97de-514ae3000ee0" )
);
```

```curl
curl -X GET 'https://api.resend.com/webhooks/4dd369bc-aa82-4ff3-97de-514ae3000ee0/events' \
     -H 'Authorization: Bearer re_xxxxxxxxx'
```

```cli
resend webhooks events list 4dd369bc-aa82-4ff3-97de-514ae3000ee0
```

</CodeTabs>

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

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

## Inspect the payload

[Retrieve Event](/docs/api-reference/webhooks/get-event) gives you the exact payload we sent and when the next automatic retry is scheduled.

<CodeTabs codeHeight={250}>

```nodejs
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',
});
```

```php
$resend = Resend::client('re_xxxxxxxxx');

$event = $resend->webhooks->events->get(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
  'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3'
);
```

```python
import resend

resend.api_key = 're_xxxxxxxxx'

event = resend.Webhooks.get_event(
    webhook_id='4dd369bc-aa82-4ff3-97de-514ae3000ee0',
    event_id='msg_1srOsB4mXhCqCVwAxYRNnpFZhb3',
)
```

```ruby
require 'resend'

Resend.api_key = 're_xxxxxxxxx'

event = Resend::Webhooks.get_event(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
  'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3'
)
```

```go
import "github.com/resend/resend-go/v4"

client := resend.NewClient("re_xxxxxxxxx")

event, err := client.Webhooks.GetEvent(
  "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
  "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
)
```

```rust
use resend_rs::{Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let _event = resend
    .webhooks
    .get_event(
      "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
      "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
    )
    .await?;

  Ok(())
}
```

```java
import com.resend.*;
import com.resend.core.exception.ResendException;
import com.resend.services.webhooks.model.GetWebhookEventResponseSuccess;

public class Main {
    public static void main(String[] args) throws ResendException {
        Resend resend = new Resend("re_xxxxxxxxx");

        GetWebhookEventResponseSuccess event = resend.webhooks().getEvent(
            "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
            "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3"
        );
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DI

var resp = await resend.WebhookEventRetrieveAsync(
    new Guid( "4dd369bc-aa82-4ff3-97de-514ae3000ee0" ),
    "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3"
);
```

```curl
curl -X GET 'https://api.resend.com/webhooks/4dd369bc-aa82-4ff3-97de-514ae3000ee0/events/msg_1srOsB4mXhCqCVwAxYRNnpFZhb3' \
     -H 'Authorization: Bearer re_xxxxxxxxx'
```

```cli
resend webhooks events get 4dd369bc-aa82-4ff3-97de-514ae3000ee0 msg_1srOsB4mXhCqCVwAxYRNnpFZhb3
```

</CodeTabs>

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

```json
{
  "object": "webhook_event",
  "id": "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
  "type": "email.delivered",
  "created_at": "2026-08-22T15:28:00.000Z",
  "status": "failed",
  "next_attempt_at": null,
  --highlight-start
  "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"
    }
  }
  --highlight-end
}
```

## See what your endpoint returned

[List Attempts](/docs/api-reference/webhooks/list-event-attempts) shows what happened each time we tried, so you can tell a timeout from a 500 without digging through your own logs.

<CodeTabs codeHeight={250}>

```nodejs
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',
});
```

```php
$resend = Resend::client('re_xxxxxxxxx');

$attempts = $resend->webhooks->events->attempts->list(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
  'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3'
);
```

```python
import resend

resend.api_key = 're_xxxxxxxxx'

attempts = resend.Webhooks.list_event_attempts(
    webhook_id='4dd369bc-aa82-4ff3-97de-514ae3000ee0',
    event_id='msg_1srOsB4mXhCqCVwAxYRNnpFZhb3',
)
```

```ruby
require 'resend'

Resend.api_key = 're_xxxxxxxxx'

attempts = Resend::Webhooks.list_event_attempts(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
  'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3'
)
```

```go
import "github.com/resend/resend-go/v4"

client := resend.NewClient("re_xxxxxxxxx")

attempts, err := client.Webhooks.ListEventAttempts(
  "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
  "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
)
```

```rust
use resend_rs::{list_opts::ListOptions, Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let _attempts = resend
    .webhooks
    .list_event_attempts(
      "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
      "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
      ListOptions::default(),
    )
    .await?;

  Ok(())
}
```

```java
import com.resend.*;
import com.resend.core.exception.ResendException;
import com.resend.services.webhooks.model.ListWebhookEventAttemptsResponseSuccess;

public class Main {
    public static void main(String[] args) throws ResendException {
        Resend resend = new Resend("re_xxxxxxxxx");

        ListWebhookEventAttemptsResponseSuccess attempts = resend.webhooks().listEventAttempts(
            "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
            "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3"
        );
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DI

var resp = await resend.WebhookEventAttemptListAsync(
    new Guid( "4dd369bc-aa82-4ff3-97de-514ae3000ee0" ),
    "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3"
);
```

```curl
curl -X GET 'https://api.resend.com/webhooks/4dd369bc-aa82-4ff3-97de-514ae3000ee0/events/msg_1srOsB4mXhCqCVwAxYRNnpFZhb3/attempts' \
     -H 'Authorization: Bearer re_xxxxxxxxx'
```

```cli
resend webhooks events attempts 4dd369bc-aa82-4ff3-97de-514ae3000ee0 msg_1srOsB4mXhCqCVwAxYRNnpFZhb3
```

</CodeTabs>

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

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "atmpt_1srOrx2ZWZBpBUvZwXKQmoEYga2",
      --highlight-start
      "http_status_code": 500,
      "response": "Internal Server Error",
      --highlight-end
      "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.

<CodeTabs codeHeight={250}>

```nodejs
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',
});
```

```php
$resend = Resend::client('re_xxxxxxxxx');

$event = $resend->webhooks->events->replay(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
  'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3'
);
```

```python
import resend

resend.api_key = 're_xxxxxxxxx'

replayed = resend.Webhooks.replay_event(
    webhook_id='4dd369bc-aa82-4ff3-97de-514ae3000ee0',
    event_id='msg_1srOsB4mXhCqCVwAxYRNnpFZhb3',
)
```

```ruby
require 'resend'

Resend.api_key = 're_xxxxxxxxx'

replayed = Resend::Webhooks.replay_event(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0',
  'msg_1srOsB4mXhCqCVwAxYRNnpFZhb3'
)
```

```go
import "github.com/resend/resend-go/v4"

client := resend.NewClient("re_xxxxxxxxx")

replayed, err := client.Webhooks.ReplayEvent(
  "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
  "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
)
```

```rust
use resend_rs::{Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let _replayed = resend
    .webhooks
    .replay_event(
      "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
      "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3",
    )
    .await?;

  Ok(())
}
```

```java
import com.resend.*;
import com.resend.core.exception.ResendException;
import com.resend.services.webhooks.model.ReplayWebhookEventResponseSuccess;

public class Main {
    public static void main(String[] args) throws ResendException {
        Resend resend = new Resend("re_xxxxxxxxx");

        ReplayWebhookEventResponseSuccess replayed = resend.webhooks().replayEvent(
            "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
            "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3"
        );
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DI

await resend.WebhookEventReplayAsync(
    new Guid( "4dd369bc-aa82-4ff3-97de-514ae3000ee0" ),
    "msg_1srOsB4mXhCqCVwAxYRNnpFZhb3"
);
```

```curl
curl -X POST 'https://api.resend.com/webhooks/4dd369bc-aa82-4ff3-97de-514ae3000ee0/events/msg_1srOsB4mXhCqCVwAxYRNnpFZhb3/replay' \
     -H 'Authorization: Bearer re_xxxxxxxxx'
```

```cli
resend webhooks events replay 4dd369bc-aa82-4ff3-97de-514ae3000ee0 msg_1srOsB4mXhCqCVwAxYRNnpFZhb3
```

</CodeTabs>

<Callout type="insight">
  A replay queues one delivery right away and doesn't change the [automatic retry schedule](/docs/webhooks/retries-and-replays) already running for that event. The webhook must be enabled. If we auto-disabled it after repeated failures, re-enable it first, then replay.
</Callout>

## 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.
<CodeTabs codeHeight={250}>

```nodejs
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

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

```php
$resend = Resend::client('re_xxxxxxxxx');

$webhook = $resend->webhooks->rotateSigningSecret(
  '4dd369bc-aa82-4ff3-97de-514ae3000ee0'
);
```

```python
import resend

resend.api_key = 're_xxxxxxxxx'

webhook = resend.Webhooks.rotate_signing_secret(
    webhook_id='4dd369bc-aa82-4ff3-97de-514ae3000ee0'
)
```

```ruby
require 'resend'

Resend.api_key = 're_xxxxxxxxx'

webhook = Resend::Webhooks.rotate_signing_secret('4dd369bc-aa82-4ff3-97de-514ae3000ee0')
```

```go
import "github.com/resend/resend-go/v4"

client := resend.NewClient("re_xxxxxxxxx")

webhook, err := client.Webhooks.RotateSigningSecret("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
```

```rust
use resend_rs::{Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let _webhook = resend
    .webhooks
    .rotate_signing_secret("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
    .await?;

  Ok(())
}
```

```java
import com.resend.*;
import com.resend.core.exception.ResendException;
import com.resend.services.webhooks.model.RotateWebhookSigningSecretResponseSuccess;

public class Main {
    public static void main(String[] args) throws ResendException {
        Resend resend = new Resend("re_xxxxxxxxx");

        RotateWebhookSigningSecretResponseSuccess webhook = resend.webhooks().rotateSigningSecret(
            "4dd369bc-aa82-4ff3-97de-514ae3000ee0"
        );
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DI

var resp = await resend.WebhookRotateSigningSecretAsync(
    new Guid( "4dd369bc-aa82-4ff3-97de-514ae3000ee0" )
);
```

```curl
curl -X POST 'https://api.resend.com/webhooks/4dd369bc-aa82-4ff3-97de-514ae3000ee0/signing-secret/rotate' \
     -H 'Authorization: Bearer re_xxxxxxxxx'
```

```cli
resend webhooks rotate-signing-secret 4dd369bc-aa82-4ff3-97de-514ae3000ee0
```

</CodeTabs>

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

```json
{
  "object": "webhook",
  "id": "4dd369bc-aa82-4ff3-97de-514ae3000ee0",
  --highlight-start
  "signing_secret": "whsec_yyyyyyyyyy"
  --highlight-end
}
```

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](/docs/webhooks/verify-webhooks-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.
