[{"data":1,"prerenderedAt":625},["ShallowReactive",2],{"content-query-yAaivIk9N3":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"author":11,"tags":12,"categories":16,"date":18,"image":19,"faq":20,"body":30,"_type":618,"_id":619,"_source":620,"_file":621,"_stem":622,"_extension":623,"sitemap":624},"/blog/webhook-architecture-diagram","blog",false,"","Webhook Architecture Diagram: How Webhooks Flow End to End","A clear webhook architecture diagram and breakdown — from the sender that emits an event to the endpoint that receives it, plus the queue, retries, signature verification and fan-out a production-grade webhook architecture needs.","post","Karolis Rusenas",[13,14,15],"webhooks","architecture","guide",[17],"guides","2026-07-19 09:30:00","/images/generic/webhooks.png",[21,24,27],{"q":22,"a":23},"What does a webhook architecture look like?","At its simplest: a sender (the provider) emits an event, makes an HTTP POST to a public URL you control, and your endpoint acknowledges with a 2xx and processes the payload — ideally asynchronously via a queue. A production-grade webhook architecture adds signature verification, durable retries, a queue for backpressure, fan-out to multiple destinations and delivery logging.",{"q":25,"a":26},"What are the components of a webhook system?","The sender/producer that emits events, the HTTP POST request itself (headers, signature and JSON body), the public receiver endpoint, a fast acknowledgement (2xx), and asynchronous processing — usually a queue plus workers. Reliable systems also include retry logic, idempotency/de-duplication and observability.",{"q":28,"a":29},"How do I make a webhook architecture reliable?","Acknowledge quickly and process asynchronously, verify the signature, make handlers idempotent so duplicate deliveries are safe, and put durable retries and a queue in front of your consumer so an outage or spike doesn't lose events. A webhook gateway provides these without you building them yourself.",{"type":31,"children":32,"toc":610},"root",[33,58,65,70,172,223,230,361,367,383,456,465,471,491,505,533,539,582,604],{"type":34,"tag":35,"props":36,"children":37},"element","p",{},[38,41,47,49,56],{"type":39,"value":40},"text","Webhooks are simple in principle — one server POSTs to another when something happens — but a reliable webhook system has a few more moving parts than people expect. This is the ",{"type":34,"tag":42,"props":43,"children":44},"strong",{},[45],{"type":39,"value":46},"webhook architecture",{"type":39,"value":48},", from the event that starts it to the handler that finishes it. (New to the concept? Start with ",{"type":34,"tag":50,"props":51,"children":53},"a",{"href":52},"/blog/what-is-webhook",[54],{"type":39,"value":55},"what is a webhook",{"type":39,"value":57},".)",{"type":34,"tag":59,"props":60,"children":62},"h2",{"id":61},"the-basic-webhook-architecture",[63],{"type":39,"value":64},"The basic webhook architecture",{"type":34,"tag":35,"props":66,"children":67},{},[68],{"type":39,"value":69},"At its core, every webhook flows the same way:",{"type":34,"tag":71,"props":72,"children":75},"pre",{"className":73,"code":74,"language":39,"meta":7,"style":7},"language-text shiki shiki-themes github-dark","┌────────────┐   event happens   ┌──────────────┐   HTTP POST    ┌──────────────┐\n│  Sender    │ ────────────────► │  Webhook      │ ─────────────► │  Your public │\n│ (provider) │                   │  (HTTP POST)  │                │  endpoint    │\n└────────────┘                   └──────────────┘   200 OK  ◄──── └──────┬───────┘\n                                                                          │ enqueue\n                                                                          ▼\n                                                                   ┌──────────────┐\n                                                                   │  Your queue  │\n                                                                   │  + workers   │\n                                                                   └──────────────┘\n",[76],{"type":34,"tag":77,"props":78,"children":79},"code",{"__ignoreMap":7},[80,91,100,109,118,127,136,145,154,163],{"type":34,"tag":81,"props":82,"children":85},"span",{"class":83,"line":84},"line",1,[86],{"type":34,"tag":81,"props":87,"children":88},{},[89],{"type":39,"value":90},"┌────────────┐   event happens   ┌──────────────┐   HTTP POST    ┌──────────────┐\n",{"type":34,"tag":81,"props":92,"children":94},{"class":83,"line":93},2,[95],{"type":34,"tag":81,"props":96,"children":97},{},[98],{"type":39,"value":99},"│  Sender    │ ────────────────► │  Webhook      │ ─────────────► │  Your public │\n",{"type":34,"tag":81,"props":101,"children":103},{"class":83,"line":102},3,[104],{"type":34,"tag":81,"props":105,"children":106},{},[107],{"type":39,"value":108},"│ (provider) │                   │  (HTTP POST)  │                │  endpoint    │\n",{"type":34,"tag":81,"props":110,"children":112},{"class":83,"line":111},4,[113],{"type":34,"tag":81,"props":114,"children":115},{},[116],{"type":39,"value":117},"└────────────┘                   └──────────────┘   200 OK  ◄──── └──────┬───────┘\n",{"type":34,"tag":81,"props":119,"children":121},{"class":83,"line":120},5,[122],{"type":34,"tag":81,"props":123,"children":124},{},[125],{"type":39,"value":126},"                                                                          │ enqueue\n",{"type":34,"tag":81,"props":128,"children":130},{"class":83,"line":129},6,[131],{"type":34,"tag":81,"props":132,"children":133},{},[134],{"type":39,"value":135},"                                                                          ▼\n",{"type":34,"tag":81,"props":137,"children":139},{"class":83,"line":138},7,[140],{"type":34,"tag":81,"props":141,"children":142},{},[143],{"type":39,"value":144},"                                                                   ┌──────────────┐\n",{"type":34,"tag":81,"props":146,"children":148},{"class":83,"line":147},8,[149],{"type":34,"tag":81,"props":150,"children":151},{},[152],{"type":39,"value":153},"                                                                   │  Your queue  │\n",{"type":34,"tag":81,"props":155,"children":157},{"class":83,"line":156},9,[158],{"type":34,"tag":81,"props":159,"children":160},{},[161],{"type":39,"value":162},"                                                                   │  + workers   │\n",{"type":34,"tag":81,"props":164,"children":166},{"class":83,"line":165},10,[167],{"type":34,"tag":81,"props":168,"children":169},{},[170],{"type":39,"value":171},"                                                                   └──────────────┘\n",{"type":34,"tag":173,"props":174,"children":175},"ol",{},[176,187,199],{"type":34,"tag":177,"props":178,"children":179},"li",{},[180,185],{"type":34,"tag":42,"props":181,"children":182},{},[183],{"type":39,"value":184},"The sender",{"type":39,"value":186}," (Stripe, GitHub, Twilio…) detects an event.",{"type":34,"tag":177,"props":188,"children":189},{},[190,192,197],{"type":39,"value":191},"It makes an ",{"type":34,"tag":42,"props":193,"children":194},{},[195],{"type":39,"value":196},"HTTP POST",{"type":39,"value":198}," to the URL you registered, carrying headers (content type, an event-type header, a signature) and a JSON body.",{"type":34,"tag":177,"props":200,"children":201},{},[202,207,209,214,216,221],{"type":34,"tag":42,"props":203,"children":204},{},[205],{"type":39,"value":206},"Your endpoint",{"type":39,"value":208}," receives it, returns a fast ",{"type":34,"tag":42,"props":210,"children":211},{},[212],{"type":39,"value":213},"2xx",{"type":39,"value":215}," to acknowledge, and hands the payload to a ",{"type":34,"tag":42,"props":217,"children":218},{},[219],{"type":39,"value":220},"queue",{"type":39,"value":222}," so a worker can process it without holding up the response.",{"type":34,"tag":224,"props":225,"children":227},"h3",{"id":226},"the-components",[228],{"type":39,"value":229},"The components",{"type":34,"tag":231,"props":232,"children":233},"table",{},[234,253],{"type":34,"tag":235,"props":236,"children":237},"thead",{},[238],{"type":34,"tag":239,"props":240,"children":241},"tr",{},[242,248],{"type":34,"tag":243,"props":244,"children":245},"th",{},[246],{"type":39,"value":247},"Component",{"type":34,"tag":243,"props":249,"children":250},{},[251],{"type":39,"value":252},"Role",{"type":34,"tag":254,"props":255,"children":256},"tbody",{},[257,274,290,313,329,345],{"type":34,"tag":239,"props":258,"children":259},{},[260,269],{"type":34,"tag":261,"props":262,"children":263},"td",{},[264],{"type":34,"tag":42,"props":265,"children":266},{},[267],{"type":39,"value":268},"Sender / producer",{"type":34,"tag":261,"props":270,"children":271},{},[272],{"type":39,"value":273},"The provider that emits the event and POSTs the webhook",{"type":34,"tag":239,"props":275,"children":276},{},[277,285],{"type":34,"tag":261,"props":278,"children":279},{},[280],{"type":34,"tag":42,"props":281,"children":282},{},[283],{"type":39,"value":284},"HTTP request",{"type":34,"tag":261,"props":286,"children":287},{},[288],{"type":39,"value":289},"Method + URL, headers (incl. the signature), and the JSON payload",{"type":34,"tag":239,"props":291,"children":292},{},[293,301],{"type":34,"tag":261,"props":294,"children":295},{},[296],{"type":34,"tag":42,"props":297,"children":298},{},[299],{"type":39,"value":300},"Receiver endpoint",{"type":34,"tag":261,"props":302,"children":303},{},[304,306,311],{"type":39,"value":305},"The public URL that accepts the POST and returns ",{"type":34,"tag":77,"props":307,"children":309},{"className":308},[],[310],{"type":39,"value":213},{"type":39,"value":312}," fast",{"type":34,"tag":239,"props":314,"children":315},{},[316,324],{"type":34,"tag":261,"props":317,"children":318},{},[319],{"type":34,"tag":42,"props":320,"children":321},{},[322],{"type":39,"value":323},"Signature verification",{"type":34,"tag":261,"props":325,"children":326},{},[327],{"type":39,"value":328},"Confirms the request is authentic (HMAC) before you trust it",{"type":34,"tag":239,"props":330,"children":331},{},[332,340],{"type":34,"tag":261,"props":333,"children":334},{},[335],{"type":34,"tag":42,"props":336,"children":337},{},[338],{"type":39,"value":339},"Queue + workers",{"type":34,"tag":261,"props":341,"children":342},{},[343],{"type":39,"value":344},"Absorbs bursts and processes events asynchronously",{"type":34,"tag":239,"props":346,"children":347},{},[348,356],{"type":34,"tag":261,"props":349,"children":350},{},[351],{"type":34,"tag":42,"props":352,"children":353},{},[354],{"type":39,"value":355},"Retry logic",{"type":34,"tag":261,"props":357,"children":358},{},[359],{"type":39,"value":360},"Re-delivers events your endpoint couldn't accept",{"type":34,"tag":59,"props":362,"children":364},{"id":363},"production-grade-webhook-architecture",[365],{"type":39,"value":366},"Production-grade webhook architecture",{"type":34,"tag":35,"props":368,"children":369},{},[370,372,381],{"type":39,"value":371},"The basic flow works until real traffic hits it — a deploy drops events, a spike overwhelms a fragile service, a forged request slips through, or you need the same event in three systems. A production webhook architecture puts a ",{"type":34,"tag":42,"props":373,"children":374},{},[375],{"type":34,"tag":50,"props":376,"children":378},{"href":377},"/webhook-gateway",[379],{"type":39,"value":380},"webhook gateway",{"type":39,"value":382}," between the sender and your services to handle exactly that:",{"type":34,"tag":384,"props":385,"children":386},"ul",{},[387,400,414,428,442],{"type":34,"tag":177,"props":388,"children":389},{},[390,398],{"type":34,"tag":42,"props":391,"children":392},{},[393],{"type":34,"tag":50,"props":394,"children":396},{"href":395},"/blog/webhook-authentication",[397],{"type":39,"value":323},{"type":39,"value":399}," so only genuine events are processed.",{"type":34,"tag":177,"props":401,"children":402},{},[403,412],{"type":34,"tag":42,"props":404,"children":405},{},[406],{"type":34,"tag":50,"props":407,"children":409},{"href":408},"/features/durable-retries",[410],{"type":39,"value":411},"Durable retries",{"type":39,"value":413}," so an event survives an outage or a deploy — persisted and retried for up to 30 days.",{"type":34,"tag":177,"props":415,"children":416},{},[417,426],{"type":34,"tag":42,"props":418,"children":419},{},[420],{"type":34,"tag":50,"props":421,"children":423},{"href":422},"/features/throttling",[424],{"type":39,"value":425},"Throttling",{"type":39,"value":427}," and a queue so a burst becomes a steady, survivable stream.",{"type":34,"tag":177,"props":429,"children":430},{},[431,440],{"type":34,"tag":42,"props":432,"children":433},{},[434],{"type":34,"tag":50,"props":435,"children":437},{"href":436},"/features/webhook-multiple-destinations",[438],{"type":39,"value":439},"Fan-out to multiple destinations",{"type":39,"value":441}," so one event reaches your API, a backup and an analytics sink at once.",{"type":34,"tag":177,"props":443,"children":444},{},[445,454],{"type":34,"tag":42,"props":446,"children":447},{},[448],{"type":34,"tag":50,"props":449,"children":451},{"href":450},"/features/webhook-logs",[452],{"type":39,"value":453},"Delivery logs",{"type":39,"value":455}," so you can see, inspect and replay every delivery.",{"type":34,"tag":35,"props":457,"children":458},{},[459],{"type":34,"tag":460,"props":461,"children":464},"img",{"alt":462,"src":463},"A production webhook architecture with Webhook Relay as the gateway","/images/webhookrelay_basic_diagram.png",[],{"type":34,"tag":59,"props":466,"children":468},{"id":467},"webhook-architecture-for-local-development",[469],{"type":39,"value":470},"Webhook architecture for local development",{"type":34,"tag":35,"props":472,"children":473},{},[474,476,481,483,489],{"type":39,"value":475},"There's one more architecture worth drawing: ",{"type":34,"tag":42,"props":477,"children":478},{},[479],{"type":39,"value":480},"receiving webhooks during development",{"type":39,"value":482},". Providers can only POST to public URLs, but your handler runs on ",{"type":34,"tag":77,"props":484,"children":486},{"className":485},[],[487],{"type":39,"value":488},"localhost",{"type":39,"value":490}," or inside a private network. A reverse tunnel closes the gap:",{"type":34,"tag":71,"props":492,"children":494},{"className":73,"code":493,"language":39,"meta":7,"style":7},"Provider ──POST──► Webhook Relay (stable public URL) ──► relay agent (outbound) ──► localhost:3000\n",[495],{"type":34,"tag":77,"props":496,"children":497},{"__ignoreMap":7},[498],{"type":34,"tag":81,"props":499,"children":500},{"class":83,"line":84},[501],{"type":34,"tag":81,"props":502,"children":503},{},[504],{"type":39,"value":493},{"type":34,"tag":35,"props":506,"children":507},{},[508,510,516,518,523,525,531],{"type":39,"value":509},"The ",{"type":34,"tag":50,"props":511,"children":513},{"href":512},"/webhooks",[514],{"type":39,"value":515},"relay agent",{"type":39,"value":517}," connects ",{"type":34,"tag":42,"props":519,"children":520},{},[521],{"type":39,"value":522},"outbound",{"type":39,"value":524},", so events reach your machine with no public IP and no open firewall ports — and the URL stays fixed so you configure the provider once. See ",{"type":34,"tag":50,"props":526,"children":528},{"href":527},"/blog/how-to-test-webhooks",[529],{"type":39,"value":530},"how to test webhooks",{"type":39,"value":532},".",{"type":34,"tag":59,"props":534,"children":536},{"id":535},"build-the-diagram-into-something-real",[537],{"type":39,"value":538},"Build the diagram into something real",{"type":34,"tag":384,"props":540,"children":541},{},[542,552,571],{"type":34,"tag":177,"props":543,"children":544},{},[545,550],{"type":34,"tag":50,"props":546,"children":547},{"href":52},[548],{"type":39,"value":549},"What is a webhook",{"type":39,"value":551}," — the anatomy of a single request.",{"type":34,"tag":177,"props":553,"children":554},{},[555,561,563,569],{"type":34,"tag":50,"props":556,"children":558},{"href":557},"/blog/webhooks-vs-api",[559],{"type":39,"value":560},"Webhooks vs API",{"type":39,"value":562}," and ",{"type":34,"tag":50,"props":564,"children":566},{"href":565},"/blog/webhook-vs-websocket",[567],{"type":39,"value":568},"webhook vs WebSocket",{"type":39,"value":570}," — where webhooks fit.",{"type":34,"tag":177,"props":572,"children":573},{},[574,580],{"type":34,"tag":50,"props":575,"children":577},{"href":576},"/blog/webhook-security",[578],{"type":39,"value":579},"Webhook security best practices",{"type":39,"value":581}," — verification, secrets and idempotency.",{"type":34,"tag":35,"props":583,"children":584},{},[585,587,593,595,603],{"type":39,"value":586},"Want to watch a real request flow through this architecture? ",{"type":34,"tag":50,"props":588,"children":590},{"href":589},"/webhook-bin",[591],{"type":39,"value":592},"Open a free Webhook Bin",{"type":39,"value":594}," and send it a webhook, or ",{"type":34,"tag":50,"props":596,"children":600},{"href":597,"rel":598},"https://my.webhookrelay.com/register",[599],"nofollow",[601],{"type":39,"value":602},"start forwarding to localhost for free",{"type":39,"value":532},{"type":34,"tag":605,"props":606,"children":607},"style",{},[608],{"type":39,"value":609},"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":102,"depth":102,"links":611},[612,615,616,617],{"id":61,"depth":93,"text":64,"children":613},[614],{"id":226,"depth":102,"text":229},{"id":363,"depth":93,"text":366},{"id":467,"depth":93,"text":470},{"id":535,"depth":93,"text":538},"markdown","content:blog:webhook-architecture-diagram.md","content","blog/webhook-architecture-diagram.md","blog/webhook-architecture-diagram","md",{"loc":4},1784458885534]