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 |
partner.idp.domain_verified | An IdP email domain was verified for your app |
partner.idp.domain_verification_failed | Domain verification failed (token mismatch or expiry) |
partner.idp.domain_revoked | A previously verified IdP domain lost its DNS proof and was revoked |
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",
"actor_user_id": "user-uuid",
"for_user": {
"id": "user-uuid",
"email": "user@example.com"
},
"data": { /* event-specific payload */ }
}
actor_user_id— the user who triggered the event. Omitted for headless/system-triggered events.for_user— the recipient of this delivery (the user whose partner connection the webhook is sent to). Omitted for partner-app-scoped events such aspartner.idp.*.
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 X-Contio-Idempotency-Key | Idempotency — the same event is only acted on once, even if delivered twice. | Record processed keys and ignore repeats. See De-duplication. |
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-Event-Type | The event type, e.g. action_item.created. |
X-Contio-Event-ID | Unique ID of the logical event (UUID). Stable across retries and redelivery; matches the payload's event_id. |
X-Contio-Idempotency-Key | The de-duplication key. Opaque, stable identifier for this event as delivered to your app (and, for user-scoped events, to that user). Identical on every retry. See De-duplication. |
X-Contio-Delivery-ID | ID of Contio's internal delivery record (UUID). Stable across retries of that record, but not a logical-event key — do not dedupe on it. |
X-Contio-Timestamp | Unix timestamp (seconds) of the event. Informational; see below. |
X-Contio-Retry-Count | Present only on retried deliveries; the attempt number. |
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 X-Contio-Idempotency-Key so a replayed request within the window is still ignored.
De-duplication¶
X-Contio-Idempotency-Key is the single header you should dedupe on. Contio guarantees:
- It is derived from the logical event (
event_type+event_id), your partner app, and — for user-scoped events — thefor_userthe delivery is addressed to. It is never derived from the delivery attempt or the send time. - Every delivery attempt of the same event to your app (for the same
for_user) carries the same key, whether it is an automatic retry, a manual retry via the Admin API, or an internal redelivery of the event. - Different events carry different keys. When one event fans out to several of your connected users, each per-user delivery is a distinct notification and carries its own key; use
event_idif you want to correlate them. - Contio also enforces this key on its side: a repeated internal redelivery of the same event never produces a second
POSTto your endpoint. The header exists so you can enforce the same guarantee end-to-end.
Treat the value as an opaque string of up to 64 characters. Store each processed key (with a TTL at least as long as your replay tolerance window plus our retry horizon — 24 hours is a safe default) and skip processing when a key has already been seen.
X-Contio-Event-ID (equal to the payload's event_id) is also stable per logical event. X-Contio-Delivery-ID identifies Contio's internal delivery record (an operational handle for support) and must not be used for de-duplication.
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 X-Contio-Idempotency-Key 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 + idempotency-key dedup are belt-and-suspenders. If your handler triggers side effects, make those effects idempotent (keyed on X-Contio-Idempotency-Key) — 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
X-Contio-Idempotency-Keyfor idempotency - Monitor failures - Check delivery status regularly