Skip to main content

Overview

Openfront’s webhook system automatically notifies your application when events occur, such as orders being created, products being updated, or payments being captured. This enables real-time integrations with external systems.

How Webhooks Work

The webhook system (features/webhooks/webhook-plugin.ts) automatically captures events from all KeystoneJS models:
  1. Event Detection - Hooks capture create, update, and delete operations
  2. Payload Enrichment - Enrichers add related data to webhook payloads
  3. Event Batching - Events are batched every 100ms for performance
  4. Webhook Delivery - Events are sent to registered endpoints with signatures
  5. Retry Logic - Failed deliveries are retried with exponential backoff

Webhook Events

Events are automatically generated for all model operations:
// Event naming convention
{listKey}.{operation}

// Examples
order.created
order.updated
order.deleted
product.created
product.updated
cart.completed
payment.captured
You can also subscribe to all events using the wildcard *.

Webhook Models

WebhookEndpoint

Webhook endpoints are configured in the admin panel:
features/keystone/models/WebhookEndpoint.ts
type WebhookEndpoint {
  id: ID!
  url: String!              // Endpoint URL to receive webhooks
  events: [String!]!        // Event types to subscribe to
  isActive: Boolean!        // Enable/disable endpoint
  secret: String            // Auto-generated signature secret
  lastTriggered: DateTime   // Last successful delivery
  failureCount: Int!        // Consecutive delivery failures
  webhookEvents: [WebhookEvent!]!  // Related events
  createdAt: DateTime!
  updatedAt: DateTime!
}

WebhookEvent

Each webhook delivery is tracked:
features/keystone/models/WebhookEvent.ts
type WebhookEvent {
  id: ID!
  eventType: String!        // e.g., "order.created"
  resourceId: String!       // ID of the resource
  resourceType: String!     // Model name
  payload: JSON!            // Full event payload
  deliveryAttempts: Int!    // Number of attempts
  delivered: Boolean!       // Success status
  lastAttempt: DateTime     // Last delivery attempt
  nextAttempt: DateTime     // Scheduled retry time
  responseStatus: Int       // HTTP status code
  responseBody: String      // Response from endpoint
  endpoint: WebhookEndpoint!
  createdAt: DateTime!
}

Creating a Webhook Endpoint

Via Admin Panel

  1. Navigate to Webhooks > Webhook Endpoints
  2. Click Create Webhook Endpoint
  3. Enter your endpoint URL
  4. Select events to subscribe to
  5. Save - a secret key is automatically generated

Via GraphQL API

mutation CreateWebhook {
  createWebhookEndpoint(
    data: {
      url: "https://your-app.com/webhooks/openfront"
      events: ["order.created", "order.updated", "payment.captured"]
      isActive: true
    }
  ) {
    id
    url
    secret
    events
  }
}

Webhook Payload Format

Webhooks are delivered as POST requests with this structure:
{
  "event": "order.created",
  "timestamp": "2024-02-28T10:30:00.000Z",
  "listKey": "Order",
  "operation": "create",
  "data": {
    "id": "order_123",
    "status": "pending",
    "total": 12999,
    "currency": "USD",
    "customer": {
      "id": "customer_456",
      "email": "[email protected]",
      "firstName": "John",
      "lastName": "Doe"
    },
    "items": [
      {
        "id": "item_789",
        "productVariant": {
          "id": "variant_012",
          "title": "Blue T-Shirt - Medium",
          "sku": "TSHIRT-BLU-M"
        },
        "quantity": 2,
        "unitPrice": 2999
      }
    ],
    "shippingAddress": {
      "firstName": "John",
      "lastName": "Doe",
      "address1": "123 Main St",
      "city": "New York",
      "province": "NY",
      "postalCode": "10001",
      "country": "US"
    }
  }
}

Webhook Headers

Webhook requests include these headers:
{
  "Content-Type": "application/json",
  "X-OpenFront-Webhook-Signature": "sha256=abc123...",
  "X-OpenFront-Topic": "order.created",
  "X-OpenFront-ListKey": "Order",
  "X-OpenFront-Operation": "create",
  "X-OpenFront-Delivery-ID": "webhook_event_123"
}

Verifying Webhook Signatures

Always verify webhook signatures to ensure authenticity:
import crypto from 'crypto';
import express from 'express';

const app = express();

app.post('/webhooks/openfront', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-openfront-webhook-signature'];
  const secret = process.env.OPENFRONT_WEBHOOK_SECRET; // From admin panel
  
  // Compute expected signature
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');
  
  // Verify signature
  if (signature !== expectedSignature) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }
  
  // Parse and process webhook
  const payload = JSON.parse(req.body.toString());
  
  switch (payload.event) {
    case 'order.created':
      handleOrderCreated(payload.data);
      break;
    case 'order.updated':
      handleOrderUpdated(payload.data, payload.changes);
      break;
  }
  
  res.status(200).send('OK');
});

Payload Enrichment

Webhook payloads can be enriched with related data using enrichers.

Order Enricher Example

The order enricher (features/webhooks/enrichers/order-enricher.ts) adds customer and line item data:
import { BaseWebhookEnricher } from './base-enricher';

export class OrderWebhookEnricher extends BaseWebhookEnricher {
  entityType = 'Order';

  async enrich(order: any, context: any) {
    // Fetch complete order with relationships
    const enrichedOrder = await context.query.Order.findOne({
      where: { id: order.id },
      query: `
        id
        status
        displayId
        total
        currency { code }
        customer {
          id
          email
          firstName
          lastName
        }
        items {
          id
          quantity
          unitPrice
          productVariant {
            id
            title
            sku
          }
        }
        shippingAddress {
          firstName
          lastName
          address1
          city
          province
          postalCode
          country
        }
      `,
    });

    return enrichedOrder;
  }
}

Creating Custom Enrichers

Create enrichers for any model:
features/webhooks/enrichers/product-enricher.ts
import { BaseWebhookEnricher } from './base-enricher';
import { registerWebhookEnricher } from './registry';

export class ProductWebhookEnricher extends BaseWebhookEnricher {
  entityType = 'Product';

  async enrich(product: any, context: any) {
    const enrichedProduct = await context.query.Product.findOne({
      where: { id: product.id },
      query: `
        id
        title
        handle
        description
        status
        variants {
          id
          title
          sku
          price
          inventory
        }
        images {
          id
          url
          alt
        }
        collections {
          id
          title
        }
      `,
    });

    return enrichedProduct;
  }
}

// Register the enricher
registerWebhookEnricher(new ProductWebhookEnricher());

Webhook Implementation Details

Automatic Hook Registration

The webhook system automatically wraps all KeystoneJS models:
features/webhooks/webhook-plugin.ts
export function withWebhooks<TypeInfo extends BaseListTypeInfo>(
  config: KeystoneConfig<TypeInfo>
): KeystoneConfig<TypeInfo> {
  const enhancedLists = Object.fromEntries(
    Object.entries(config.lists || {}).map(([listKey, listConfig]) => [
      listKey,
      {
        ...listConfig,
        hooks: {
          ...listConfig.hooks,
          afterOperation: async (args) => {
            // Call original hook
            if (listConfig.hooks?.afterOperation) {
              await listConfig.hooks.afterOperation(args);
            }
            
            // Trigger webhook
            await queueWebhook({
              listKey,
              operation: args.operation,
              item: args.item,
              originalItem: args.originalItem,
              context: args.context.sudo()
            });
          }
        }
      }
    ])
  );

  return { ...config, lists: enhancedLists };
}

Batching and Performance

Webhooks are batched to improve performance:
let webhookQueue: WebhookPayload[] = [];
let batchTimer: NodeJS.Timeout | null = null;

async function queueWebhook(payload: WebhookPayload) {
  webhookQueue.push(payload);
  
  if (!batchTimer) {
    batchTimer = setTimeout(processBatch, 100); // 100ms batch window
  }
}

Retry Logic

Failed webhook deliveries are automatically retried:
// Exponential backoff retry schedule
const nextAttempt = new Date(
  Date.now() + Math.pow(2, deliveryAttempts) * 60000
);

// Disable endpoint after too many failures
if (failureCount > 10) {
  await context.query.WebhookEndpoint.updateOne({
    where: { id: endpoint.id },
    data: { isActive: false },
  });
}

Testing Webhooks

Local Development

Use tools like ngrok to expose your local server:
ngrok http 3000
Then use the ngrok URL in your webhook configuration:
https://abc123.ngrok.io/webhooks/openfront

Webhook Testing Services

Manual Webhook Trigger

Trigger webhooks manually for testing:
import { manualTriggerWebhook } from '@/features/webhooks/webhook-plugin';

await manualTriggerWebhook(
  context,
  'Order',
  'create',
  orderData
);

Best Practices

  • Always verify webhook signatures
  • Use HTTPS endpoints only
  • Keep webhook secrets secure
  • Implement IP allowlisting if possible
  • Log all webhook attempts for auditing
  • Return 200 OK quickly (within 5 seconds)
  • Process webhooks asynchronously in background jobs
  • Implement idempotency using delivery IDs
  • Handle duplicate deliveries gracefully
  • Monitor webhook delivery success rates
  • Return appropriate HTTP status codes
  • Log errors for debugging
  • Implement dead letter queues for failed webhooks
  • Alert on consistent failures
  • Provide webhook retry mechanisms
  • Subscribe only to needed events
  • Batch process webhook data when possible
  • Cache frequently accessed data
  • Use webhook event IDs to prevent duplicate processing
  • Monitor webhook processing times

Common Use Cases

Order Fulfillment Integration

app.post('/webhooks/openfront', async (req, res) => {
  const { event, data } = req.body;
  
  if (event === 'order.created') {
    // Send order to fulfillment service
    await fulfillmentService.createOrder({
      orderId: data.id,
      items: data.items.map(item => ({
        sku: item.productVariant.sku,
        quantity: item.quantity,
      })),
      shippingAddress: data.shippingAddress,
    });
  }
  
  res.status(200).send('OK');
});

Inventory Sync

if (event === 'product.updated' && payload.changes.inventory) {
  await inventorySystem.updateStock(
    payload.data.sku,
    payload.data.inventory
  );
}

Analytics Tracking

if (event === 'order.created') {
  await analytics.track('Order Placed', {
    orderId: data.id,
    total: data.total,
    currency: data.currency.code,
    items: data.items.length,
  });
}

Build docs developers (and LLMs) love