Retries & Auto-Disable
How Medblocks retries failed deliveries, when an endpoint gets auto-disabled, and how to bring it back.
If your receiver returns a non-2xx status, or times out, Medblocks retries on a back-off curve. After enough consecutive failures on one endpoint, that endpoint is auto-disabled.
The Retry Schedule
Medblocks retries each event up to 9 times. Delays between attempts:
| Attempt | Delay since last attempt |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
| 7 | 24 hours |
| 8 | 24 hours |
| 9 | 24 hours |
End-to-end, the worst case from first failure to exhaustion is about 3.6 days. That gives you enough time to be paged, deploy a fix, and let the retries themselves recover any in-flight events without redelivery.
event.attempts and event.next_attempt_at on each WebhookEventRecord reflect the current state. Inspect them with mb.webhooks.listEvents.
What Counts As A Failure
- HTTP response with a status code outside
200–299. - The connection times out (default 5 seconds from Medblocks’ side).
- TLS handshake fails or the connection is refused.
A 400 Bad Signature from your receiver counts as a failure. But if your signing secret is wrong, every event will fail. Auto-disable is the safety valve.
Auto-Disable
After the 9th attempt fails, the endpoint’s status flips to disabled. No event is emitted for this transition — auto-disable is a status change, not a notification.
Detect a disabled endpoint by reading status via mb.webhooks.retrieve(id) (or GET /webhooks/{id}), or by polling mb.webhooks.list() on a cron and alerting on any row where status === "disabled":
import { Medblocks } from "@medblocks/connect";
const mb = new Medblocks(process.env.MEDBLOCKS_API_KEY!);
for await (const ep of mb.webhooks.list({ status: "disabled" }).autoPagingIterator()) {
alerting.page("medblocks webhook endpoint auto-disabled", {
id: ep.id,
url: ep.url,
});
}Run this on a short interval (every few minutes is reasonable) so a disabled endpoint surfaces quickly. The disabled state itself is permanent until you re-enable it — there’s no race against re-disable in between polls.
Reactivating A Disabled Endpoint
Once your receiver is fixed, flip the endpoint back to active:
import { Medblocks } from "@medblocks/connect";
const mb = new Medblocks(process.env.MEDBLOCKS_API_KEY!);
await mb.webhooks.update("wh_01J9YR9N3X4VZ6P2K5RH7M3LMP", {
status: "active",
});New events delivered after this update will go through. Events that exhausted retries during the outage are not automatically redelivered. They’re terminal. Use manual redelivery to replay the specific events you need.
Reading Delivery Health
mb.webhooks.listEvents returns recent deliveries with state per event. Use it to spot endpoints that are climbing toward auto-disable before they get there.
const page = await mb.webhooks.listEvents("wh_01J9YR9N3X4VZ6P2K5RH7M3LMP", { limit: 50 });
const climbing = page.data.filter((evt) => evt.attempts > 1 && evt.delivered_at === null);
for (const evt of climbing) {
console.warn(`evt ${evt.id} on attempt ${evt.attempts}, last status ${evt.last_status_code}`);
}Fields worth looking at:
| Field | Meaning |
|---|---|
attempts | How many times we’ve tried, including the current one. |
next_attempt_at | When we’ll try again. null means terminal (delivered or exhausted). |
delivered_at | Successful delivery timestamp. null until a 2xx. |
last_status_code | Status of the most recent attempt. |
last_response_body | Body of the most recent attempt, truncated to 4 KB. |
Designing A Resilient Receiver
A few things that pay off in practice:
- Accept 2xx early. Acknowledge the delivery, then process asynchronously. A slow upstream dependency shouldn’t burn retry budget.
- Dedupe by
event.id. Retries and redeliveries reuse the same id; idempotency is the only safe contract. - Don’t return non-2xx for business-rule failures. “I already processed this” is a 200, not a 409. Returning 4xx for cases Medblocks can’t fix wastes retries.
- Poll endpoint status periodically. Run a short-interval job (
mb.webhooks.list({ status: "disabled" })) so an auto-disabled endpoint surfaces in your monitoring within minutes, not hours.
Related
- Quickstart. Wire up a working receiver.
- Redelivery. Replay specific events after fixing a bug.
- Managing Endpoints · Update. Flip
statusback toactive. - Managing Endpoints · List Recent Deliveries.
