Webhook Events¶
Receive real-time notifications when events occur in Contio MeetingOS.
Full Reference
For complete event schemas and field documentation, see the Webhook Events Reference.
Webhook Management
To manage webhook delivery status and event filtering, see Webhook Management.
Overview¶
Webhooks allow your application to receive push notifications for key events:
| Event Type | Description |
|---|---|
automation.assignment.created | Action item assigned to your automation |
action_item.created | New action item created |
action_item.updated | Action item status changed |
action_item.completed | Action item marked as completed |
meeting.created | New meeting created |
meeting.updated | Meeting properties changed |
meeting.completed | Meeting processing completed, notes available |
calendar_event.created | Calendar event synced from external calendar |
calendar_event.updated | Calendar event updated in external calendar |
calendar_event.deleted | Calendar event deleted from external calendar |
agenda_item.created | Agenda item created in a meeting |
agenda_item.updated | Agenda item updated |
agenda_item.deleted | Agenda item deleted |
participant.added | Participant(s) added to a meeting |
participant.removed | Participant removed from a meeting |
user.connection.revoked | User disconnected from your app |
Quick Start¶
1. Configure Webhook URL¶
Set your webhook URL when creating an automation:
const automation = await admin.createAutomation({
name: 'CRM Integration',
webhook_url: 'https://your-app.com/webhooks/contio',
matching_rules: { keywords: ['follow-up', 'sales'] },
is_active: true
});
2. Implement Webhook Handler¶
import express from 'express';
import { WebhookVerifier } from '@contio/partner-sdk';
const app = express();
const verifier = new WebhookVerifier(process.env.WEBHOOK_SECRET!);
app.post('/webhooks/contio',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-contio-signature'] as string;
// The signature is HMAC-SHA256 over the raw request body only.
if (!verifier.verifySignature(req.body, signature).isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
processWebhookAsync(event); // Process async, respond fast
res.status(200).json({ received: true });
});
Event Payload Structure¶
All events follow a consistent envelope:
{
"event_type": "automation.assignment.created",
"event_id": "evt-uuid",
"timestamp": "2025-01-15T10:30:00Z",
"partner_app_id": "app-uuid",
"data": { /* event-specific payload */ }
}
Minimal Payloads
Webhook payloads are intentionally minimal (IDs + key state). Use the Partner API to fetch full details when needed.
Security model¶
Securing a webhook endpoint is deliberately simple: three small, independent checks give you strong guarantees, and each is only a few lines of code.
| Layer | What it protects | How |
|---|---|---|
| 1. Verify the signature | Authenticity & integrity — the request really came from Contio and was not altered. | HMAC-SHA256 over the raw body, compared in constant time. |
| 2. Check freshness | Staleness — bounds how long a captured delivery stays acceptable. | Reject if X-Contio-Timestamp is outside your tolerance window. |
3. De-duplicate by event_id | Idempotency — the same event is only acted on once, even if delivered twice. | Record processed event_ids and ignore repeats. |
Implement these three checks — the SDK's WebhookVerifier handles layer 1 for you — and your endpoint is secure by construction. The sections below explain when layers 2 and 3 matter, so you can right-size the effort.
Signature Verification¶
All webhooks include a signature for security:
| Header | Description |
|---|---|
X-Contio-Signature | HMAC-SHA256 signature of the raw body, format sha256=<hex>. |
X-Contio-Timestamp | Unix timestamp (seconds) of the event. Informational; see below. |
The signature is computed over the raw request body only — the timestamp is not part of the signed material.
Manual Verification¶
import crypto from 'crypto';
function verifyWebhook(
payload: Buffer,
signature: string,
secret: string
): boolean {
// Signature header is "sha256=<hex>"; compare over the decoded digest.
const [scheme, provided] = signature.split('=');
if (scheme !== 'sha256' || !provided) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const providedBuf = Buffer.from(provided, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
if (providedBuf.length !== expectedBuf.length) return false;
return crypto.timingSafeEqual(providedBuf, expectedBuf);
}
Replay protection¶
A replay is when someone captures a legitimate, correctly-signed delivery and re-sends the exact bytes later. The signature still validates (the body is unchanged), so signature verification alone doesn't stop it — freshness and de-duplication do.
Use the X-Contio-Timestamp header (the event's Unix timestamp in seconds) to bound staleness: verify the signature first, then reject deliveries whose timestamp is outside your tolerance window (for example, more than five minutes old), and de-duplicate by event_id so a replayed request within the window is still ignored.
The header mirrors the signed timestamp inside the payload for convenience (so you can check freshness before parsing JSON). If you want a fully-trusted value, read timestamp from the verified payload rather than the header.
Do you need to worry about replays?¶
Contio webhooks are event signals, not commands — payloads are intentionally minimal (IDs + key state), and the expected reaction is to fetch full details from the Partner API. For that fetch-and-reconcile pattern, a replay is harmless: you re-fetch and converge on the same authoritative state, because the fetch is naturally idempotent.
Replays only cause problems if your handler has non-idempotent side effects — for example creating a CRM record, sending a message, or enqueuing a job on each delivery. If that describes your integration, the event_id de-duplication above is your mitigation: it guarantees each logical event is acted on exactly once.
Rule of thumb
If your handler only reads Contio data and reconciles local state, freshness + event_id dedup are belt-and-suspenders. If your handler triggers side effects, make those effects idempotent (keyed on event_id) — that single step neutralizes replays entirely.
Delivery & Retries¶
- Timeout: 30 seconds
- Retries: 3 attempts with exponential backoff
- Retry intervals: 1 min, 5 min, 30 min
Retry Failed Deliveries¶
// List failed deliveries
const deliveries = await admin.getWebhookDeliveries({
status: 'failed'
});
// Retry a specific delivery
await admin.retryWebhookDelivery(deliveryId);
Best Practices¶
- Respond quickly - Return 200 within 5 seconds, process async
- Verify signatures - Always validate webhook authenticity
- Handle duplicates - Use
event_idfor idempotency - Monitor failures - Check delivery status regularly