Quickback Docs

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/:provider

The :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:

FieldTypeDescription
typestringFull event type (e.g., stripe:checkout.session.completed)
dataunknownParsed event payload
providerstringProvider name (e.g., stripe)
envCloudflareBindingsCloudflare environment bindings

Execution order:

  1. Specific handlers run first (exact match on event type)
  2. Wildcard handlers run after
  3. All matching handlers run in parallel
  4. 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:

ColumnTypeDescription
idtextPrimary key (whe_<uuid>)
providertextProvider name
eventTypetextEvent type from provider
externalIdtextProvider's event ID (for deduplication)
payloadtextRaw JSON payload
statustextreceived, processing, processed, failed
attemptsintegerProcessing attempt count
processedAttimestampWhen successfully processed
errortextError message if failed
createdAttimestampWhen received

Admin Routes

Two admin-only routes are generated for monitoring inbound webhooks:

List Events

GET /webhooks/v1/inbound/events

Returns recent inbound events with their status. Requires admin role.

Retry Failed Event

POST /webhooks/v1/inbound/events/:id/retry

Re-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 failed status; 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_SECRET

See Also

On this page