Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/openshiporg/openfront/llms.txt

Use this file to discover all available pages before exploring further.

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": "customer@example.com",
      "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,
  });
}