From harness-claude
Implements reliable webhook delivery in TypeScript with HMAC-SHA256 signature verification, exponential backoff retries with jitter, queuing, and failure handling for push notifications to external services.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Implement reliable webhook delivery with retry backoff, signature verification, and queuing.
Implements secure webhook systems for event-driven integrations with signature verification, retry logic, queues, and delivery guarantees. Use for third-party services, notifications, and real-time sync.
Designs reliable webhook systems for APIs covering registration, payload schema/versioning, at-least-once delivery, retries, idempotency, and fan-out. Use for new platforms, audits, or developer docs.
Guides webhook design, inbound handling with HMAC verification and idempotency, outbound delivery with retries, circuit breakers, and dead letter queues.
Share bugs, ideas, or general feedback.
Implement reliable webhook delivery with retry backoff, signature verification, and queuing.
Webhook sender with retry and signature:
import crypto from 'crypto';
interface WebhookEndpoint {
id: string;
url: string;
secret: string;
events: string[]; // subscribed event types
enabled: boolean;
}
interface WebhookDelivery {
id: string;
endpointId: string;
eventType: string;
payload: object;
status: 'pending' | 'delivered' | 'failed';
attempts: number;
nextRetryAt?: Date;
}
class WebhookSender {
// Sign the payload with HMAC-SHA256
private sign(payload: string, secret: string): string {
return 'sha256=' + crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
}
async deliver(endpoint: WebhookEndpoint, delivery: WebhookDelivery): Promise<boolean> {
const body = JSON.stringify({
id: delivery.id,
type: delivery.eventType,
created: new Date().toISOString(),
data: delivery.payload,
});
const signature = this.sign(body, endpoint.secret);
try {
const response = await fetch(endpoint.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Id': delivery.id,
'X-Webhook-Timestamp': Date.now().toString(),
'User-Agent': 'MyPlatform-Webhooks/1.0',
},
body,
signal: AbortSignal.timeout(10_000), // 10 second timeout
});
// 2xx = success, 3xx redirect = fail, 4xx = fail (don't retry client errors), 5xx = retry
if (response.ok) {
await this.markDelivered(delivery.id);
return true;
}
if (response.status >= 400 && response.status < 500) {
// Client error — endpoint is broken, don't retry
await this.markFailed(delivery.id, `HTTP ${response.status}`);
return false;
}
throw new Error(`HTTP ${response.status}`);
} catch (err) {
await this.scheduleRetry(delivery, (err as Error).message);
return false;
}
}
// Exponential backoff with jitter
private async scheduleRetry(delivery: WebhookDelivery, error: string): Promise<void> {
const MAX_ATTEMPTS = 10;
if (delivery.attempts >= MAX_ATTEMPTS) {
await this.markFailed(delivery.id, `Max retries exceeded: ${error}`);
return;
}
// 5s, 25s, 125s, 625s, ... up to ~17 hours
const baseDelay = 5_000 * Math.pow(5, delivery.attempts);
const jitter = Math.random() * 0.2 * baseDelay; // ±20% jitter
const delay = Math.min(baseDelay + jitter, 17 * 60 * 60 * 1000);
const nextRetryAt = new Date(Date.now() + delay);
await this.db.webhookDelivery.update({
where: { id: delivery.id },
data: { attempts: { increment: 1 }, nextRetryAt, lastError: error },
});
}
private async markDelivered(id: string): Promise<void> {
await this.db.webhookDelivery.update({
where: { id },
data: { status: 'delivered', deliveredAt: new Date() },
});
}
private async markFailed(id: string, error: string): Promise<void> {
await this.db.webhookDelivery.update({
where: { id },
data: { status: 'failed', lastError: error },
});
}
}
Webhook receiver — verify signature:
import express from 'express';
import crypto from 'crypto';
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
// Use timingSafeEqual to prevent timing attacks
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
} catch {
return false; // different lengths
}
}
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['stripe-signature'] as string;
const payload = req.body.toString('utf8'); // must be raw body, not parsed JSON
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
res.status(401).json({ error: 'Invalid signature' });
return;
}
const event = JSON.parse(payload);
// Respond quickly — offload processing to queue
res.status(200).json({ received: true });
// Process async (don't block the response)
processWebhookAsync(event).catch(console.error);
});
Respond fast, process async: Webhook senders time out (often 10-30s). Respond 200 OK immediately and enqueue the event for processing. Never do heavy work in the webhook handler.
Idempotency: Webhooks can be retried. The receiver must be idempotent — use the event id field to deduplicate (see events-idempotency skill).
Anti-patterns:
Webhook database schema:
CREATE TABLE webhook_endpoints (
id UUID PRIMARY KEY,
url TEXT NOT NULL,
secret TEXT NOT NULL, -- store encrypted
events TEXT[] NOT NULL,
enabled BOOLEAN DEFAULT TRUE
);
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY,
endpoint_id UUID REFERENCES webhook_endpoints(id),
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
next_retry_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
last_error TEXT
);
microservices.io/patterns/communication-style/messaging.html