Replay a Failed Webhook Without Re-Triggering the Provider

Published October 17, 20267 min read

To replay a failed webhook without re-triggering the provider, capture it once at a Hooklistener endpoint, then open the request and click Replay (or have your coding agent call replay_request) to send the exact headers and body to localhost, staging or production. Because the replay is built from the stored copy, Stripe, GitHub or any other sender never fires a new event, so you can repeat it until your handler returns 2xx.

This guide covers the steps in the dashboard, from the CLI and over MCP, plus the two things that trip up most replays: signature timestamps and idempotency checks.

Why Not Just Trigger the Event Again?

Re-triggering means creating another test charge, pushing another commit or waiting for the next real customer. The new event has a different ID and often different data, so you are no longer debugging the request that failed. Provider redelivery (Stripe's resend, GitHub's Redeliver in Recent Deliveries) keeps the payload but only goes to the URL registered with the provider, and you can't edit it.

Replaying from a captured copy gives you the same bytes every time, sent to whichever environment you are fixing.

Replay a Webhook in Six Steps

  1. 01

    Capture the webhook at a Hooklistener endpoint

    Point Stripe, GitHub or any other provider at a Hooklistener Debug Endpoint. Every delivery is stored with its method, path, headers, query parameters and raw body, so a failed one can be sent again later.

  2. 02

    Find the failed request

    Open the endpoint in the dashboard (or the CLI TUI) and search or filter the request history for the delivery your handler rejected.

  3. 03

    Pick where the replay should go

    Choose a destination URL: production, staging, or your laptop through a hooklistener tunnel URL or a running hooklistener listen session.

  4. 04

    Replay the request

    Click Replay in the dashboard, or ask your coding agent to call the replay_request MCP tool. The stored headers and body are sent as they were captured; edit them first if you need to. For Stripe, GitHub or Slack, pass signing_provider and signing_secret_id so the request is re-signed with a fresh timestamp at delivery (stored secrets are on paid plans).

  5. 05

    Check the response and repeat

    Every replay attempt records the status code and response. Fix your handler and replay the same request again until it returns 2xx, then compare the attempts.

  6. 06

    Keep it as a regression case

    Save the request as a replay case so you can rerun it, alone or in a suite, against localhost or staging after the next change.

New to endpoints? See setting up an endpoint. A replay can only be built from a request Hooklistener has stored, so the endpoint has to be in the delivery path before the failure happens.

Replay From the Dashboard

  1. Sign in at app.hooklistener.com and open the endpoint that received the webhook.
  2. Select the failed request in the history list.
  3. Choose Replay and enter the destination URL, for example https://staging.example.com/webhooks/stripe.
  4. Optionally add headers or edit the body. If you change the body of a Stripe, GitHub or Slack webhook, re-sign it so your signature check still passes.
  5. Send it, read the response, and replay again after each fix. Each attempt is recorded so you can compare responses.

Replay to Localhost With the CLI

Your handler runs on your laptop, so it needs a way to receive the replay. Install the CLI and sign in:

npm install -g hooklistener
hooklistener login

Option A: replay to a tunnel URL

Open a tunnel to your local port and use the public URL it prints as the replay destination:

hooklistener tunnel --port 3000
# Forwarding: https://abc123.hook.events → http://localhost:3000

Replay the failed request to https://abc123.hook.events/webhooks/stripe and it lands on localhost:3000/webhooks/stripe. Requests that come in through the tunnel are captured too, so they can be replayed later. See the tunnel command reference for persistent subdomains and other options.

Option B: listen to the endpoint your provider already uses

If Stripe or GitHub already sends to a Hooklistener endpoint, forward that endpoint to your machine and replay from the dashboard:

hooklistener listen --endpoint ep_abc123 --port 3000 --path-prefix /api/webhooks

The CLI sends each request to your local server with the same method, headers and body, and logs your server's response. Details are in the listen command reference.

Replay With Your Coding Agent (MCP)

With the Hooklistener MCP server connected, Claude Code, Codex or Cursor can find the failed request and replay it in one prompt:

> Find the last Stripe request on my endpoint that my handler
  rejected and replay it to https://abc123.hook.events/webhooks/stripe.
  Tell me the response status.

The agent calls replay_request, which can send the request to any URL or your CLI tunnel, with an edited body and optional Stripe, GitHub or Slack re-signing. To keep the request as a test, it can call save_request_case and later run_endpoint_cases to replay the whole suite. For a full session, see debugging Stripe webhooks with Claude Code.

Two Things That Make Replays Fail

Signature timestamps

GitHub signs only the body, so an unedited replay verifies no matter how old it is. Stripe's Stripe-Signature and Slack's signature also cover a timestamp, and the official libraries reject old ones (stripe-node's default tolerance is 300 seconds), so an unedited replay of an older request fails verification. Fix it by re-signing instead of loosening the tolerance: store the provider's signing secret in Hooklistener, then call replay_request with signing_provider and signing_secret_id. The signature header is generated at delivery with a fresh timestamp, so the replay verifies against your real secret and default tolerance, in production as well as locally.

> Replay request req_abc123 to https://api.example.com/webhooks/stripe,
  re-signed with signing_provider "stripe" and my stored whsec_ secret.

Stored signing secrets are on paid plans; capture and replay work on every plan. Keep your tolerance where it is, since it is your protection against replay attacks. More in the Stripe webhook security guide and webhook signing and HMAC verification.

Idempotency checks

A replay carries the original event ID, so a handler that deduplicates correctly will skip it. That proves deduplication works; to exercise the processing path again, clear the ID in your dev database. See webhook idempotency and deduplication.

Stripe and GitHub Notes

Frequently Asked Questions

Does replaying a webhook create a new event at Stripe or GitHub?

No. Hooklistener builds the replay from the copy it captured, so the provider is never contacted. Stripe does not create a new event, GitHub does not record a new delivery, and nothing in your provider dashboard changes.

Will signature verification pass on a replayed webhook?

An unedited replay carries the original signature headers and the same raw body, so HMAC checks such as GitHub's X-Hub-Signature-256 still match. Stripe and Slack signatures also include a timestamp, and their libraries reject old ones (Stripe's default tolerance is 300 seconds). For those, or whenever you edit the body, call replay_request with signing_provider (stripe, github or slack) and signing_secret_id: Hooklistener generates the signature header at delivery with a fresh timestamp, so a request captured days ago still verifies without loosening your tolerance. Stored signing secrets are on paid plans.

Can I replay a webhook to localhost?

Yes. Run hooklistener tunnel --port 3000 and replay to the public tunnel URL it prints, or keep hooklistener listen --endpoint <ID> --port 3000 running and replay from the dashboard so the request reaches your local server through the CLI.

How far back can I replay?

Any request still in your history. The Free plan keeps 1 day, Pro 14 days, Production 30 days and Scale 90 days, and replay works on every plan. Re-signing old Stripe or Slack requests with a fresh timestamp needs a stored signing secret, which is on paid plans.

My handler skipped the replay as a duplicate. Is that a bug?

Usually not. A replay carries the same event ID (Stripe's evt_ ID, GitHub's X-GitHub-Delivery header), so an idempotent handler treats it as already processed. To exercise the processing path again, clear that ID from your dev database or edit it in the replayed body.

How is this different from Stripe's resend or GitHub's Redeliver?

Provider redelivery sends the original event again to the URL registered with the provider, and you cannot change the payload. A Hooklistener replay can target any URL, including localhost and staging, lets you edit and re-sign the body, and records every attempt next to the original request.

Replay is included on every plan, including Free; stored signing secrets for re-signed replays are on paid plans. Create an endpoint or browse the rest of the debugger features.