Retries and delivery

How Certivant retries failed webhook deliveries — exact backoff schedule, 10-second timeout rule, exhausted-event recovery, and a fast handler pattern to stay under budget.

Certivant is designed to deliver every event at least once. If your endpoint is briefly unavailable — a deploy, a database hiccup, a 30-second outage — retries make sure the event still gets through. This page explains the exact behaviour so you can plan accordingly.

The retry policy at a glance

Value
Max delivery attempts5 (including the first try)
Retry schedule5s, 30s, 5min, 1h, 24h, each with ±10% jitter
Retry-AfterHonoured on any failed attempt (seconds or an HTTP date, capped at 24 h): the next attempt is never earlier than you asked
Per-attempt timeout10 seconds
Success criterionHTTP status 200–299
Failure criteriaStatus >= 300, timeout, connection error, DNS failure
Total delivery window~25 hours from first attempt

After the fifth failed attempt, the event is marked exhausted. It will not be retried automatically. An email is sent to the organization owner alerting them that webhooks are failing.

The timeline

gantt
  title Webhook delivery attempts
  dateFormat X
  axisFormat %s
  section Attempts
  Attempt 1 (immediate) :a1, 0, 1s
  Attempt 2 (+5s)       :a2, 5, 1s
  Attempt 3 (+30s)      :a3, 35, 1s
  Attempt 4 (+5min)     :a4, 335, 1s
  Attempt 5 (+1h)       :a5, 3935, 1s
  Attempt 6 (+24h)      :a6, 90335, 1s

The clock starts when the event is generated. If your endpoint is healthy on attempt 1, you never see retries. If it's down for the full ~25 hours, the event exhausts.

A short transient outage (a deploy, a database failover, anything under a minute) is fully absorbed by the schedule — your endpoint will see the next attempt about 5 seconds later and succeed.

What counts as success

Certivant treats your endpoint as healthy when:

  • The HTTP status is 2xx (200, 201, 204, etc.).
  • The response arrives within 10 seconds of the request being sent.

Anything else is a delivery failure:

  • 3xx redirects — Certivant does not follow redirects. Point the webhook URL at the final destination.
  • 4xx (including 401 and 404) — Certivant retries anyway, in case the problem is transient (cold-start auth check, route-not-yet-deployed). Most integrations want to investigate any 4xx — it usually indicates a misconfiguration on your side.
  • 5xx — server error. Standard retry.
  • Connection refused, DNS failure, TLS handshake error — same retry behaviour.
  • No response within 10 seconds — Certivant aborts the request and counts it as a failure. Retry continues.

Designing a fast handler

You have a 10-second budget. Do the bare minimum synchronously:

app.post("/webhooks/certivant", async (c) => {
  const rawBody = await c.req.text();

  // 1. Verify signature
  if (!verifyCertivantSignature(rawBody, c.req.header("X-Certivant-Signature-V2"), SECRET)) {
    return c.json({ error: "Invalid signature" }, 401);
  }

  // 2. Persist to queue (single DB write, fast)
  const eventId = c.req.header("X-Certivant-Event-Id")!;
  await db.webhookEventQueue.create({
    data: { id: eventId, payload: rawBody },
  });

  // 3. Acknowledge immediately
  return c.json({ received: true }, 200);
});

// Process the queue asynchronously
async function workerLoop() {
  while (true) {
    const event = await db.webhookEventQueue.findFirst({ where: { processedAt: null } });
    if (!event) { await sleep(1000); continue; }
    try {
      await handleEvent(JSON.parse(event.payload));
      await db.webhookEventQueue.update({ where: { id: event.id }, data: { processedAt: new Date() } });
    } catch (err) {
      // Bounded retry on your side; surface after N attempts
    }
  }
}

This pattern decouples acknowledgement (must be fast) from business logic (can be slow / retried independently). Your downstream services (sending welcome emails, provisioning accounts, etc.) can fail without making Certivant retry.

When your endpoint is genuinely down

If you're going to be down for an extended window (maintenance, migration), the retries will still cover you for ~25 hours. Beyond that:

  • Events that exhaust are still queryable via GET /api/v1/kyc/requests/:id — the current state is always available on the request itself. Walk the requests created during your downtime and sync state manually.
  • The organization owner gets an email when an event exhausts. The email includes the request ID so it's easy to look up.
  • Webhook delivery status (delivered / failed / exhausted), the attempt count, the last response code and the next scheduled retry are visible in the dashboard at KYC → Settings → Webhooks → Delivery log, where each failed or exhausted event has a Retry now button (and a bulk retry).

Re-delivering an exhausted event

From the delivery log, Retry now re-sends a failed or exhausted event immediately, signed with the current secret, and restarts its attempt counter. There is no public (API-key) endpoint to re-trigger a single event. Other options:

  1. Pull state via the API. GET /api/v1/kyc/requests/:id always returns the current state including the final decision. This is the right answer for catch-up after downtime.
  2. Ask support to replay if you're missing a large batch. Include the request IDs or the time window — support can re-emit events for you.

Rate limit interaction

Webhook delivery does not count against your API key rate limit — those are separate systems. You can be receiving hundreds of webhooks per minute while also making hundreds of API calls per minute on the same key, and neither budget affects the other.

Idempotency revisited

Because retries exist, the same logical event will sometimes arrive twice. See Events → Idempotency for the recommended deduplication pattern using X-KYC-Event-Id.

Monitoring suggestions

A healthy webhook integration has:

  • Median response time on the webhook endpoint under 500ms. If it climbs, you risk hitting the 10-second timeout under load.
  • Zero 4xx and 5xx from the webhook URL in normal operation. Alert on either.
  • An idempotency log table you can query to confirm every webhook was processed at least once.

If you're seeing exhausted-event emails, the most common root causes are: (a) the webhook URL is wrong (typo in the dashboard), (b) your endpoint requires auth that Certivant doesn't provide (the URL must accept unauthenticated POSTs — the signature header is the auth), (c) your endpoint is consistently slower than 10 seconds. Investigate each.