Templates
Types
EN

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.

Published on ·9 min read
webhooksapisequence-diagramtemplate

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 200 is 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 OK
A minimal webhook flow diagram where the endpoint does the work first and returns 200 afterwards.

How 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
    end
A webhook receiver sequence diagram: raw-body signature verification, a unique event id insert for deduplication, a 200 acknowledgement, and asynchronous processing by a worker.

2 · 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 note
A state diagram of one webhook delivery: pending, delivering, delivered, failed with exponential backoff, dropped on a 4xx, dead-lettered after five attempts, and manually replayed.

3 · 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: 200
A webhook sequence diagram showing a duplicate delivery caught by a unique event id, and an out-of-order retry rejected by a version comparison.

Cover 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

Continue reading

Try text2diagram now

Open the tool
← Back to all tutorials