convex-webhook-sender
v1.1.0
Published
Managed webhook delivery system for Convex — queue outbound webhooks with retries, exponential backoff, HMAC signing, and delivery tracking
Maintainers
Readme
convex-webhook-sender
A Convex component for managed outbound webhook delivery with HMAC signing, retries, exponential backoff, and delivery tracking.
Features
- HMAC-SHA256 signing with
whsec_prefixed secrets (Standard Webhooks compatible) - Automatic retries with exponential backoff (configurable max retries & window)
- Delivery modes: parallel (default) or serialized (FIFO per destination)
- Delivery tracking: full attempt history with status codes and errors
- Signature verification utility for webhook recipients
Installation
npm install convex-webhook-senderSetup
In your convex/convex.config.ts:
import { defineApp } from "convex/server";
import webhookSender from "convex-webhook-sender/convex.config";
const app = defineApp();
app.use(webhookSender);
export default app;Create the client in a helper file like convex/webhooks.ts:
import { WebhookSender } from "convex-webhook-sender";
import { components } from "./_generated/api";
export const webhooks = new WebhookSender(components.webhookSender);Usage
Register a destination
// convex/myFunctions.ts
import { mutation } from "./_generated/server";
import { webhooks } from "./webhooks";
export const addWebhookEndpoint = mutation({
args: { url: v.string() },
handler: async (ctx, args) => {
const { destinationId, secret } = await webhooks.registerDestination(ctx, {
url: args.url,
maxRetries: 5,
retryWindowMs: 24 * 60 * 60 * 1000, // 24 hours
});
// Share `secret` with the recipient for signature verification
return { destinationId, secret };
},
});Send a webhook
export const onOrderCreated = mutation({
handler: async (ctx) => {
await webhooks.send(ctx, {
destinationId: "...",
eventType: "order.created",
payload: { orderId: "123", total: 99.99 },
});
},
});Query delivery status
export const checkWebhook = query({
args: { webhookId: v.string() },
handler: async (ctx, args) => {
return await webhooks.getWebhookStatus(ctx, args.webhookId);
},
});Verify signatures (recipient side)
import { verifyWebhookSignature } from "convex-webhook-sender";
// In your webhook handler (e.g., Express, Next.js API route)
const isValid = await verifyWebhookSignature(
rawBody,
{
"webhook-id": req.headers["webhook-id"],
"webhook-timestamp": req.headers["webhook-timestamp"],
"webhook-signature": req.headers["webhook-signature"],
},
"whsec_your_secret_here"
);API
WebhookSender class
| Method | Description |
|--------|-------------|
| registerDestination(ctx, config) | Register a new webhook URL. Returns { destinationId, secret } |
| removeDestination(ctx, id) | Deactivate a destination |
| updateDestination(ctx, id, updates) | Update destination config |
| send(ctx, { destinationId, eventType, payload }) | Queue a webhook for delivery |
| getDestination(ctx, id) | Get destination details |
| listDestinations(ctx, options?) | List all destinations |
| getSigningSecret(ctx, id) | Get the HMAC signing secret |
| getWebhookStatus(ctx, id) | Get webhook delivery status |
| getDeliveryHistory(ctx, destId, options?) | Get delivery attempt history |
| getFailedWebhooks(ctx, destId, options?) | Get failed webhooks |
Webhook payload format
Webhooks are delivered as HTTP POST with:
POST <destination_url>
Content-Type: application/json
webhook-id: msg_<webhook_id>
webhook-timestamp: <unix_seconds>
webhook-signature: v1,<base64_hmac_sha256>
{"type":"<event_type>","timestamp":<unix_seconds>,"data":<payload>}Signature verification
The signature is computed as: HMAC-SHA256(secret, "msg_id.timestamp.body")
License
MIT
