This page explains exactly what DevStride does with an event after it happens, so you can build a receiver that copes with slow periods, outages and duplicates.
When a change happens, DevStride queues one delivery of its event for every enabled endpoint subscribed to that event type. Each delivery moves through these statuses, all visible in the delivery log:
| Status | Means | What happens next |
|---|---|---|
pending | Queued, or waiting for its next retry. | DevStride sends it when its turn comes. |
succeeded | Your endpoint answered 2xx. | Nothing: it is done. |
failed | All 10 attempts failed. | Nothing, until you replay it. |
dead | Parked because you disabled the endpoint. | Nothing, until you enable the endpoint and replay it. |
Endpoints that are disabled when a change happens get no delivery for it at all.
A delivery succeeds when your endpoint answers with any 2xx status (200, 201, 202, 204 …) within the time limits below. The response body does not matter, but DevStride reads at most 64 KB of it: a response larger than 64 KB counts as a failure, even with a 2xx status. Answer with an empty body or a short one.
Everything else is a failed attempt, and is retried:
| Outcome | Example |
|---|---|
Any status that is not 2xx | 400, 401, 404, 410, 413, 429, 500, 503 |
| A redirect DevStride does not follow | 301, 302 or 303 (following them would turn the POST into a GET) |
| No answer in time | Nothing within 10 seconds of connecting, or the whole request (DNS lookup, connection, any redirects and the response) taking more than 15 seconds |
| A connection or TLS failure | Connection refused or reset; a certificate that is expired, self-signed or for another name |
| A refused address | The host now resolves to a private, loopback, link-local or cloud-metadata address |
DevStride does not treat any status specially: a 400 or 410 is retried exactly like a 500. So never answer with an error for an event you simply choose not to process; answer 2xx and ignore it.
DevStride follows 307 and 308 redirects, which keep the request a POST with the same body, for up to 3 hops. Every hop must be https:// and is checked against the same address rules as your endpoint URL; the 15-second limit covers all of them. Prefer registering the final URL.
After a failed attempt, DevStride waits before trying again. Each wait is a random time between zero and the limit for that attempt (the "jitter" stops thousands of retries hitting a recovering server at the same moment):
| After failed attempt | Next attempt within | Latest time since the first attempt |
|---|---|---|
| 1 | 1 minute | 1 minute |
| 2 | 5 minutes | 6 minutes |
| 3 | 15 minutes | 21 minutes |
| 4 | 30 minutes | 51 minutes |
| 5 | 1 hour | 1 hour 51 minutes |
| 6 | 2 hours | 3 hours 51 minutes |
| 7 | 4 hours | 7 hours 51 minutes |
| 8 | 8 hours | 15 hours 51 minutes |
| 9 | 8 hours | 23 hours 51 minutes |
| 10 | (none) | The delivery becomes failed |
So a delivery gets 10 attempts over at most about 24 hours, and on average about half that. Because the waits are random, a retry can also come much sooner than its limit. After the tenth failure the delivery is failed, and it stays in the delivery log, ready to replay, until at least 30 days after it was queued.
The Next attempt column of the delivery log shows when a pending delivery is due.
Each endpoint has its own queue, and deliveries to one endpoint go out one at a time, in the order they were queued. DevStride does not send the next one until the current one has succeeded or used up its attempts.
That has two consequences worth designing for:
2xx again, the backlog drains in order. If one specific event makes your handler fail (a bug, a payload you cannot parse), that one event blocks the queue: answer 2xx, store the event and deal with it separately.What ordering does not promise:
occurredAt, or read the current state from the API rather than trusting the last event you received.DevStride delivers every event at least once, which means occasionally more than once: for example, when your endpoint processed a request but the connection dropped before DevStride saw the answer, or when a replay resends a delivery your endpoint actually handled. Duplicates carry the same event id, in the body and in X-DevStride-Event-Id.
Make your handler idempotent: record each event id you process (a unique key in a table works well) and skip one you have seen. Keep ids for at least 30 days, the period over which a delivery can still be replayed. See Deduplicate by event id.
DevStride watches each endpoint's health for you:
On each of those two changes, and only then, DevStride emails every active member who holds the Manage Webhooks permission (Owners included):
| When | Subject | What it says |
|---|---|---|
| It starts failing | Webhook endpoint failing: hooks.example.com | The full URL, since when (in UTC), and the last HTTP status or the reason no response arrived. Deliveries are still queued and retried for about a day; nothing is lost yet. |
| It recovers | Webhook endpoint recovered: hooks.example.com | It is accepting webhooks again, since when it had been failing, and that the queued deliveries are going out in order. |
Each email has an Open Webhooks settings button. It names the host and status only: never a payload or a secret. You get one email per change, not one per failed attempt.
While an enabled endpoint is failing, the Webhooks settings page shows a red banner, "hooks.example.com has been failing since 2 hours ago; last response 503.", with a Replay failed button, and the endpoint's row shows Failing since.
Every delivery, with the exact payload that was sent and the last failed response, stays in the delivery log for at least 30 days after it was queued. After that, finished deliveries (succeeded, failed and dead) are removed. A pending delivery is never removed. A delivery can be replayed only while it is still in the log.
Deleting an endpoint deletes its delivery log with it. Erasing a requester or redacting a comment also removes the stored deliveries that carried their words; see Privacy.
Verifying Signatures
How DevStride signs every webhook with HMAC-SHA256, the exact string it signs, the headers it sends, replay protection, secret rotation and raw-body pitfalls, with tested verification code in Node.js, Python, Go, Ruby, PHP and C#.
Managing Endpoints
Edit, enable, disable, test and delete webhook endpoints, rotate signing secrets with a 24-hour overlap, read the delivery log, and replay one or all failed deliveries, from Settings → Webhooks, the REST API or the DevStride MCP server.