Bunqueue Webhooks & Job Event Notifications
Receive HTTP callbacks when job events occur. Webhooks enable real-time notifications to external systems without polling.
Overview
Section titled “Overview”When you register a webhook URL, bunqueue sends HTTP POST requests to that URL whenever specified events occur. This is useful for:
- Notifying external services when jobs complete
- Triggering downstream workflows
- Logging job events to external systems
- Alerting on failures
CLI Commands
Section titled “CLI Commands”Add Webhook
Section titled “Add Webhook”# Add webhook for specific events (--events is required)bunqueue webhook add https://api.example.com/webhooks/bunqueue \ --events job.completed,job.failed
# Add webhook for a specific queuebunqueue webhook add https://api.example.com/webhooks/emails \ --events job.completed,job.failed,job.progress --queue emails
# Add webhook with a signing secretbunqueue webhook add https://api.example.com/webhooks/failures \ --events job.failed --secret my-webhook-secretList Webhooks
Section titled “List Webhooks”bunqueue webhook listOutput:
URL QUEUE EVENTShttps://api.example.com/webhooks/bunqueue * allhttps://api.example.com/webhooks/emails emails allhttps://api.example.com/webhooks/failures * job.failed,job.stalledEnable/Disable Webhook
Section titled “Enable/Disable Webhook”# Disable a webhook without removing itbunqueue webhook set-enabled wh_abc123 false
# Re-enable the webhookbunqueue webhook set-enabled wh_abc123 trueDisabling a webhook stops event delivery but preserves the configuration. This is useful for temporarily pausing webhook notifications during maintenance.
Remove Webhook
Section titled “Remove Webhook”# Remove webhook by its ID (shown in webhook list output)bunqueue webhook remove wh_abc123Event Types
Section titled “Event Types”| Event | Description | When Triggered |
|---|---|---|
job.completed | Job finished successfully | Worker completes job |
job.failed | Job failed | Worker throws error |
job.progress | Progress updated | job.updateProgress() |
job.active | Job started processing | Worker picks up job |
job.waiting | Job added to queue | After queue.add() |
job.delayed | Job scheduled for later | Job with delay added |
Webhook Payload
Section titled “Webhook Payload”Request Format
Section titled “Request Format”bunqueue sends POST requests with JSON body:
POST /webhooks/bunqueue HTTP/1.1Host: api.example.comContent-Type: application/jsonX-Webhook-Event: job.completedX-Webhook-Timestamp: 1704067200000X-Webhook-Signature: a1b2c3d4e5f6...
{ "event": "job.completed", "timestamp": 1704067200000, "jobId": "1001", "queue": "emails", "data": { "to": "user@example.com", "subject": "Welcome" }}Event-Specific Payloads
Section titled “Event-Specific Payloads”job.completed
{ "event": "job.completed", "timestamp": 1704067200000, "jobId": "1001", "queue": "emails", "data": { "to": "user@example.com", "subject": "Welcome" }}job.failed
{ "event": "job.failed", "timestamp": 1704067200000, "jobId": "1001", "queue": "emails", "data": { "to": "user@example.com", "subject": "Welcome" }, "error": "SMTP connection timeout"}job.progress
{ "event": "job.progress", "timestamp": 1704067200000, "jobId": "1001", "queue": "emails", "data": { "to": "user@example.com", "subject": "Welcome" }, "progress": 75}Signature Verification
Section titled “Signature Verification”When a webhook is registered with a --secret flag, bunqueue signs all payloads using HMAC-SHA256. Always verify signatures to ensure requests are authentic.
Setting the Secret
Section titled “Setting the Secret”Secrets are configured per-webhook when adding them via the --secret flag:
bunqueue webhook add https://api.example.com/webhooks/bunqueue \ --events job.completed,job.failed --secret your-secret-keyVerifying Signatures
Section titled “Verifying Signatures”TypeScript/JavaScript:
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature( payload: string, signature: string, secret: string): boolean { const expected = createHmac('sha256', secret) .update(payload) .digest('hex');
const expectedBuf = Buffer.from(expected); const receivedBuf = Buffer.from(signature);
if (expectedBuf.length !== receivedBuf.length) { return false; }
return timingSafeEqual(expectedBuf, receivedBuf);}
// Usage in webhook handlerapp.post('/webhooks/bunqueue', (req, res) => { const signature = req.headers['x-webhook-signature'] as string; const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, MY_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); }
// Process webhook... res.status(200).json({ received: true });});Python:
import hmacimport hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest()
return hmac.compare_digest(expected, signature)
# Usage in Flask@app.route('/webhooks/bunqueue', methods=['POST'])def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') payload = request.get_data()
if not verify_webhook_signature(payload, signature, MY_WEBHOOK_SECRET): return jsonify({'error': 'Invalid signature'}), 401
# Process webhook... return jsonify({'received': True})Go:
import ( "crypto/hmac" "crypto/sha256" "encoding/hex")
func verifyWebhookSignature(payload []byte, signature, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payload) expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))}Retry Behavior
Section titled “Retry Behavior”bunqueue automatically retries failed webhook deliveries with linear backoff. The number of retries is controlled by the WEBHOOK_MAX_RETRIES environment variable (default: 3) and the base delay by WEBHOOK_RETRY_DELAY_MS (default: 1000ms):
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 second (WEBHOOK_RETRY_DELAY_MS * 1) |
| 3 | 2 seconds (WEBHOOK_RETRY_DELAY_MS * 2) |
After all attempts are exhausted, the webhook delivery is abandoned and logged.
Success Criteria
Section titled “Success Criteria”A webhook delivery is considered successful if:
- HTTP status code is 2xx (200-299)
- Response is received within 10 seconds
Failure Handling
Section titled “Failure Handling”Webhook failures are logged but don’t affect job processing. Use bunqueue webhook list to review registered webhooks.
Example Webhook Server
Section titled “Example Webhook Server”Complete example using Hono (works with Bun):
import { Hono } from 'hono';import { createHmac, timingSafeEqual } from 'crypto';
const app = new Hono();
// The secret you used when registering the webhook with --secretconst MY_WEBHOOK_SECRET = process.env.MY_WEBHOOK_SECRET || 'your-secret';
// Signature verification middlewareasync function verifySignature(c: any, next: any) { const signature = c.req.header('x-webhook-signature'); const body = await c.req.text();
if (!signature) { return c.json({ error: 'Missing signature' }, 401); }
const expected = createHmac('sha256', MY_WEBHOOK_SECRET) .update(body) .digest('hex');
const expectedBuf = Buffer.from(expected); const receivedBuf = Buffer.from(signature);
if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) { return c.json({ error: 'Invalid signature' }, 401); }
// Store parsed body for handler c.set('webhookPayload', JSON.parse(body)); await next();}
// Webhook handlerapp.post('/webhooks/bunqueue', verifySignature, async (c) => { const payload = c.get('webhookPayload');
console.log(`Received ${payload.event} event for job ${payload.jobId}`);
switch (payload.event) { case 'job.completed': console.log('Job completed:', payload.jobId); // Notify downstream service await notifyCompletion(payload); break;
case 'job.failed': console.error('Job failed:', payload.error); // Alert on failure await sendAlert({ type: 'job_failed', jobId: payload.jobId, queue: payload.queue, error: payload.error, }); break;
case 'job.progress': console.log(`Job ${payload.jobId}: ${payload.progress}%`); // Update UI or notify await broadcastProgress(payload); break; }
return c.json({ received: true });});
// Health check endpointapp.get('/health', (c) => c.json({ status: 'ok' }));
export default { port: 3000, fetch: app.fetch,};Express.js Example
Section titled “Express.js Example”import express from 'express';import crypto from 'crypto';
const app = express();app.use(express.json({ verify: (req: any, res, buf) => { req.rawBody = buf; }}));
// The secret you used when registering the webhook with --secretconst MY_WEBHOOK_SECRET = process.env.MY_WEBHOOK_SECRET!;
function verifySignature(req: any, res: any, next: any) { const signature = req.headers['x-webhook-signature'];
if (!signature) { return res.status(401).json({ error: 'Missing signature' }); }
const expected = crypto .createHmac('sha256', MY_WEBHOOK_SECRET) .update(req.rawBody) .digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) { return res.status(401).json({ error: 'Invalid signature' }); }
next();}
app.post('/webhooks/bunqueue', verifySignature, async (req, res) => { const { event, jobId, queue, data, error } = req.body;
console.log(`[${queue}] ${event}: Job ${jobId}`);
if (event === 'job.completed') { // Handle completion console.log('Completed job:', jobId); } else if (event === 'job.failed') { // Handle failure console.error('Error:', error); }
res.json({ received: true });});
app.listen(3000, () => { console.log('Webhook server listening on port 3000');});Best Practices
Section titled “Best Practices”1. Always Verify Signatures
Section titled “1. Always Verify Signatures”Never trust webhook payloads without signature verification. Attackers could forge requests.
2. Respond Quickly
Section titled “2. Respond Quickly”Return a 200 response as soon as possible. Process webhooks asynchronously if needed:
app.post('/webhooks/bunqueue', verifySignature, async (c) => { const payload = c.get('webhookPayload');
// Queue for async processing await processQueue.add('webhook', payload);
// Return immediately return c.json({ received: true });});3. Handle Duplicates
Section titled “3. Handle Duplicates”Webhooks may be delivered multiple times due to retries. Use the jobId and event combination to deduplicate:
const processedWebhooks = new Set<string>();
app.post('/webhooks/bunqueue', verifySignature, async (c) => { const payload = c.get('webhookPayload'); const dedupeKey = `${payload.jobId}:${payload.event}`;
if (processedWebhooks.has(dedupeKey)) { return c.json({ received: true, duplicate: true }); }
processedWebhooks.add(dedupeKey); // Process webhook...});4. Log Everything
Section titled “4. Log Everything”Log all webhook events for debugging:
app.post('/webhooks/bunqueue', verifySignature, async (c) => { const payload = c.get('webhookPayload');
console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: payload.event, queue: payload.queue, jobId: payload.jobId, }));
// Process...});5. Monitor Delivery
Section titled “5. Monitor Delivery”Monitor webhook registrations and server logs for delivery issues:
# Review registered webhooksbunqueue webhook listTroubleshooting
Section titled “Troubleshooting”Webhooks Not Delivered
Section titled “Webhooks Not Delivered”- Check webhook is registered:
bunqueue webhook list - Verify URL is accessible from server
- Check firewall rules
- Review server logs:
bunqueue logs --filter webhook
Invalid Signature Errors
Section titled “Invalid Signature Errors”- Verify secret matches on both ends
- Check payload isn’t modified by proxies
- Ensure raw body is used for signature verification
- Check for encoding issues (UTF-8)
Slow Webhook Processing
Section titled “Slow Webhook Processing”- Return 200 immediately, process async
- Add more webhook endpoints
- Implement request queuing
Missing Events
Section titled “Missing Events”- Check event filter:
--eventsflag - Verify queue filter:
--queueflag - Check job options (some jobs skip events)