OptimoCMSDocs
Guides

Webhook sync with CRM

Keep your CRM automatically synchronised with OptimoCMS via webhooks. Learn how to set up webhooks, process payloads and verify signatures.

Webhook sync with your CRM

Webhooks send real-time notifications to your server whenever something happens in OptimoCMS — a form is submitted, an order is placed, or a page is published.

In this tutorial:

  • Create a webhook via the SDK
  • Build an Express server to receive payloads
  • Cryptographic signature verification
  • Process payloads and forward them to a CRM

Prerequisites

  • Node.js 18+
  • npm install @optimocms/sdk express
  • A publicly reachable endpoint (use ngrok for local testing)

Step 1 — Create a webhook

SDK

import { OptimoCMS } from '@optimocms/sdk';

const client = new OptimoCMS({
  apiKey: process.env.OPTIMOCMS_API_KEY!,
});

const webhook = await client.webhooks.create('site_abc123', {
  url: 'https://your-server.com/webhooks/optimocms',
  events: ['form.submitted', 'order.created', 'booking.created'],
});

console.log(`Webhook created: ${webhook.id}`);
console.log(`Secret: ${webhook.secret}`);

REST

curl -X POST https://api.optimocms.com/v1/sites/$SITE_ID/webhooks \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/optimocms",
    "events": ["form.submitted", "order.created", "booking.created"]
  }'

Response:

{
  "id": "wh_abc123",
  "url": "https://your-server.com/webhooks/optimocms",
  "events": ["form.submitted", "order.created", "booking.created"],
  "secret": "whsec_k7x9m2p4q8r1t5w3",
  "active": true,
  "createdAt": "2026-05-26T14:00:00Z",
  "failCount": 0,
  "lastDeliveredAt": null
}

Step 2 — Build a webhook endpoint

Create an Express server that receives and processes webhook payloads:

import express from 'express';
import crypto from 'node:crypto';

const app = express();
const WEBHOOK_SECRET = process.env.OPTIMOCMS_WEBHOOK_SECRET!;

app.post(
  '/webhooks/optimocms',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-optimocms-signature'] as string;
    const timestamp = req.headers['x-optimocms-timestamp'] as string;

    if (!verifySignature(req.body, signature, timestamp)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString());

    switch (event.type) {
      case 'form.submitted':
        handleFormSubmission(event.data);
        break;
      case 'order.created':
        handleNewOrder(event.data);
        break;
      case 'booking.created':
        handleNewBooking(event.data);
        break;
      default:
        console.log(`Unknown event type: ${event.type}`);
    }

    res.status(200).json({ received: true });
  }
);

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

Step 3 — Signature verification

OptimoCMS signs every webhook with HMAC-SHA256. Always verify before processing:

function verifySignature(
  body: Buffer,
  signature: string,
  timestamp: string
): boolean {
  if (!signature || !timestamp) return false;

  const timestampMs = parseInt(timestamp, 10);
  const ageMs = Date.now() - timestampMs;
  if (ageMs > 5 * 60 * 1000) {
    console.error(`Webhook too old: ${ageMs}ms`);
    return false;
  }

  const payload = `${timestamp}.${body.toString()}`;
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  const expectedBuffer = Buffer.from(expected, 'hex');
  const receivedBuffer = Buffer.from(signature, 'hex');

  if (expectedBuffer.length !== receivedBuffer.length) return false;

  return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
}

Step 4 — Forward payloads to CRM

Example: forward form data to HubSpot CRM:

interface FormSubmission {
  formId: string;
  siteId: string;
  fields: Record<string, string>;
  submittedAt: string;
}

async function handleFormSubmission(data: FormSubmission): Promise<void> {
  const { fields } = data;

  const response = await fetch('https://api.hubapi.com/crm/v3/objects/contacts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.HUBSPOT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      properties: {
        firstname: fields.firstName || fields.name?.split(' ')[0] || '',
        lastname: fields.lastName || fields.name?.split(' ').slice(1).join(' ') || '',
        email: fields.email,
        phone: fields.phone || '',
        company: fields.company || '',
        hs_lead_status: 'NEW',
        source: `OptimoCMS form: ${data.formId}`,
      },
    }),
  });

  if (!response.ok) {
    const error = await response.text();
    console.error(`HubSpot sync failed: ${error}`);
    throw new Error(`CRM sync failed: ${response.status}`);
  }

  console.log(`✓ Contact created in HubSpot for ${fields.email}`);
}

async function handleNewOrder(data: { orderId: string; customer: { email: string; name: string }; totalCents: number }) {
  console.log(`New order: ${data.orderId} from ${data.customer.name} (€${(data.totalCents / 100).toFixed(2)})`);
}

async function handleNewBooking(data: { bookingId: string; customer: { name: string; email: string }; date: string; startTime: string }) {
  console.log(`New booking: ${data.bookingId} - ${data.customer.name} on ${data.date} at ${data.startTime}`);
}

Step 5 — Monitor webhook deliveries

Check whether webhooks are delivered successfully:

SDK

for await (const delivery of client.webhooks.listDeliveries('site_abc123', 'wh_abc123')) {
  const status = delivery.success ? '✓' : '✗';
  console.log(`${status} ${delivery.event} → ${delivery.statusCode} (${delivery.durationMs}ms)`);
}

const replay = await client.webhooks.replay('site_abc123', 'wh_abc123', {
  since: '2026-05-25T00:00:00Z',
  eventTypes: ['form.submitted'],
});

console.log(`${replay.replayed} events replayed`);

REST

curl https://api.optimocms.com/v1/sites/$SITE_ID/webhooks/$WEBHOOK_ID/deliveries \
  -H "Authorization: Bearer $API_KEY"

curl -X POST https://api.optimocms.com/v1/sites/$SITE_ID/webhooks/$WEBHOOK_ID/replay \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "since": "2026-05-25T00:00:00Z" }'

Available webhook events

EventTrigger
form.submittedVisitor submits a form
order.createdNew webshop order
order.updatedOrder status changes
booking.createdNew reservation
booking.cancelledReservation cancelled
page.createdNew page created
page.updatedPage content changed
page.deletedPage deleted
site.publishedSite published to production

Best practices

  1. Always verify the signature — Never blindly trust incoming requests
  2. Respond quickly with 200 — Process the payload asynchronously if it takes long
  3. Idempotency — Use the id field to detect duplicates
  4. Retry-tolerant — OptimoCMS automatically retries failed deliveries (max 5x)
  5. Logging — Log every incoming webhook for debugging

Next steps

On this page