Subscribe to real-time events when bookings are created, updated, or cancelled.
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.
| 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_revokedis not a webhook event. If a recipient revokesbooking:proposeconsent between create and accept, the proposal transitions toexpiredand the originator is notified by email — no webhook is fired.
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"
}
The webhook signing secret is only shown once at creation. Use it to verify incoming payloads.
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 })
})
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.
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.resolvedFires 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"
}
}
}