Webhook Flow Diagram Guide
How to draw a webhook flow diagram that shows what actually matters — where the 200 goes, what a retry does to you, and three editable examples for both sides of the call.
What is a webhook flow diagram?
A webhook flow diagram shows an HTTP request arriving at your server that you did not ask for, and — this is the part that makes it worth drawing — everything the sender does when your response is not a 2xx.
An ordinary API diagram has one arrow out and one arrow back, and if the call fails the caller finds out immediately and decides what to do. A webhook inverts every part of that. You are the callee, the caller is a system you do not operate, and the decision about what happens after a failure was made by someone else and written into their retry schedule. Your 200 is not a return value. It is a promise, made to a stranger, that they can stop holding this event.
That promise is why the delivery guarantee is at-least-once rather than exactly-once, and why the honest subject of this diagram is duplicates rather than the happy path. A network timeout after your database commit looks identical, from the sender's side, to a request that never arrived — so it gets sent again. Any webhook receiver that has been running long enough has processed the same event twice; the only question is whether it noticed.
So the diagram has two jobs: mark exactly where the acknowledgement is sent and what is durable at that instant, and show what a second copy of the same event does. Everything else is arrows.
What a webhook flow diagram has to show
Six things. The first one is the diagram's spine — get it wrong and the rest cannot save it.
- The ack boundary, drawn as a line. Mark where the
200is returned and state what is durably stored at that moment. Everything above the line is a promise you are keeping; everything below it is your own problem. The most common design error in webhook handling is doing the work above the line, which means a slow database turns into a retry storm from a system you cannot throttle. - Signature verification, on the raw bytes, before parsing. Draw it as the first arrow inside your endpoint. It has to happen on the body exactly as received, because any JSON round-trip changes whitespace and key order and the signature will no longer match. Framework middleware that parses bodies automatically is the usual culprit, and the diagram is where you notice the order is wrong.
- The event id and where it is remembered. Idempotency is not a property you can wish for; it is a unique index on a column. Draw the insert, and draw what happens when it collides — because "we already have this one" is a success, not an error, and returning a 500 there guarantees the sender keeps trying.
- The retry schedule, with real numbers. "It retries" is not a design. Write the attempts and the backoff on the diagram — 5 attempts at 10s, 1m, 10m, 1h, 6h is a normal shape — because those numbers determine how long a bad deploy keeps hurting and how far behind your data can be at the worst moment.
- Arrival order is not event order. Two events created a second apart can arrive in either sequence, and a retry of an old event can land after a newer one. If your handler writes fields directly from the payload, draw the version or timestamp comparison that stops a stale event from overwriting a fresh one. If there is no such comparison, the diagram has just told you about a bug that reproduces once a month and never in staging.
- What happens when the retries run out. Dead letter queue, alert, manual replay — draw whichever you have. Systems without this branch do not lose events loudly; they lose them silently, and you find out from a customer who is looking at an order that never moved.
Here is the skeleton, and it contains the mistake. Every arrow is correct and the shape is what most teams ship: receive, do the work, reply. The bug is the position of the last arrow — the 200 is behind the work, so the sender's timeout is now effectively your processing budget, and it was set by someone who has never seen your code.
View the Mermaid source
sequenceDiagram
participant Src as Source System
participant Hook as Your Endpoint
participant DB as Your Database
Src->>Hook: POST an event
Hook->>DB: do all the work
DB-->>Hook: done
Hook-->>Src: 200 OKHow to draw a webhook flow diagram
Three steps, and the first one is a question you have to answer before drawing anything.
Step 1 · Decide which side you are drawing
Receiving webhooks and sending them are two different diagrams with almost no overlap, and trying to draw both at once produces something that describes neither.
Receiving. Your participants are the source system, your endpoint, your storage, and usually a queue and a worker. Your failure modes are: an event you cannot verify, an event you have already seen, an event you cannot process yet, and an event that arrived out of order. You control none of the retry behaviour, which is exactly why it belongs on your diagram — it is an input to your design.
Sending. Your participants are your application, a delivery service, and an endpoint belonging to someone whose availability is unrelated to yours. Your failure modes are: a subscriber that is down, one that is slow enough to tie up your workers, one that returns 200 to everything including things it did not understand, and one whose URL stopped existing six months ago. Here the retry schedule is a decision you are making, and the diagram is where you make it visible enough to argue about.
Pick one. If you do both, draw both, and put them on different pages.
Step 2 · Draw the ack boundary before anything else
On the receiving side, put down two arrows before you draw a single feature: the request coming in, and the 200 going back. Then answer one question in writing on the diagram — what is durably stored at the moment that 200 leaves?
If the answer is "the event, and nothing else has happened yet", you have the right shape. Verify, persist, acknowledge, then process asynchronously. The sender is released in milliseconds, your processing time is yours again, and a failure during processing is a bug you retry on your own schedule rather than an outage the sender amplifies.
If the answer involves your business logic, three things follow, and they arrive together. Your endpoint's latency is now bounded by whatever the sender's timeout happens to be. A slow dependency turns every in-flight event into a retry, so load goes up exactly when you are least able to serve it. And a failure halfway through leaves you with the sender still holding the event — which is survivable — plus whatever partial state you already wrote — which frequently is not.
There is one legitimate exception: a handler that is genuinely a single idempotent write, finishing in a few milliseconds. Draw it, keep it that way, and expect to revisit the decision the first time somebody adds an email send to it.
Step 3 · Draw it
Describe it in sentences and let text2diagram lay it out — "the source posts an event, we verify the signature on the raw body, insert the event id into a table with a unique index, return 200, then a worker picks it up from a queue and processes it; a duplicate event id returns 200 without doing anything" comes back as an editable sequenceDiagram with the branches already nested.
Or open one of the three below and rename the participants. The branch structure is the part worth keeping.
Webhook flow diagram examples
Three diagrams: receiving one correctly, sending one and dealing with a subscriber who will not cooperate, and the two arrival problems that only show up in production.
1 · Receiving, with the ack in the right place
The Note is doing real work here — it marks the ack boundary, which is the one thing a reader must not have to infer. Everything above it happens while the sender waits; everything below is on your own time.
Three details are worth copying exactly. Signature verification is the first thing, on the raw bytes, before any parsing — and note that the rejection branch stores nothing, because writing unverified data is how a public endpoint becomes a public database.
The duplicate branch returns 200, not 409. This is the arrow people get wrong. A duplicate means the sender already succeeded and lost the response; telling them it failed guarantees they keep retrying an event you have handled. The unique index is what makes this cheap — the database decides, not a read-then-write race.
And the work happens after the ack. If the worker fails, that is your retry, on your schedule, with your logs. The sender never finds out, because as far as it is concerned this event was delivered — which is exactly what you promised.
View the Mermaid source
sequenceDiagram
autonumber
participant Src as Source System
participant Hook as Webhook Endpoint
participant Log as Event Log
participant Q as Queue
participant W as Worker
Src->>Hook: POST raw body plus signature header
Hook->>Hook: verify the signature on the raw bytes
alt signature does not match
Hook-->>Src: 401 and nothing is stored
else signature is valid
Hook->>Log: insert event_id, unique index
alt event_id is already there
Log-->>Hook: duplicate
Hook-->>Src: 200 already handled
else first time we see it
Log-->>Hook: stored
Hook->>Q: enqueue event_id
Hook-->>Src: 200 accepted
Note over Hook,Src: the ack ends here - everything below is our own time
Q->>W: deliver event_id
W->>Log: load the payload and process it
end
end2 · Sending — one delivery, from creation to dead letter
A state diagram, because a delivery attempt is an object with a life, not a conversation. This is the diagram to put in front of anyone asking "what happens if their endpoint is down" — the answer is a path through it, and the path takes seven hours.
The split between Failed and Dropped is the part worth arguing about. A 5xx or a timeout means try again, they might recover. A 4xx means your payload is wrong and it will still be wrong in an hour — retrying it is just noise in two systems. The one exception is 429, which is a 4xx that explicitly means retry, and it belongs on the Failed side. Getting this split wrong in either direction is expensive: retry everything and you hammer a subscriber over a payload they will never accept; retry nothing and one bad minute costs a customer their data.
DeadLetter having an arrow back into Delivering is the difference between an operable system and one where an incident ends with an apology. Somebody's endpoint will be down for a day. When it comes back, you need a button.
View the Mermaid source
stateDiagram-v2
[*] --> Pending: event created
Pending --> Delivering: attempt 1
Delivering --> Delivered: 2xx
Delivering --> Failed: 5xx, timeout or connection refused
Delivering --> Dropped: 4xx other than 429
Failed --> Waiting: attempts remaining
Waiting --> Delivering: backoff 10s, 1m, 10m, 1h, 6h
Failed --> DeadLetter: all 5 attempts used
DeadLetter --> Delivering: manual replay
Delivered --> [*]
Dropped --> [*]
note right of Dropped
a 4xx means our payload is wrong
an hour of backoff will not fix that
end note3 · The two things that only happen in production
Both halves of this diagram describe events that are, individually, delivered perfectly. Nothing here is a failure. That is why neither shows up in testing, where events are created one at a time by a person who then looks at the result.
The top half is at-least-once delivery doing exactly what it promises. Your handler succeeded, the response was lost on the way back, and the sender did the only correct thing available to it. The unique index turns a potential double-charge into a no-op, and the reply is still a 200.
The bottom half is the one that produces the ticket nobody can reproduce. A retry of an older event lands after a newer one, and a handler that writes fields straight from the payload will happily move a delivered order back to shipped. The fix is one comparison — reject writes whose version is not newer than what is stored — and it is one arrow on the diagram. Its absence is also one arrow on the diagram, which is the entire argument for drawing this.
View the Mermaid source
sequenceDiagram
autonumber
participant Src as Source System
participant Hook as Webhook Endpoint
participant DB as Order Table
Note over Src,Hook: same event twice - our 200 was lost on the way back
Src->>Hook: evt_9 order.shipped
Hook->>DB: insert evt_9, then set status shipped
Hook-->>Src: 200
Src->>Hook: evt_9 order.shipped again
Hook->>DB: insert evt_9
DB-->>Hook: unique violation
Hook-->>Src: 200 without touching the order
Note over Src,Hook: the newer event arrives first
Src->>Hook: evt_11 order.delivered, version 11
Hook->>DB: set status delivered, store version 11
Src->>Hook: evt_10 order.shipped, version 10, retried
Hook->>DB: compare version 10 against the stored 11
DB-->>Hook: stale, the write is refused
Hook-->>Src: 200Cover everything below the ack and ask what you just lost
Take the finished receiver diagram and cover every arrow after the 200. What is left is the only part the sender will ever know about, and the only part that gets a retry if it fails.
Now ask: if the machine died right at that line, what would be lost? If the answer is "nothing — the event is stored and the worker will pick it up", the design holds. If the answer includes any work you actually needed to do, then that work has no retry at all: the sender has already been told the event was delivered and will never send it again, and nothing on your side is holding it either. That is not a slow path or a degraded path. It is a silently dropped event, and the diagram is the only place it is visible before a customer finds it for you.
FAQ
Should the webhook handler do the work, or just enqueue it?
Enqueue, in almost every case. Verify, store, acknowledge, process later. The reason is not performance, it is control: once the work is inside the request, the sender's timeout becomes your deadline and their retry policy becomes your load profile, and you can change neither. The exception is a handler that is genuinely one idempotent write finishing in a few milliseconds — that is fine, and it stays fine right up until someone adds a notification to it. A useful rule for the diagram: if there is more than one arrow between receiving the request and returning the 200, ask why.
How do I make a webhook handler idempotent?
Store the sender's event id in a column with a unique index, and insert it in the same transaction as the work. The insert failing is your duplicate check — a separate
SELECTfirst is a race, and under retries that race is not rare. If the sender does not provide an id, hash the raw body and use that, accepting that two genuinely identical events become one. Then answer the duplicate with a 200: it means the sender already succeeded and lost your response, so any error status just guarantees more retries. On the diagram this is onealtblock with the collision branch returning success, which looks wrong until you remember who the 200 is for.How many retries should the diagram show, and with what backoff?
Five attempts over roughly seven hours — 10s, 1m, 10m, 1h, 6h — is a common and defensible shape, and it is what the examples above use. Exponential backoff with jitter matters more than the exact numbers: without jitter, everything that failed during an outage retries in the same instant when it ends, and your recovery is a second outage. What the diagram must show is where retrying stops, because that is the decision, not the schedule. Also draw which status codes retry at all: 5xx, timeouts and 429 do; other 4xx do not, since the payload will be just as invalid an hour later.
Do I need signature verification if the webhook URL is secret?
Yes. A URL is not a secret in any useful sense — it travels through proxies, CDN logs, error trackers, browser history, and screenshots in support tickets, and none of those treat it as sensitive. More importantly, a secret URL proves at most that someone knows the URL; a signature proves this specific body came from the holder of the shared secret and was not modified in transit. On the diagram, verification is the first arrow inside your endpoint, computed over the raw bytes before any JSON parsing, since re-serializing changes whitespace and key order and breaks the comparison. Drawing it first is also how you catch body-parsing middleware that has quietly consumed the raw stream.
Webhooks arrive out of order — how should the diagram handle that?
Draw a comparison, not a queue. Ordering cannot be guaranteed across independent HTTP requests, and building a reordering buffer means blocking on an event that may never arrive. Instead, have every event carry a version or a source timestamp, and make the handler refuse any write that is not newer than what is stored — one
altblock, shown in the third example above. Where this is not enough is when you need the transitions rather than the final value; there, treat the webhook as a notification and fetch the current state from the sender's API, which is a different diagram with one extra arrow and no ordering problem at all.Is it free?
Yes. Anonymous users get 20 generations per day, logged-in users 500. Opening any example on this page in the editor costs nothing — that path does not call the model at all.