Skip to main content

Retry Policy

When a webhook delivery fails, Invoice Maker automatically retries with exponential backoff to give your server time to recover.

What counts as a failure

A delivery is considered failed if:

  • Your endpoint returns a non-2xx HTTP status code (e.g., 400, 500)
  • Your endpoint doesn't respond within 10 seconds
  • The connection can't be established (DNS failure, connection refused, TLS error)

A delivery is considered successful if your endpoint returns any 2xx status code (200, 201, 202, 204, etc.).

Retry schedule

Failed deliveries are retried up to 5 times with exponential backoff:

AttemptDelay after failureCumulative time
1st retry1 minute1 minute
2nd retry5 minutes6 minutes
3rd retry30 minutes36 minutes
4th retry2 hours~2.5 hours
5th retry24 hours~26.5 hours

After 5 failed retries, the delivery is marked as permanently failed and no further attempts are made.

Same event, different delivery IDs

Each retry attempt gets a new X-Webhook-ID header, but the event id in the payload body and X-Webhook-Event-ID header stay the same. Use the event ID for deduplication.

Auto-disable

If an endpoint accumulates 100 consecutive failed deliveries (across any number of events), it is automatically disabled. This prevents wasting resources on endpoints that are consistently unreachable.

When an endpoint is auto-disabled:

  • No new events are sent to it
  • Events that occur while disabled are not queued — they are dropped
  • You'll see a "Disabled" badge in your webhook settings
  • Re-enable it manually from the dashboard after fixing the issue

Delivery logs

Each webhook endpoint shows a log of recent delivery attempts. For each attempt you can see:

FieldDescription
StatusSuccess or Failed
HTTP codeThe response status code from your server
DurationHow long the request took (ms)
TimestampWhen the attempt was made
AttemptWhich attempt number (1 = original, 2-6 = retries)

Best practices

Respond quickly

Return a 200 OK as soon as you've received and validated the event. Do heavy processing asynchronously (e.g., in a background job queue).

app.post('/webhooks', async (req, res) => {
// Verify signature
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature');
}

// Acknowledge immediately
res.status(200).send('OK');

// Process asynchronously
await queue.add('process-webhook', {
event: req.body
});
});

Make handlers idempotent

Since the same event may be delivered multiple times (due to retries or network issues), your handler should produce the same result whether it runs once or multiple times.

async function handleInvoicePaid(event) {
const eventId = event.id;

// Check if already processed
const existing = await db.query(
'SELECT id FROM processed_events WHERE event_id = $1',
[eventId]
);

if (existing.rows.length > 0) {
console.log(`Event ${eventId} already processed, skipping`);
return;
}

// Process the event
await updatePaymentStatus(event.data.invoice.id, 'paid');

// Record that we processed it
await db.query(
'INSERT INTO processed_events (event_id, processed_at) VALUES ($1, NOW())',
[eventId]
);
}

Return meaningful status codes

Your responseWhat happens
200 - 299Delivery marked as successful
400 - 499Delivery marked as failed, will be retried
500 - 599Delivery marked as failed, will be retried
Timeout (>10s)Delivery marked as failed, will be retried
Don't return 4xx for invalid events

If you receive an event type you don't handle, return 200 anyway. Returning 400 triggers unnecessary retries.

app.post('/webhooks', (req, res) => {
switch (req.body.type) {
case 'invoice.paid':
handleInvoicePaid(req.body);
break;
default:
// Unknown event type — acknowledge it, don't reject it
console.log(`Unhandled event type: ${req.body.type}`);
}

res.status(200).send('OK');
});

Monitor your endpoint

  • Check delivery logs regularly for failed deliveries
  • Set up alerting if your webhook endpoint goes down
  • Watch for the auto-disable threshold (100 consecutive failures)

Plan limits

FeatureBasicPro
Retry attempts55
Delivery timeout10s10s
Deliveries per hour1005,000
Auto-disable threshold100 failures100 failures