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
| Event | Trigger |
|---|---|
form.submitted | Visitor submits a form |
order.created | New webshop order |
order.updated | Order status changes |
booking.created | New reservation |
booking.cancelled | Reservation cancelled |
page.created | New page created |
page.updated | Page content changed |
page.deleted | Page deleted |
site.published | Site published to production |
Best practices
- Always verify the signature — Never blindly trust incoming requests
- Respond quickly with 200 — Process the payload asynchronously if it takes long
- Idempotency — Use the
idfield to detect duplicates - Retry-tolerant — OptimoCMS automatically retries failed deliveries (max 5x)
- Logging — Log every incoming webhook for debugging
Next steps
- API Reference: Webhooks — Full webhook API documentation
- CI/CD pipeline — Automate deployments
- Build a restaurant site — Example site with forms