[{"data":1,"prerenderedAt":743},["ShallowReactive",2],{"content-query-hmsXgI1Y2y":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"crumb":11,"term":11,"summary":12,"group":13,"order":14,"updated":15,"cta":16,"related":24,"faq":43,"body":56,"_type":736,"_id":737,"_source":738,"_file":739,"_stem":740,"_extension":741,"sitemap":742},"/glossary/webhook-replay","glossary",false,"","What is webhook replay? Redelivery vs replay attacks","Webhook replay means resending a past webhook, either deliberately to recover missed events or maliciously as a replay attack. How each works and how to handle both safely.","reference","Webhook replay","Resending a past webhook: recovery tool or replay attack.","Delivery and reliability",25,"2026-10-28",{"eyebrow":17,"title":18,"text":19,"to":20,"label":21,"secondaryTo":22,"secondaryLabel":23},"Webhook Relay","Replay missed webhooks without asking the provider","Webhook Relay stores every webhook it receives. Resend any request from the logs, recover failed deliveries in bulk after an outage, or replay webhooks that arrived while your local agent was offline as soon as it reconnects.","/features/webhook-logs/","See webhook logs","/docs/webhooks/replay-on-agent-connect/","Replay on agent connect",[25,28,31,34,37,40],{"title":26,"to":27},"Webhook signature","/glossary/webhook-signature/",{"title":29,"to":30},"Idempotency","/glossary/idempotency/",{"title":32,"to":33},"Dead-letter queue","/glossary/dead-letter-queue/",{"title":35,"to":36},"Webhook retries","/glossary/webhook-retries/",{"title":38,"to":39},"Standard Webhooks","/glossary/standard-webhooks/",{"title":41,"to":42},"Durable retries","/features/durable-retries/",[44,47,50,53],{"q":45,"a":46},"What is webhook replay?","Webhook replay means sending a webhook that was already sent before, again. Deliberate replay, also called redelivery, is a recovery tool for re-sending events your endpoint missed or failed to process. A replay attack is the malicious version, in which someone captures a valid signed webhook and sends it again to trigger the same action.",{"q":48,"a":49},"How do I replay a webhook?","Use the provider's redelivery option if it has one, such as the Redeliver button on a GitHub webhook delivery or resending an event from the Stripe dashboard. If webhooks pass through a gateway or request log that stored them, resend them from there. Without a stored copy, a missed webhook can only be recovered by fetching the current state from the provider's API.",{"q":51,"a":52},"How do I prevent webhook replay attacks?","Verify a signature that covers a timestamp, and reject requests whose timestamp is more than a few minutes old. Stripe, Slack and Standard Webhooks all sign a timestamp for this reason. Also deduplicate on the event ID, so a replay inside the tolerance window has no effect.",{"q":54,"a":55},"Is replaying a webhook safe?","It is safe if your handler is idempotent. A replayed webhook usually carries the same event ID as the original, so a handler that records processed IDs will skip events it already handled and process only the ones it missed.",{"type":57,"children":58,"toc":727},"root",[59,87,94,99,141,146,226,233,238,341,347,359,364,378,539,582,611,617,636,642,721],{"type":60,"tag":61,"props":62,"children":63},"element","p",{},[64,71,73,78,80,85],{"type":60,"tag":65,"props":66,"children":67},"strong",{},[68],{"type":69,"value":70},"text","Webhook replay means sending a webhook that was already sent once, again.",{"type":69,"value":72}," The term has two meanings. ",{"type":60,"tag":65,"props":74,"children":75},{},[76],{"type":69,"value":77},"Deliberate replay",{"type":69,"value":79}," (redelivery) is a recovery tool: re-sending events your endpoint missed during an outage or failed to process because of a bug. A ",{"type":60,"tag":65,"props":81,"children":82},{},[83],{"type":69,"value":84},"replay attack",{"type":69,"value":86}," is the malicious version: someone captures a valid, correctly signed webhook and sends it again to repeat its effect. A good webhook setup supports the first and blocks the second.",{"type":60,"tag":88,"props":89,"children":91},"h2",{"id":90},"deliberate-replay-recovering-missed-webhooks",[92],{"type":69,"value":93},"Deliberate replay: recovering missed webhooks",{"type":60,"tag":61,"props":95,"children":96},{},[97],{"type":69,"value":98},"Typical reasons to replay:",{"type":60,"tag":100,"props":101,"children":102},"ul",{},[103,117,131,136],{"type":60,"tag":104,"props":105,"children":106},"li",{},[107,109,115],{"type":69,"value":108},"Your endpoint was down during a deploy and the provider's ",{"type":60,"tag":110,"props":111,"children":112},"a",{"href":36},[113],{"type":69,"value":114},"retries",{"type":69,"value":116}," ran out.",{"type":60,"tag":104,"props":118,"children":119},{},[120,122,129],{"type":69,"value":121},"A bug made your handler return ",{"type":60,"tag":123,"props":124,"children":126},"code",{"className":125},[],[127],{"type":69,"value":128},"200",{"type":69,"value":130}," without doing the work. You fixed it and need to reprocess the affected events.",{"type":60,"tag":104,"props":132,"children":133},{},[134],{"type":69,"value":135},"You added a new destination and want to backfill it with recent events.",{"type":60,"tag":104,"props":137,"children":138},{},[139],{"type":69,"value":140},"You're debugging and want to send the exact same request to a local build.",{"type":60,"tag":61,"props":142,"children":143},{},[144],{"type":69,"value":145},"Where you can replay from:",{"type":60,"tag":147,"props":148,"children":149},"table",{},[150,169],{"type":60,"tag":151,"props":152,"children":153},"thead",{},[154],{"type":60,"tag":155,"props":156,"children":157},"tr",{},[158,164],{"type":60,"tag":159,"props":160,"children":161},"th",{},[162],{"type":69,"value":163},"Source",{"type":60,"tag":159,"props":165,"children":166},{},[167],{"type":69,"value":168},"How",{"type":60,"tag":170,"props":171,"children":172},"tbody",{},[173,187,200,213],{"type":60,"tag":155,"props":174,"children":175},{},[176,182],{"type":60,"tag":177,"props":178,"children":179},"td",{},[180],{"type":69,"value":181},"The provider",{"type":60,"tag":177,"props":183,"children":184},{},[185],{"type":69,"value":186},"GitHub has a Redeliver button per delivery. Stripe lets you resend events from the dashboard. Many providers have nothing.",{"type":60,"tag":155,"props":188,"children":189},{},[190,195],{"type":60,"tag":177,"props":191,"children":192},{},[193],{"type":69,"value":194},"A request log or gateway",{"type":60,"tag":177,"props":196,"children":197},{},[198],{"type":69,"value":199},"Any tool that stored the original request can resend it.",{"type":60,"tag":155,"props":201,"children":202},{},[203,208],{"type":60,"tag":177,"props":204,"children":205},{},[206],{"type":69,"value":207},"Your own store",{"type":60,"tag":177,"props":209,"children":210},{},[211],{"type":69,"value":212},"If your endpoint saved the raw payload before processing, reprocess from there.",{"type":60,"tag":155,"props":214,"children":215},{},[216,221],{"type":60,"tag":177,"props":217,"children":218},{},[219],{"type":69,"value":220},"The provider's API",{"type":60,"tag":177,"props":222,"children":223},{},[224],{"type":69,"value":225},"No replay at all: list objects changed since the outage and reconcile.",{"type":60,"tag":227,"props":228,"children":230},"h3",{"id":229},"replay-in-webhook-relay",[231],{"type":69,"value":232},"Replay in Webhook Relay",{"type":60,"tag":61,"props":234,"children":235},{},[236],{"type":69,"value":237},"Because Webhook Relay stores each request before delivering it, you can replay without involving the provider:",{"type":60,"tag":100,"props":239,"children":240},{},[241,258,291],{"type":60,"tag":104,"props":242,"children":243},{},[244,249,251,256],{"type":60,"tag":65,"props":245,"children":246},{},[247],{"type":69,"value":248},"From the logs.",{"type":69,"value":250}," ",{"type":60,"tag":110,"props":252,"children":253},{"href":20},[254],{"type":69,"value":255},"Resend",{"type":69,"value":257}," any single delivery, recover failed deliveries in bulk after an endpoint comes back, or replay missing events to backfill a destination.",{"type":60,"tag":104,"props":259,"children":260},{},[261,266,268,274,276,282,284,289],{"type":60,"tag":65,"props":262,"children":263},{},[264],{"type":69,"value":265},"On agent connect.",{"type":69,"value":267}," For destinations on localhost or a private network, webhooks that arrive while no ",{"type":60,"tag":123,"props":269,"children":271},{"className":270},[],[272],{"type":69,"value":273},"relay",{"type":69,"value":275}," agent is connected are stored as ",{"type":60,"tag":123,"props":277,"children":279},{"className":278},[],[280],{"type":69,"value":281},"received",{"type":69,"value":283},". With ",{"type":60,"tag":110,"props":285,"children":286},{"href":22},[287],{"type":69,"value":288},"replay on agent connect",{"type":69,"value":290}," enabled, they're sent oldest-first as soon as the agent reconnects. You choose the lookback window (8 hours by default, up to 7 days) and the maximum number considered (100 by default, up to 5,000).",{"type":60,"tag":104,"props":292,"children":293},{},[294,299,301,307,309,315,317,323,325,331,333,339],{"type":60,"tag":65,"props":295,"children":296},{},[297],{"type":69,"value":298},"Outbound webhooks.",{"type":69,"value":300}," In the ",{"type":60,"tag":110,"props":302,"children":304},{"href":303},"/outbound-webhooks/",[305],{"type":69,"value":306},"Outbound Gateway",{"type":69,"value":308},", ",{"type":60,"tag":123,"props":310,"children":312},{"className":311},[],[313],{"type":69,"value":314},"recover",{"type":69,"value":316}," re-sends deliveries that ran out of retries and ",{"type":60,"tag":123,"props":318,"children":320},{"className":319},[],[321],{"type":69,"value":322},"replay-missing",{"type":69,"value":324}," sends deliveries skipped while an endpoint was paused or disabled, within the 35-day history. Every copy keeps the original ",{"type":60,"tag":123,"props":326,"children":328},{"className":327},[],[329],{"type":69,"value":330},"webhook-id",{"type":69,"value":332},". See ",{"type":60,"tag":110,"props":334,"children":336},{"href":335},"/docs/outbound/operating/",[337],{"type":69,"value":338},"monitor, retry and recover deliveries",{"type":69,"value":340},".",{"type":60,"tag":88,"props":342,"children":344},{"id":343},"replay-attacks",[345],{"type":69,"value":346},"Replay attacks",{"type":60,"tag":61,"props":348,"children":349},{},[350,352,357],{"type":69,"value":351},"If an attacker captures a signed webhook, from a log, a proxy, or a misconfigured request bin, the ",{"type":60,"tag":110,"props":353,"children":354},{"href":27},[355],{"type":69,"value":356},"signature",{"type":69,"value":358}," is still valid. Sending it again could, for example, mark an order paid twice.",{"type":60,"tag":61,"props":360,"children":361},{},[362],{"type":69,"value":363},"Defences:",{"type":60,"tag":365,"props":366,"children":367},"ol",{},[368],{"type":60,"tag":104,"props":369,"children":370},{},[371,376],{"type":60,"tag":65,"props":372,"children":373},{},[374],{"type":69,"value":375},"Sign a timestamp and check it.",{"type":69,"value":377}," If the signed string includes the time, the attacker can't change it without breaking the signature. Reject anything older than a few minutes:",{"type":60,"tag":379,"props":380,"children":384},"pre",{"className":381,"code":382,"language":383,"meta":7,"style":7},"language-js shiki shiki-themes github-dark","const age = Math.abs(Date.now() / 1000 - Number(headers['webhook-timestamp']))\nif (age > 300) return res.sendStatus(401)   // outside the 5-minute tolerance\n","js",[385],{"type":60,"tag":123,"props":386,"children":387},{"__ignoreMap":7},[388,474],{"type":60,"tag":389,"props":390,"children":393},"span",{"class":391,"line":392},"line",1,[394,400,406,411,417,423,428,433,438,443,448,453,458,463,469],{"type":60,"tag":389,"props":395,"children":397},{"style":396},"--shiki-default:#F97583",[398],{"type":69,"value":399},"const",{"type":60,"tag":389,"props":401,"children":403},{"style":402},"--shiki-default:#79B8FF",[404],{"type":69,"value":405}," age",{"type":60,"tag":389,"props":407,"children":408},{"style":396},[409],{"type":69,"value":410}," =",{"type":60,"tag":389,"props":412,"children":414},{"style":413},"--shiki-default:#E1E4E8",[415],{"type":69,"value":416}," Math.",{"type":60,"tag":389,"props":418,"children":420},{"style":419},"--shiki-default:#B392F0",[421],{"type":69,"value":422},"abs",{"type":60,"tag":389,"props":424,"children":425},{"style":413},[426],{"type":69,"value":427},"(Date.",{"type":60,"tag":389,"props":429,"children":430},{"style":419},[431],{"type":69,"value":432},"now",{"type":60,"tag":389,"props":434,"children":435},{"style":413},[436],{"type":69,"value":437},"() ",{"type":60,"tag":389,"props":439,"children":440},{"style":396},[441],{"type":69,"value":442},"/",{"type":60,"tag":389,"props":444,"children":445},{"style":402},[446],{"type":69,"value":447}," 1000",{"type":60,"tag":389,"props":449,"children":450},{"style":396},[451],{"type":69,"value":452}," -",{"type":60,"tag":389,"props":454,"children":455},{"style":419},[456],{"type":69,"value":457}," Number",{"type":60,"tag":389,"props":459,"children":460},{"style":413},[461],{"type":69,"value":462},"(headers[",{"type":60,"tag":389,"props":464,"children":466},{"style":465},"--shiki-default:#9ECBFF",[467],{"type":69,"value":468},"'webhook-timestamp'",{"type":60,"tag":389,"props":470,"children":471},{"style":413},[472],{"type":69,"value":473},"]))\n",{"type":60,"tag":389,"props":475,"children":477},{"class":391,"line":476},2,[478,483,488,493,498,503,508,513,518,523,528,533],{"type":60,"tag":389,"props":479,"children":480},{"style":396},[481],{"type":69,"value":482},"if",{"type":60,"tag":389,"props":484,"children":485},{"style":413},[486],{"type":69,"value":487}," (age ",{"type":60,"tag":389,"props":489,"children":490},{"style":396},[491],{"type":69,"value":492},">",{"type":60,"tag":389,"props":494,"children":495},{"style":402},[496],{"type":69,"value":497}," 300",{"type":60,"tag":389,"props":499,"children":500},{"style":413},[501],{"type":69,"value":502},") ",{"type":60,"tag":389,"props":504,"children":505},{"style":396},[506],{"type":69,"value":507},"return",{"type":60,"tag":389,"props":509,"children":510},{"style":413},[511],{"type":69,"value":512}," res.",{"type":60,"tag":389,"props":514,"children":515},{"style":419},[516],{"type":69,"value":517},"sendStatus",{"type":60,"tag":389,"props":519,"children":520},{"style":413},[521],{"type":69,"value":522},"(",{"type":60,"tag":389,"props":524,"children":525},{"style":402},[526],{"type":69,"value":527},"401",{"type":60,"tag":389,"props":529,"children":530},{"style":413},[531],{"type":69,"value":532},")   ",{"type":60,"tag":389,"props":534,"children":536},{"style":535},"--shiki-default:#6A737D",[537],{"type":69,"value":538},"// outside the 5-minute tolerance\n",{"type":60,"tag":61,"props":540,"children":541},{},[542,544,550,552,558,560,566,568,572,574,580],{"type":69,"value":543},"Stripe (",{"type":60,"tag":123,"props":545,"children":547},{"className":546},[],[548],{"type":69,"value":549},"t=",{"type":69,"value":551}," in ",{"type":60,"tag":123,"props":553,"children":555},{"className":554},[],[556],{"type":69,"value":557},"Stripe-Signature",{"type":69,"value":559},"), Slack (",{"type":60,"tag":123,"props":561,"children":563},{"className":562},[],[564],{"type":69,"value":565},"X-Slack-Request-Timestamp",{"type":69,"value":567},") and ",{"type":60,"tag":110,"props":569,"children":570},{"href":39},[571],{"type":69,"value":38},{"type":69,"value":573}," (",{"type":60,"tag":123,"props":575,"children":577},{"className":576},[],[578],{"type":69,"value":579},"webhook-timestamp",{"type":69,"value":581},") all sign a timestamp. GitHub doesn't, so rely on the delivery ID there.",{"type":60,"tag":365,"props":583,"children":584},{"start":476},[585,601],{"type":60,"tag":104,"props":586,"children":587},{},[588,593,595,600],{"type":60,"tag":65,"props":589,"children":590},{},[591],{"type":69,"value":592},"Deduplicate on the event ID.",{"type":69,"value":594}," A replay inside the tolerance window still carries an ID you've already processed. See ",{"type":60,"tag":110,"props":596,"children":597},{"href":30},[598],{"type":69,"value":599},"idempotency",{"type":69,"value":340},{"type":60,"tag":104,"props":602,"children":603},{},[604,609],{"type":60,"tag":65,"props":605,"children":606},{},[607],{"type":69,"value":608},"Keep your clocks synced.",{"type":69,"value":610}," Timestamp checks need NTP on the receiving server, or legitimate requests start failing.",{"type":60,"tag":88,"props":612,"children":614},{"id":613},"making-deliberate-replay-work-with-timestamp-checks",[615],{"type":69,"value":616},"Making deliberate replay work with timestamp checks",{"type":60,"tag":61,"props":618,"children":619},{},[620,622,627,629,635],{"type":69,"value":621},"There's a tension: a replay of a week-old webhook fails a 5-minute timestamp check. Good senders re-sign on every attempt. Standard Webhooks senders, including the Webhook Relay Outbound Gateway, give each retry a new timestamp and signature while keeping the same ",{"type":60,"tag":123,"props":623,"children":625},{"className":624},[],[626],{"type":69,"value":330},{"type":69,"value":628},", so the receiver accepts it and still recognises it as the same event. If you replay a captured request yourself to a local build, either turn off the timestamp check in development or re-sign the request with the ",{"type":60,"tag":110,"props":630,"children":632},{"href":631},"/standard-webhooks-signature-generator/",[633],{"type":69,"value":634},"signature generator",{"type":69,"value":340},{"type":60,"tag":88,"props":637,"children":639},{"id":638},"common-mistakes",[640],{"type":69,"value":641},"Common mistakes",{"type":60,"tag":100,"props":643,"children":644},{},[645,655,673,683],{"type":60,"tag":104,"props":646,"children":647},{},[648,653],{"type":60,"tag":65,"props":649,"children":650},{},[651],{"type":69,"value":652},"Non-idempotent handlers.",{"type":69,"value":654}," Replaying a batch that includes events you already processed then repeats their side effects.",{"type":60,"tag":104,"props":656,"children":657},{},[658,663,665,671],{"type":60,"tag":65,"props":659,"children":660},{},[661],{"type":69,"value":662},"No stored copy.",{"type":69,"value":664}," If nothing kept the raw request, there's nothing to replay. Store payloads, or put a ",{"type":60,"tag":110,"props":666,"children":668},{"href":667},"/glossary/webhook-gateway/",[669],{"type":69,"value":670},"gateway",{"type":69,"value":672}," in front that does.",{"type":60,"tag":104,"props":674,"children":675},{},[676,681],{"type":60,"tag":65,"props":677,"children":678},{},[679],{"type":69,"value":680},"Replaying out of order.",{"type":69,"value":682}," Replay oldest-first, and make handlers compare timestamps rather than trusting arrival order.",{"type":60,"tag":104,"props":684,"children":685},{},[686,691,693,703,705,711,713,719],{"type":60,"tag":65,"props":687,"children":688},{},[689],{"type":69,"value":690},"Dumping a large backlog at full speed.",{"type":69,"value":692}," A replay of thousands of events can overload the endpoint and trigger ",{"type":60,"tag":110,"props":694,"children":696},{"href":695},"/http-errors/429-too-many-requests/",[697],{"type":60,"tag":123,"props":698,"children":700},{"className":699},[],[701],{"type":69,"value":702},"429",{"type":69,"value":704}," or ",{"type":60,"tag":123,"props":706,"children":708},{"className":707},[],[709],{"type":69,"value":710},"503",{"type":69,"value":712},". Limit the batch size or ",{"type":60,"tag":110,"props":714,"children":716},{"href":715},"/features/throttling/",[717],{"type":69,"value":718},"throttle",{"type":69,"value":720}," delivery.",{"type":60,"tag":722,"props":723,"children":724},"style",{},[725],{"type":69,"value":726},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":728,"depth":728,"links":729},3,[730,733,734,735],{"id":90,"depth":476,"text":93,"children":731},[732],{"id":229,"depth":728,"text":232},{"id":343,"depth":476,"text":346},{"id":613,"depth":476,"text":616},{"id":638,"depth":476,"text":641},"markdown","content:glossary:webhook-replay.md","content","glossary/webhook-replay.md","glossary/webhook-replay","md",{"loc":4},1793199806856]