Skip to content

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 as partner.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 — the for_user the 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_id if 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 POST to 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

  1. Respond quickly - Return 200 within 5 seconds, process async
  2. Verify signatures - Always validate webhook authenticity
  3. Handle duplicates - Use X-Contio-Idempotency-Key for idempotency
  4. Monitor failures - Check delivery status regularly