> ## Documentation Index
> Fetch the complete documentation index at: https://supahooks.ar27111994.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Replay captured webhook events to any destination

> Resend any previously captured webhook event to a new target URL, with the original headers and body, so you can test a fix against real traffic.

Replay lets you take any event that was previously captured and resend it to a destination of your choice. This is useful when you fix a bug in your webhook handler and want to verify the fix against real production traffic, or when you need to route a captured payload to a staging environment for QA without waiting for the original provider to resend it.

Replay is on-demand and targets a single specific event. If you want every incoming request automatically forwarded to another server, see [forwarding](/features/forwarding) instead.

## How to replay

Send a `POST` request to `/replay/:webhookId/:itemId` with the destination URL as a query parameter.

```bash theme={null}
curl -X POST \
  "https://<run-id>.runs.apify.net/replay/wh_abc123/evt_8m2L5p9xR?url=https%3A%2F%2Ftarget.example%2Fwebhook"
```

Replace `wh_abc123` with the webhook ID and `evt_8m2L5p9xR` with the log entry ID you want to replay. The `url` parameter is required and must be URL-encoded.

### What gets sent

The replay request is constructed from the original captured event:

* The **original body** is sent as-is.
* The **original headers** are forwarded, minus masked headers (such as `Authorization` and `Cookie` when `maskSensitiveData` is on) and transport headers managed by the HTTP layer (such as `host` and `content-length`).
* Three additional headers are added to every replay request:

| Header                  | Value                                |
| ----------------------- | ------------------------------------ |
| `X-Apify-Replay`        | `true`                               |
| `X-Original-Webhook-Id` | The source webhook ID                |
| `Idempotency-Key`       | A unique key for this replay attempt |

### Response

```json theme={null}
{
  "status": "replayed",
  "targetUrl": "https://target.example/webhook",
  "targetResponseCode": 200,
  "targetResponseBody": "OK"
}
```

When headers were stripped before forwarding, the response also includes:

```json theme={null}
{
  "status": "replayed",
  "targetUrl": "https://target.example/webhook",
  "targetResponseCode": 200,
  "targetResponseBody": "OK",
  "strippedHeaders": ["host", "content-length"]
}
```

## Retry behavior

If the destination does not respond successfully, the runtime retries automatically.

<ParamField query="replayMaxRetries" type="integer" default="3">
  Maximum retry attempts for a failed replay request. Accepted range: 0–10.
</ParamField>

<ParamField query="replayTimeoutMs" type="integer" default="10000">
  Timeout for each individual replay attempt in milliseconds. Accepted range: 1,000–60,000. Default is 10 seconds.
</ParamField>

When all retry attempts time out, the endpoint returns `504 Gateway Timeout`:

```json theme={null}
{
  "error": "Replay Failed",
  "message": "Target destination timed out after 3 attempts (10s timeout per attempt)",
  "code": "ECONNABORTED"
}
```

## SSRF protection

Replay targets are validated before any request is sent. The following destinations are blocked:

* Private networks (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`)
* Loopback (`127.0.0.0/8`, `::1`)
* Link-local (`169.254.0.0/16`, `fe80::/10`)
* Cloud metadata endpoints (`169.254.169.254`, `100.100.100.200`)
* Hostnames that cannot be safely resolved

If the target URL fails SSRF validation, you receive a `400` response:

```json theme={null}
{
  "error": "URL resolves to internal/reserved IP range"
}
```

## Common error responses

| Scenario                     | Status | Response                                                  |
| ---------------------------- | ------ | --------------------------------------------------------- |
| Missing `url` parameter      | `400`  | `{"error": "Missing 'url' parameter"}`                    |
| URL resolves to private IP   | `400`  | `{"error": "URL resolves to internal/reserved IP range"}` |
| Hostname cannot be resolved  | `400`  | `{"error": "Unable to resolve hostname"}`                 |
| All retry attempts timed out | `504`  | See timeout response above                                |
