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

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

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