Quickback Docs

Outbound Webhooks

Send webhooks when data changes in your Quickback API.

Outbound webhooks notify external services when events happen in your API. Users register webhook endpoints, subscribe to event types, and receive signed HTTP POST requests when events are emitted.

Overview

Outbound webhooks are a multi-step system:

  1. Users register endpoints via the REST API
  2. Your code emits events using emitWebhookEvent()
  3. Quickback queues deliveries for each matching endpoint
  4. The queue consumer delivers with retry and exponential backoff
  5. Payloads are signed with HMAC-SHA256 (Stripe-compatible format)

Emitting Events

Call emitWebhookEvent() in your actions or custom routes:

import { emitWebhookEvent } from '../lib/webhooks/emit';

// After creating a user
const newUser = await db.insert(users).values(data).returning();

await emitWebhookEvent(
  "user.created",
  newUser[0],
  { organizationId: ctx.activeOrgId },
  env
);

Parameters:

ParameterTypeDescription
eventTypestringEvent name (e.g., user.created)
payloadunknownEvent data (auto-wrapped with metadata)
optionsobject{ organizationId?, userId? } — scope for endpoint matching
envCloudflareBindingsCloudflare environment bindings

Note: The compiler does NOT auto-emit events after CRUD operations. You must call emitWebhookEvent() explicitly where you want webhooks triggered.

Event Types

Quickback provides recommended event naming conventions:

CategoryEvents
Useruser.created, user.updated, user.deleted
Subscriptionsubscription.created, subscription.updated, subscription.cancelled, subscription.renewed
Organizationorganization.created, organization.updated, organization.deleted, organization.member_added, organization.member_removed
Filefile.uploaded, file.deleted

You can emit any custom event type — these are conventions, not restrictions.

Payload Format

The emitted payload is wrapped automatically:

{
  "type": "user.created",
  "data": {
    "id": "usr_abc123",
    "name": "Jane Doe",
    "email": "jane@example.com"
  },
  "createdAt": "2025-01-15T10:00:00.000Z"
}

Payload Signing

Every delivery is signed with HMAC-SHA256 using the endpoint's secret. The signature format is Stripe-compatible:

X-Webhook-Signature: t=1705312800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Signed payload: <timestamp>.<json_body>

Additional headers:

HeaderDescription
X-Webhook-Signaturet=<timestamp>,v1=<hmac>
X-Webhook-EventEvent type (e.g., user.created)
X-Webhook-DeliveryDelivery ID for tracking
Content-Typeapplication/json

Verifying Signatures (Consumer Side)

import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const [tPart, v1Part] = signature.split(',');
  const timestamp = tPart.replace('t=', '');
  const expectedSig = v1Part.replace('v1=', '');

  // Check timestamp (5 minute tolerance)
  const age = Date.now() / 1000 - parseInt(timestamp);
  if (age > 300) return false;

  const signed = `${timestamp}.${payload}`;
  const computed = createHmac('sha256', secret).update(signed).digest('hex');
  return timingSafeEqual(Buffer.from(computed), Buffer.from(expectedSig));
}

Endpoint Management API

Register an Endpoint

POST /webhooks/v1/endpoints
{
  "name": "My Integration",
  "url": "https://example.com/webhook",
  "events": ["user.created", "subscription.*"]
}

Response (201):

{
  "id": "wep_abc123",
  "name": "My Integration",
  "url": "https://example.com/webhook",
  "secret": "whsec_a1b2c3d4...",
  "events": ["user.created", "subscription.*"],
  "enabled": true,
  "createdAt": "2025-01-15T10:00:00.000Z"
}

The secret is only returned on creation. Store it securely.

Event Pattern Matching

Endpoints subscribe to events using patterns:

PatternMatches
user.createdExact match only
user.*All user events
*All events

Other Endpoints

MethodPathDescription
GET/webhooks/v1/endpointsList your endpoints
GET/webhooks/v1/endpoints/:idGet endpoint details (secret masked)
PATCH/webhooks/v1/endpoints/:idUpdate endpoint (name, url, events, enabled)
DELETE/webhooks/v1/endpoints/:idDelete endpoint
GET/webhooks/v1/endpoints/:id/deliveriesList delivery attempts
POST/webhooks/v1/endpoints/:id/testSend a test event
POST/webhooks/v1/endpoints/:id/rotate-secretGenerate a new signing secret
GET/webhooks/v1/eventsList available event types

Access Control

Endpoints are scoped by userId or organizationId. Users can only manage their own endpoints. When emitting events, only endpoints matching the event's scope receive deliveries.

Delivery and Retry

Deliveries are processed asynchronously via a Cloudflare Queue.

Success: HTTP 2xx response marks the delivery as delivered.

Failure: HTTP 4xx/5xx or network errors trigger retries with exponential backoff:

AttemptDelay
1st retry~2 seconds
2nd retry~4 seconds
3rd retry (final)~8 seconds

Maximum backoff is capped at 1 hour. After 3 failed attempts, the delivery is marked as failed.

Disabled endpoints: If an endpoint is disabled or deleted between emission and delivery, the delivery is marked as failed immediately.

Delivery Record

Each delivery attempt is tracked in the webhook_deliveries table:

FieldDescription
statuspending, delivered, or failed
attemptsNumber of delivery attempts
responseStatusHTTP status from last attempt
responseBodyResponse body (truncated to 1000 chars)
lastAttemptAtTimestamp of last attempt
nextRetryAtWhen the next retry will occur

Infrastructure

Queue Configuration

Outbound webhooks use a dedicated Cloudflare Queue:

# Auto-generated in wrangler.toml
[[queues.producers]]
queue = "my-app-webhooks-queue"
binding = "WEBHOOKS_QUEUE"

[[queues.consumers]]
queue = "my-app-webhooks-queue"
max_batch_size = 10
max_batch_timeout = 30
max_retries = 3
dead_letter_queue = "my-app-webhooks-dlq"

Dead Letter Queue

Failed webhook deliveries (after all retries) are sent to a dead letter queue for investigation:

[[queues.consumers]]
queue = "my-app-webhooks-dlq"
max_batch_size = 1

Enabling Webhooks

Set webhooksBinding in your database provider config:

database: defineDatabase("cloudflare-d1", {
  binding: "DB",
  webhooksBinding: "WEBHOOKS_DB",
})

This generates the webhook database schema, routes, queue consumer, and all supporting code.

See Also

  • Inbound Webhooks — Receive webhooks from external services
  • Queues — Background processing infrastructure

On this page