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:

AttemptDelay since last attempt
1immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours
724 hours
824 hours
924 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 200299.
  • 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":

server/scripts/check-disabled-webhooks.ts
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:

server/scripts/reenable-webhook.ts
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.

server/scripts/check-webhook-health.ts
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:

FieldMeaning
attemptsHow many times we’ve tried, including the current one.
next_attempt_atWhen we’ll try again. null means terminal (delivered or exhausted).
delivered_atSuccessful delivery timestamp. null until a 2xx.
last_status_codeStatus of the most recent attempt.
last_response_bodyBody 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.