The shape of a delivery
Webhooks are the general-purpose half of our integration options: a subscription is a URL, a secret, and a list of events you want. When one of those events happens — a ticket created, a status changed, a reply posted, an article published — a delivery row is written and queued, and a worker sweeps the queue about once a minute and posts it.
Two things follow from that design, and both explain reports you will otherwise find confusing.
First, queuing and sending are separate. Recording the event never waits on your endpoint, and a webhook failure can never break the thing that caused it — a ticket still gets created when your server is down. Second, because the queue is swept on a timer, a delivery that misses its fast path arrives on the sweep rather than instantly. Delayed is the normal failure mode here; lost is rare.
Six attempts, then failed
The retry schedule is fixed and worth committing to memory: about thirty seconds, then two minutes, then ten minutes, then one hour, then six hours. Five retries after the first attempt, six attempts in total. When the sixth fails the row is marked failed and the worker stops trying.
End to end that is a window of roughly eight hours. A deploy that takes your endpoint down for twenty minutes will be ridden out with no intervention and no data loss. An outage that runs overnight will not be, and the deliveries that expired during it are still on record — which is what the bulk retry exists for.
Success means an HTTP status in the 200s. Everything else — a 500, a 404, a redirect you meant to be helpful, a timeout, a TLS failure — counts as a failed attempt and takes another slot in the schedule. A receiver that answers 302 to a POST is one of the more common self-inflicted causes of a stuck subscription.
This is the part that sends people to support, so it is worth being precise.
The per-row Retry next to a delivery only acts on a row that is still pending. All it does is move that row's next-attempt time to now, so the next sweep picks it up instead of waiting out the backoff. Press it on a row that already succeeded, or on one that has exhausted its six attempts, and it will tell you there is nothing to retry. It is a hurry-up button, not a resurrect button.
The Retry all on the failures panel is the resurrect button, and it has the opposite scope: it acts only on rows already marked failed, and only those from the last twenty-four hours. It flips them back to pending with a fresh attempt time so the worker takes them up again.
So the two buttons partition the problem between them, and the row you are looking at determines which one can help. A failed delivery from three days ago falls outside both: too old for the bulk retry, and the wrong status for the per-row one.
One deliberate detail in the bulk path: the attempt counter is not reset. A chronically broken endpoint therefore accumulates attempts across every rescue, and sorting the log by attempt count is the fastest way to find the subscriber that has been quietly failing for a week while everything else worked.
Verifying the signature
Every delivery is signed. The body is HMAC-SHA256 with your subscription's secret and the digest travels in the X-MLC-Signature header, alongside a version header so the scheme can change later without breaking you. Compute the same digest over the raw body you received and compare.
Three implementation notes that account for most verification bugs. Sign the raw bytes, before any JSON parse and re-serialise — a round trip through most JSON libraries changes whitespace or key order and destroys the digest. Compare with a constant-time equality function rather than string equality. And be tolerant about the prefix: accept the digest whether or not it arrives with a sha256= label in front of it, comparing only the hex. That tolerance costs one line and makes your receiver robust to the label changing.
The Send test button on each subscription posts a small, clearly-marked test payload immediately rather than through the queue, with a short timeout, and reports the status code it got back. It is the right first move whenever you are not sure whether a problem is yours or ours, because it isolates reachability and signing from anything to do with events or scheduling.
Rotating a secret
Rotation generates a fresh random secret for that subscription. There is no overlap period and no dual-signing window, so the change takes effect on the next delivery — which means the correct order is to have the new secret ready in your receiver, rotate, and then deploy, or to accept a short window in which deliveries fail verification and are retried.
Because the backoff rides out eight hours, a rotation that briefly breaks verification is recoverable rather than lossy. That is not a reason to be casual about it, but it is a reason not to schedule it at two in the morning.
Why an internal URL is refused
A destination that resolves to an internal or otherwise non-public address is refused, and it is checked twice: when you save the subscription and again at send time. The second check is not redundant — it covers rows saved before the guard existed, and a hostname that resolved publicly when you saved it and points inward by the time it is used.
This cannot break a working subscription, because an endpoint has to be reachable from the outside to have ever worked in the first place. A self-hosted deployment that genuinely needs to deliver to an internal address has a configuration switch to allow it; the managed service does not.
How long the evidence lasts
Delivered rows are pruned after about thirty days and failed rows after about ninety. Failures outlive successes deliberately: they are rarer and they are what you need during a postmortem.
Ninety days is generous but it is not an archive. If a webhook feeds anything you would have to reconstruct — billing, provisioning, an audit trail — your receiver should be recording what it accepted, and you should not be relying on our delivery log as your record.
What to measure
Watch the failed count per subscription per day, not in total. One endpoint failing constantly and nine healthy ones average out to something that looks tolerable and is not.
Then watch attempts per successful delivery. Sitting near one means your endpoint answers first time. Drifting up means it is succeeding on retry — usually a slow receiver rather than a broken one, and the fix is to acknowledge fast and do the work afterwards rather than doing the work inside the request. That single change resolves most of the retry traffic teams see.