Error Handling
Webhook delivery can fail for many reasons — your server is down, network issues, or invalid responses. Here's how HookSniff handles errors and how to build resilient webhook handlers.
API Error Codes
The HookSniff API returns standard HTTP status codes. All SDKs throw typed exceptions for each:
| Code | Meaning | SDK Behavior |
|---|---|---|
| 400 | Bad Request — invalid parameters | Throws validation error with details |
| 401 | Unauthorized — invalid API key | Throws authentication error |
| 403 | Forbidden — insufficient permissions | Throws permission error |
| 404 | Not Found — resource doesn't exist | Throws not found error |
| 409 | Conflict — duplicate request (idempotency) | Returns existing resource |
| 422 | Unprocessable — validation failed | Throws validation error with field details |
| 429 | Rate Limited — too many requests | Auto-retries after Retry-After header |
| 500 | Server Error — HookSniff internal error | Auto-retries up to 2 times with backoff |
SDK Error Handling Examples
Node.js
import { HookSniff, HttpError, ValidationError } from 'hooksniff';
const hs = new HookSniff({ apiKey: 'hr_live_xxx' });
try {
const endpoint = await hs.endpoint.create({
url: 'https://myapp.com/webhook',
});
} catch (err) {
if (err instanceof HttpError) {
console.error(`HTTP ${err.statusCode}: ${err.message}`);
if (err.statusCode === 429) {
const retryAfter = err.headers['retry-after'];
console.log(`Retry after ${retryAfter} seconds`);
}
} else if (err instanceof ValidationError) {
console.error('Validation failed:', err.errors);
} else {
throw err; // Unexpected error
}
}Python
from hooksniff import HookSniff
from hooksniff.exceptions import HttpError, ValidationError
hs = HookSniff(api_key="hr_live_xxx")
try:
endpoint = hs.endpoint.create(url="https://myapp.com/webhook")
except HttpError as e:
print(f"HTTP {e.status_code}: {e.message}")
if e.status_code == 429:
retry_after = e.headers.get("retry-after")
print(f"Retry after {retry_after} seconds")
except ValidationError as e:
print(f"Validation failed: {e.errors}")
except Exception as e:
raise # Unexpected errorGo
endpoint, err := hs.Endpoint.Create(ctx, &hooksniff.EndpointIn{
Url: "https://myapp.com/webhook",
})
if err != nil {
var httpErr *hooksniff.HttpError
if errors.As(err, &httpErr) {
fmt.Printf("HTTP %d: %s\n", httpErr.StatusCode, httpErr.Message)
if httpErr.StatusCode == 429 {
retryAfter := httpErr.Headers.Get("Retry-After")
fmt.Printf("Retry after %s seconds\n", retryAfter)
}
} else {
// Unexpected error
panic(err)
}
}Webhook Delivery Errors
When HookSniff delivers webhooks to your endpoint, these are considered failures:
| Your Response | HookSniff Action |
|---|---|
| 2xx | Success — delivery complete |
| 3xx | Follow redirect (up to 3 hops) |
| 4xx (except 429) | No retry — your client error |
| 429 | Retry after Retry-After header |
| 5xx | Retry with exponential backoff |
| Timeout (30s) | Retry |
| Connection refused | Retry |
| DNS failure | Retry |
Webhook Handler Best Practices
Return 200 immediately
Don't process the webhook synchronously. Acknowledge receipt, then handle the event in a background job.
Verify signature first
Always verify the webhook signature before parsing the body. Reject invalid signatures with 401.
Use idempotency keys
The webhook-id header is unique per delivery. Use it to deduplicate if HookSniff retries.
Log everything
Log the webhook-id, timestamp, and event type. Essential for debugging delivery issues.
Handle retries gracefully
Your handler may be called multiple times for the same event. Design for idempotency.
Respond within 30 seconds
HookSniff times out after 30s. If you need more time, queue the event and respond immediately.
Example: Resilient Webhook Handler
import express from 'express';
import { Webhook } from 'hooksniff';
const app = express();
const wh = new Webhook(process.env.WEBHOOK_SECRET!);
// Use raw body for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const startTime = Date.now();
// 1. Verify signature FIRST
let payload;
try {
payload = wh.verify(req.body, {
'webhook-id': req.headers['webhook-id']!,
'webhook-timestamp': req.headers['webhook-timestamp']!,
'webhook-signature': req.headers['webhook-signature']!,
});
} catch (err) {
console.error('Signature verification failed:', err);
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Acknowledge immediately
res.status(200).json({ received: true });
// 3. Process asynchronously
try {
await processWebhook(payload);
console.log(`Processed ${payload.event} in ${Date.now() - startTime}ms`);
} catch (err) {
console.error(`Failed to process ${payload.event}:`, err);
// Don't send error response — already acknowledged
// Log for debugging, set up alerting for repeated failures
}
});
async function processWebhook(payload: any) {
const { event, data } = payload;
// Idempotency: check if already processed
const existing = await db.webhookEvents.findOne({ webhook_id: payload.webhook_id });
if (existing) {
console.log(`Duplicate: ${payload.webhook_id}`);
return;
}
// Process event
switch (event) {
case 'order.created':
await handleOrderCreated(data);
break;
case 'payment.completed':
await handlePaymentCompleted(data);
break;
default:
console.log(`Unhandled event: ${event}`);
}
// Mark as processed
await db.webhookEvents.insert({
webhook_id: payload.webhook_id,
event,
processed_at: new Date(),
});
}