Inbound Webhooks
Receive and process webhooks from external services in your Quickback API.
Inbound webhooks allow external services (Stripe, Paddle, GitHub, etc.) to notify your API of events. Quickback generates endpoints that verify signatures, deduplicate events, and dispatch to your handler functions via a Cloudflare Queue.
Endpoint
POST /webhooks/v1/inbound/:providerThe :provider parameter identifies which service sent the webhook (e.g., stripe, paddle, github). Each provider has its own signature verification logic.
How It Works
External Service → POST /webhooks/v1/inbound/stripe
│
├─ 1. Verify signature (HMAC-SHA256)
├─ 2. Parse event (provider-specific)
├─ 3. Deduplicate via externalId
├─ 4. Store in webhook_events table
├─ 5. Enqueue for async processing
└─ 6. Return 200 OK immediately
Queue Consumer
│
├─ 1. Update status → 'processing'
├─ 2. Call registered handlers
└─ 3. Update status → 'processed' or 'failed'The API returns 200 OK immediately after enqueuing. Your handler logic runs asynchronously via the queue consumer, so external services don't time out waiting for your processing to complete.
Registering Handlers
Use onWebhookEvent() in your action code to register handlers for specific events:
onWebhookEvent("stripe:checkout.session.completed", async (ctx) => {
const { type, data, provider, env } = ctx;
await createSubscription(data, env);
});
// Wildcard — handle all events from a provider
onWebhookEvent("stripe:*", async (ctx) => {
console.log(`Received ${ctx.type} from ${ctx.provider}`);
});Handler context:
| Field | Type | Description |
|---|---|---|
type | string | Full event type (e.g., stripe:checkout.session.completed) |
data | unknown | Parsed event payload |
provider | string | Provider name (e.g., stripe) |
env | CloudflareBindings | Cloudflare environment bindings |
Execution order:
- Specific handlers run first (exact match on event type)
- Wildcard handlers run after
- All matching handlers run in parallel
- If any handler throws, the message is retried
Signature Verification
Each provider verifies signatures differently. The secret is read from an environment variable.
Stripe
Environment variable: STRIPE_WEBHOOK_SECRET
Stripe uses HMAC-SHA256 with a timestamp-based signature:
Stripe-Signature: t=1614556800,v1=abc123...The signed payload is <timestamp>.<raw_body>. Verification checks:
- HMAC matches
- Timestamp is within 5 minutes (prevents replay attacks)
Custom Providers
Providers implement this interface:
interface InboundProvider {
name: string;
verifySignature(payload: string, signature: string, secret: string): Promise<boolean>;
parseEvent(payload: string): {
type: string;
data: unknown;
externalId?: string;
};
}Idempotency
Events are deduplicated using the externalId field (e.g., Stripe's event.id). If an event with the same externalId has already been received, the endpoint returns 200 OK without re-processing.
Database Schema
Inbound events are stored in the webhook_events table:
| Column | Type | Description |
|---|---|---|
id | text | Primary key (whe_<uuid>) |
provider | text | Provider name |
eventType | text | Event type from provider |
externalId | text | Provider's event ID (for deduplication) |
payload | text | Raw JSON payload |
status | text | received, processing, processed, failed |
attempts | integer | Processing attempt count |
processedAt | timestamp | When successfully processed |
error | text | Error message if failed |
createdAt | timestamp | When received |
Admin Routes
Two admin-only routes are generated for monitoring inbound webhooks:
List Events
GET /webhooks/v1/inbound/eventsReturns recent inbound events with their status. Requires admin role.
Retry Failed Event
POST /webhooks/v1/inbound/events/:id/retryRe-queues a failed event for processing. Requires admin role.
Error Handling
- Invalid signature — Returns
401 Unauthorized, event not stored - Handler failure — Event marked as
failed, message retried (up to 3 times via queue) - All retries exhausted — Event remains in
failedstatus; use the admin retry endpoint to re-process manually
Configuration
Webhooks are enabled by setting webhooksBinding in your database config:
database: defineDatabase("cloudflare-d1", {
binding: "DB",
webhooksBinding: "WEBHOOKS_DB",
})Set the provider secret as a Wrangler secret:
wrangler secret put STRIPE_WEBHOOK_SECRETSee Also
- Outbound Webhooks — Send webhooks when data changes
- Queues — Background processing infrastructure