Retry Logic
NotifyKit automatically retries failed webhook deliveries. This page explains how retries work and how to handle failures.
Automatic Retries
When a webhook delivery fails, NotifyKit re-queues it with exponential backoff:
| Attempt | Delay before retry |
|---|---|
| 1st | Immediate |
| 2nd | ~2 seconds |
| 3rd | ~4 seconds |
After 3 failed attempts, the job moves to failed status and retries stop.
When NotifyKit Retries
| Condition | Retried? |
|---|---|
| 5xx response from your endpoint | Yes |
| Network error or connection refused | Yes |
| Request timeout | Yes |
| 4xx response (400, 401, 403, 404, etc.) | No |
| 2xx response | No (success) |
A 4xx response means your endpoint rejected the request. Retrying the same payload to the same endpoint would produce the same result. Fix the endpoint configuration or payload instead.
Checking Job Status After Delivery
After sending a webhook, poll the job endpoint to confirm delivery:
const job = await client.sendWebhook({
url: "https://your-app.com/webhook",
payload: { event: "order.placed", orderId: "123" },
idempotencyKey: "order-123-webhook",
});
// Check status after a short delay
setTimeout(async () => {
const status = await client.getJob(job.jobId);
switch (status.status) {
case "completed":
console.log(`Delivered after ${status.attempts} attempt(s)`);
break;
case "failed":
console.error(
`Failed after ${status.attempts} attempt(s): ${status.errorMessage}`,
);
break;
case "processing":
console.log("Still in progress...");
break;
case "pending":
console.log("Waiting in queue...");
break;
}
}, 5000);
Listing Failed Jobs
To audit all failed deliveries:
const { data, pagination } = await client.listJobs({
status: "failed",
type: "webhook",
limit: 50,
});
data.forEach((job) => {
console.log(`Job ${job.id} failed: ${job.errorMessage}`);
console.log(`Attempted ${job.attempts} time(s), last at ${job.completedAt}`);
});
Or via the REST API:
curl "https://api.notifykit.dev/api/v1/notifications/jobs?status=failed&type=webhook" \
-H "X-API-Key: nh_your_key_here"
Manual Retry
You can manually re-queue any job with failed status:
try {
const result = await client.retryJob("job_xyz789");
console.log(result.message); // "Job has been re-queued for processing"
console.log(result.status); // "pending"
} catch (error) {
if (error instanceof NotifyKitError && error.isStatus(404)) {
console.error("Job not found or not in failed status");
}
}
REST API:
curl -X POST https://api.notifykit.dev/api/v1/notifications/jobs/job_xyz789/retry \
-H "X-API-Key: nh_your_key_here"
Only jobs with failed status can be retried. Jobs in pending, processing, or completed status cannot.
Preventing Duplicate Delivery
Use idempotencyKey to ensure a notification is only sent once, even if your code retries the send request due to a network error:
await client.sendWebhook({
url: "https://your-app.com/webhook",
payload: { orderId: "order_123" },
idempotencyKey: "order-123-fulfillment-webhook", // Unique per logical event
});
If you call sendWebhook again with the same idempotencyKey, the API returns 409 Conflict and does not create a new job. The first job (and its delivery) are not affected.
Good idempotency key patterns:
order-{orderId}-confirmation
user-{userId}-welcome-email
payment-{paymentId}-receipt
invoice-{invoiceId}-reminder
Designing Your Webhook Endpoint for Reliability
For webhook endpoints that receive NotifyKit deliveries:
- Return 2xx quickly — Process the payload asynchronously. A slow endpoint may time out and trigger a retry.
- Handle duplicates — Even with idempotency keys, design your endpoint to be idempotent. Store processed event IDs and skip duplicates.
- Return 200 for known bad payloads — If the payload is malformed but expected, return 200 to prevent retries. Log the issue separately.
- Use HTTPS — Ensures the payload is encrypted in transit.