When Topiic retries a failed webhook delivery, when it gives up, and how to recover.
A delivery succeeds when your endpoint returns any 2xx status within 10 seconds. Anything else — a 4xx, a 5xx, a timeout, a TLS error — is a failure, and Topiic schedules the next attempt.
Retry schedule
Section titled “Retry schedule”| Attempt | Delay after previous |
|---|---|
| 1 | 0 (immediate) |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7 | 24 hours |
| 8 | 24 hours |
After attempt 8 fails, the delivery is marked Abandoned. The event still exists in Topiic’s history — you can replay it manually any time after fixing the receiver — but no further automatic attempts will happen.
Total automatic-retry window: ~55 hours.
What counts as a 2xx
Section titled “What counts as a 2xx”200 OK— what you should return.201,202,204, etc. — all treated as success.- Empty body, JSON body, anything — Topiic doesn’t read the body for routing.
What counts as a failure
Section titled “What counts as a failure”- Any non-2xx HTTP response (
3xxincluded — we don’t follow redirects). - TCP/TLS errors (DNS failure, connection refused, certificate invalid).
- Response not received within 10 seconds.
- Connection closed mid-response.
Idempotency
Section titled “Idempotency”Because the same event may be delivered up to 8 times, every webhook handler must be idempotent. The header to dedupe on is Topiic-Event-Id (mirrored as Topiic-Idempotency-Key).
The simplest pattern:
CREATE TABLE processed_webhooks ( event_id UUID PRIMARY KEY, processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW());In your handler:
const begin = await db.beginTransaction();try { const inserted = await db.query( 'INSERT INTO processed_webhooks (event_id) VALUES ($1) ON CONFLICT DO NOTHING', [eventId] ); if (inserted.rowCount === 0) { // Already processed - silent ack. await begin.commit(); return res.status(200).end(); } await applyEvent(event); // any side-effect, same transaction await begin.commit(); res.status(200).end();} catch (e) { await begin.rollback(); res.status(500).end(); // let Topiic retry}The insert and the side-effect happen in the same transaction, so a crash between them rolls everything back and the next retry gets a clean shot.
What to do during an outage
Section titled “What to do during an outage”If your webhook receiver is down for an hour, the first attempt fails, the 30-second retry fails, the 2-minute retry fails — and the 10-minute retry probably catches you on your way back up. You don’t have to do anything special; the backoff schedule absorbs short outages.
For longer outages (>24h on an event), you’ll see the delivery transition to Abandoned in the portal. After bringing your receiver back up:
- Filter the delivery log to Abandoned status.
- Click Replay on each one (or call the replay API in a loop).
- Each replay creates a fresh delivery chain — if it fails again, the backoff schedule starts over.
Manual replay
Section titled “Manual replay”From the portal: Settings → API keys → <key> → Deliveries shows the delivery history with a per-row Replay button.
From the API:
POST /api/Webhooks/deliveries/{deliveryId}/replay?merchantId={merchantId}Authorization: Bearer tpk_…Returns 202 Accepted and enqueues a fresh Pending delivery for the same underlying event. The replay uses the current webhook URL configured on the API key, not the URL that was in effect when the original event was produced. So if you changed your URL, replays will go to the new one.
Listing deliveries programmatically
Section titled “Listing deliveries programmatically”GET /api/Webhooks/deliveries?merchantId={merchantId}&apiKeyId={apiKeyId}&limit=100Authorization: Bearer tpk_…Returns the most-recent deliveries with attempt number, status, response code, and next-attempt time. Use this to build a “missed any?” reconciliation job — compare the events you’ve processed against the events Topiic delivered, and replay anything missing.
Putting bounds on retry storms
Section titled “Putting bounds on retry storms”If you depend on a third-party that has its own rate limits, treat each webhook handler as the unit of work and queue any onward calls. Don’t make Topiic’s POST block on a slow downstream — return 200, ack the event, and let your own worker handle the downstream call with your own retries.