GLOSSARY

What is a Webhook Replay?

A webhook replay is a re-delivery of an event the sender already generated: the same frozen payload is POSTed to the receiver again, either automatically on the retry ladder or on demand once the receiver has been repaired.

Free forever plan · No credit card required · Cancel anytime

Quick definition

A webhook replay is a re-delivery of an event the sender already generated: the same frozen payload is POSTed to the receiver again, either automatically on the retry ladder or on demand once the receiver has been repaired.

In a single sentence: the button you press after the receiver comes back up.

What it means

A webhook replay is the sender pushing an event to your endpoint again. The event is not regenerated: the same payload that was created the first time is sent again, byte for byte, with a fresh signature and a fresh attempt.

Replay exists because webhook delivery is at-least-once, not exactly-once, and because retry ladders are finite. A sender that gives up after 8 attempts is behaving correctly, but the event it abandoned is still an event you needed. Replay is the escape hatch: the receiver gets fixed, somebody presses the button, and the missing events arrive.

This page assumes you already know what a webhook is. It is about what happens after the first delivery does not stick.

The word means two opposite things

"Replay" in security writing means an attack: an adversary captures a valid signed request and sends it again, hoping it gets processed a second time. "Replay" in integration tooling means a feature: the sender re-delivers an event you missed. Both are the same HTTP request arriving twice. The difference is who sent it and whether you wanted it.

What resolves the tension is that the two are distinguishable by age, and only by age. A legitimate replay is signed at the moment it is sent, so its timestamp is current. A captured request has the timestamp it was born with. That is why a receiver rejects anything whose signed timestamp is more than 300 seconds from its own clock, and why the timestamp has to be inside the signed value rather than beside it. See HMAC signature for the construction.

Retry and replay are different operations

A retry is what the sender does on its own, automatically, against a published ladder. In the Pinlyx Data API, a delivery is attempted up to 8 times across 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, and is then marked failed. It is never retried forever, because a queue that retries forever is a queue that eventually consists entirely of dead deliveries.

A replay is an operator action taken afterwards. It requeues an abandoned delivery for immediate sending with a fresh attempt budget, and the original attempt history is preserved rather than overwritten, so the incident stays auditable. Only failed and dropped deliveries can be replayed; anything else is either still in flight or already delivered.

The delivery state machine

Four states, and the distinction between two of them is the one that saves an afternoon of debugging. failed means the sender tried eight times and your receiver never returned a 2xx. dropped means the sender never tried at all, because the destination had already been disabled when the event's turn came. Those have different causes and different fixes: a failed delivery points at your handler, a dropped one points at your endpoint's health.

Endpoints are disabled automatically. Consecutive failures reset to zero on any 2xx, and an endpoint that reaches 20 consecutive failures is switched off so a dead receiver stops consuming delivery attempts for everyone else. Bringing it back means running the verification probe again, which sends one signed request and activates the endpoint on a 2xx. Only then can the dropped deliveries be replayed.

Reading an attempt log

The single most useful thing a delivery API can give you is the per-attempt record: status code, error text and duration for each try. The example further down is a real shape, and every field in it is diagnostic.

  • status_code: 502 with a 214 ms duration means your gateway answered quickly and badly. Look at the proxy, not the handler.
  • status_code: null with a 10,004 ms duration means nothing answered before the 10-second send timeout. Null is not a status code of zero: it means a timeout, a DNS failure or a TLS error, and no HTTP response was ever received.
  • A 200 that took 9,800 ms is a warning even though it succeeded. You are one slow query away from a timeout, and the sender will retry a delivery your handler actually processed.

That last case is exactly why receivers must be idempotent. Acknowledge as soon as the event is stored, then do the work asynchronously; a handler that finishes the work before answering is a handler that will be asked to do it twice.

Ordering: the part that bites in production

Replayed events arrive out of order relative to live ones. If you replay Monday's event on Wednesday, your receiver sees Wednesday's event first and Monday's second. A handler that writes last-received-wins will overwrite current state with stale state, and nothing in the delivery pipeline will flag it.

The fix is to order by the event's own timestamp, not by arrival. Every event payload carries a created_at and, for the daily audience events, a day field. Compare against what you already stored for that entity and ignore anything older. This is a three-line guard that turns replay from a hazard into a routine operation.

What replay cannot do

Replay re-sends events that exist. It is not a backfill. If a watch was created on the 3rd, there are no events for the 1st, and no button will conjure them. Detection also runs against an end-of-day rollup, so events are daily rather than intraday, and a day whose change could not be measured produces no event on any threshold. A missing measurement is not a change of zero, and reporting it as one would be inventing a fact.

Deleting a destination is also final: it cascades to every watch pointed at it and every event still queued for it. There is nothing left to replay afterwards.

Related concepts

  • Webhook: the delivery mechanism itself, its security model and its payload anatomy.
  • HMAC signature: why a replayed payload still verifies, and why the timestamp is inside the signature.
  • Idempotency key: the receiver-side de-duplication that makes replay safe.
  • Exponential backoff: the retry ladder that runs before anyone reaches for replay.
  • AI agent: a common webhook consumer, and one that must not act twice on one event.

How Pinlyx handles it

The Data API's watch surface exposes the whole delivery lifecycle rather than hiding it. You can list every event queued for an account with its state, attempt count and last error; fetch one delivery to see the exact signed payload and the full attempt log; and requeue an abandoned delivery with a single POST once the receiver is fixed. Endpoint health is visible too: consecutive failures, last success, last failure and the reason a destination was disabled. Nothing about a missing webhook has to be guessed.

Cheat sheet · delivery states

Two of these four can be replayed.

StateWhat it meansReplayable
pendingQueued or mid-ladder. Attempts may still be scheduled.No. It has not finished trying.
deliveredA 2xx was received. attempts tells you how many tries it took.No. Re-sending a delivered event is a duplicate, not a repair.
failedAll 8 attempts were spent across the ladder without a 2xx.Yes. This is the case replay exists for.
droppedNever sent at all, because the destination was disabled before its turn came.Yes, once the destination is verified and active again.
Cheat sheet · diagnose, then replay

Two calls: read the attempt log, then requeue.

Never replay before reading the log. The log is what tells you whether the receiver is fixed or is about to fail eight more times.

1. Why did it fail?
GET /data-api/v2/watch/deliveries/c0ffee00-1111-2222-3333-444455556666
Authorization: Bearer psk_live_...

HTTP/1.1 200 OK

{
  "data": {
    "id": "c0ffee00-1111-2222-3333-444455556666",
    "status": "failed",
    "attempts": 8,
    "max_attempts": 8,
    "last_status_code": 502,
    "last_error": "Endpoint answered 502.",
    "payload": {
      "id": "c0ffee00-1111-2222-3333-444455556666",
      "type": "account.audience_changed",
      "created_at": "2026-08-31T02:15:00.000Z",
      "watch_id": "3c9e6f21-88a4-4b02-9f7d-11ab4c2e5d90",
      "data": {
        "platform": "x",
        "entity_id": "44196397",
        "handle": "elonmusk",
        "day": "2026-08-30",
        "audience": 241493123,
        "change": 118402,
        "change_pct": 0.049
      }
    },
    "attempts_log": [
      { "attempt_no": 1, "status_code": 502,
        "error": "Endpoint answered 502.",
        "duration_ms": 214,   "created_at": "2026-08-31T02:15:01.000Z" },
      { "attempt_no": 2, "status_code": null,
        "error": "Endpoint did not respond within 10s.",
        "duration_ms": 10004, "created_at": "2026-08-31T02:16:03.000Z" }
    ]
  },
  "meta": { "request_id": "req_9f2c41a8b3d5", "took_ms": 42 }
}
2. Requeue it, once the receiver is actually fixed
POST /data-api/v2/watch/deliveries/c0ffee00-1111-2222-3333-444455556666/retry
Authorization: Bearer psk_live_...

HTTP/1.1 200 OK

{
  "data": {
    "id": "c0ffee00-1111-2222-3333-444455556666",
    "status": "pending",
    "attempts": 0,
    "max_attempts": 8
  },
  "meta": { "request_id": "req_c71b09de4a22", "took_ms": 18 }
}

Note attempts back to 0: a replay grants a fresh budget. The historical attempts_log is kept, so the incident remains auditable after the replay succeeds.

Cheat sheet · a replay-safe receiver

De-duplicate on the event id, order on the event time.

async function handle(event) {
  // 1. Duplicate? A replay is indistinguishable from a retry of a
  //    delivery you already processed slowly. Keep ids for 7 days:
  //    the automatic ladder alone spans 44 h 36 m.
  if (await seen.has(event.id)) return;

  // 2. Stale? A replayed event can arrive AFTER a newer live one.
  //    Order by the event's own clock, never by arrival order.
  const current = await store.get(event.data.entity_id);
  if (current && current.day >= event.data.day) {
    await seen.add(event.id, { ttlDays: 7 });   // still mark it handled
    return;
  }

  // 3. Apply, then record. If the process dies between these two lines
  //    the next delivery re-applies an idempotent write, which is fine.
  await store.put(event.data.entity_id, {
    day: event.data.day,
    audience: event.data.audience,
  });
  await seen.add(event.id, { ttlDays: 7 });
}
Watch out for

Six ways a replay makes things worse.

  • Replaying before the receiver is actually fixed, which spends a fresh 8-attempt budget on the same 502.
  • A de-duplication window shorter than the sender ladder, so the last automatic attempt is processed a second time.
  • Applying events in arrival order, so a replayed Monday event overwrites Wednesday state.
  • Replaying a delivered event because a downstream system lost it. That is a duplicate, not a repair; re-read the resource instead.
  • Forgetting that a disabled endpoint must be verified again before any replay can be delivered to it.
  • Treating status_code null as zero. Null means no HTTP response arrived at all: a timeout, DNS or TLS failure.

Webhook replay: FAQ

What to check before you press the button, and what to build so pressing it is safe.

A retry is automatic and bounded: the sender walks a published ladder, spends its attempt budget and stops. A replay is manual and deliberate: an operator requeues an abandoned delivery after fixing the receiver, and the sender starts with a fresh attempt budget while preserving the original attempt history. Retries handle blips. Replays handle the outage that outlasted the ladder.
The original bytes, frozen at the moment the event was created. That matters twice over. A signature computed against those bytes still verifies, so a receiver that recorded the signature can check it again. And the numbers stay historically correct: an "audience changed on 30 August" event replayed on 2 September must still carry the 30 August figures, because refreshing them would quietly corrupt whatever series the receiver is building.
They are opposites that share a name. A replay attack is an adversary re-sending a request they captured, hoping it is processed again. A replay feature is the sender re-sending an event you asked for. The same design serves both: the timestamp inside the signed payload plus a freshness window rejects the attack, while a legitimate replay is signed afresh at send time and therefore passes the window. If your receiver validates only the payload and not its age, you have blocked neither.
Longer than the sender can possibly re-deliver. The Pinlyx Data API delivery ladder runs 1 m, 5 m, 30 m, 2 h, 6 h, 12 h and 24 h, a total span of 44 hours 36 minutes, and a manual replay can arrive later still. Seven days of event ids is the safe answer; 24 hours is not, because the final automatic attempt lands outside it.
Most likely it was disabled automatically. Consecutive failures reset to zero on any 2xx, and an endpoint that reaches 20 consecutive failures is disabled so a dead receiver stops consuming delivery attempts. Events generated while it was disabled are recorded as dropped rather than failed. The fix is to repair the receiver, run the verification probe again to bring the endpoint back to active, and then replay the dropped deliveries.
No. Replay re-sends events that exist in the delivery log; it is not a backfill. If no watch existed for an account on a given day, no event was ever created for it and there is nothing to replay. Detection also runs against an end-of-day rollup, so events are daily rather than intraday, and a day whose change could not be measured never produces an event at all. A missing measurement is not a change of zero.
Ready to ship

Nothing is lost. Everything is replayable.

Every delivery, every attempt, every status code and every error, visible through the API and requeueable with one call once the receiver is back.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.