For the complete documentation index, see llms.txt.

Webhooks

Subscribe to real-time events when bookings are created, updated, or cancelled.

Overview

Vennio sends HTTP POST requests to your registered URL whenever a subscribed event occurs. Webhook deliveries include a signature header for verification and are retried up to 3 times with exponential backoff.

Webhook events

Event Description
booking.created A new booking was made
booking.updated A booking was rescheduled
booking.confirmed A paid booking was confirmed after successful payment
booking.cancelled A booking was cancelled
consent.granted A user granted calendar access
consent.revoked A user revoked calendar access
proposal.created A new multi-party meeting proposal was sent
proposal.countered A participant proposed alternative time slots
proposal.accepted All required participants accepted the proposal
proposal.rejected A required participant declined the proposal
proposal.cancelled An accepted proposal was cancelled by a participant
proposal.expired A proposal expired without resolution
proposal.resolved A poll-mode proposal resolved to a winning slot

Note: proposal.consent_revoked is not a webhook event. If a recipient revokes booking:propose consent between create and accept, the proposal transitions to expired and the originator is notified by email — no webhook is fired.

Create a webhook

curl -X POST https://api.vennio.app/v1/webhooks \
  -H "Authorization: Bearer vennio_sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/vennio",
    "events": ["booking.created", "booking.cancelled"],
    "description": "My booking notifications"
  }'
{
  "id": "wh_abc123",
  "url": "https://your-app.com/webhooks/vennio",
  "events": ["booking.created", "booking.cancelled"],
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxx",
  "created_at": "2026-01-20T10:00:00Z"
}
Save your webhook secret

The webhook signing secret is only shown once at creation. Use it to verify incoming payloads.

Verify signatures

Every webhook delivery includes a X-Webhook-Signature header containing an HMAC-SHA256 signature of the raw request body. Always verify this before processing.

import crypto from 'crypto'

function verifyWebhookSignature(body, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body, 'utf8')
    .digest('hex')
  if (signature.length !== expected.length) return false
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

// Express handler
app.post('/webhooks/vennio', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-webhook-signature']
  if (!verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  const event = JSON.parse(req.body)
  console.log('Event received:', event.type)

  // Process event...
  res.json({ received: true })
})

Retry schedule

If your endpoint returns a non-2xx status or times out, Vennio retries with backoff:

Attempt Delay
1st retry 1 minute
2nd retry 5 minutes
3rd retry 30 minutes
Failed Marked as failed after 4 total attempts

View delivery history at GET /v1/webhooks/{id}/deliveries.

Event payloads

All events follow a consistent envelope:

{
  "id": "evt_abc123",
  "type": "booking.created",
  "created_at": "2026-01-20T14:30:00Z",
  "data": {
    "booking": {
      "id": "booking_xyz789",
      "status": "confirmed",
      "start_time": "2026-01-21T09:00:00Z",
      "end_time": "2026-01-21T09:30:00Z",
      "customer_email": "customer@example.com",
      "customer_name": "Jane Doe"
    }
  }
}

proposal.resolved

Fires when a poll-mode proposal resolves to a winning slot. Same shape regardless of which trigger resolved it (all-voted, organiser close, reaper expiry). Delivered post-commit, after proposals.status='accepted' is durable.

Only the organiser's webhook subscribers receive this event. Voters without Vennio accounts — including all open-poll voters and any email-keyed named voters — are unreachable via webhooks by design. Fan-out to voters is the builder's surface: see Poll-Mode Proposals for the Model 1 boundary.

{
  "id": "evt_abc123",
  "type": "proposal.resolved",
  "created_at": "2026-06-10T13:00:00Z",
  "data": {
    "proposal_id": "prop_xxx",
    "thread_id": "thr_xxx",
    "poll_variant": "open",
    "winning_slot": {
      "id": "slot_abc123",
      "start": "2026-06-12T19:00:00Z",
      "end": "2026-06-12T21:00:00Z"
    },
    "vote_count": 4,
    "total_votes": 6,
    "tie_count": 1
  }
}

poll_variant is "named" or "open". tie_count is the number of slots that finished on the winning vote count — 1 means a clean winner; 2+ means earliest start_time broke the tie.

For paid bookings, the booking.confirmed event includes payment details:

{
  "id": "evt_def456",
  "type": "booking.confirmed",
  "created_at": "2026-03-15T10:05:00Z",
  "data": {
    "booking": {
      "id": "booking_xyz789",
      "status": "confirmed",
      "start_time": "2026-03-15T10:00:00Z",
      "end_time": "2026-03-15T10:30:00Z",
      "customer_email": "customer@example.com",
      "customer_name": "Jane Doe",
      "payment_amount": 5000,
      "payment_currency": "usd"
    }
  }
}