For the complete documentation index, see llms.txt.

Common Integration Patterns

Practical patterns for the three most common integration challenges: calendar sync, webhook handling, and timezone-aware scheduling.


Calendar sync

Vennio reads and writes calendar events via connected Google or Microsoft calendars. The connection is per-user and uses OAuth 2.0.

Connecting a calendar (OAuth flow)

To connect a user's calendar, redirect them to the Vennio OAuth initiation endpoint. Vennio handles the provider OAuth consent screen and exchanges tokens internally.

GET https://api.vennio.app/v1/calendars/google/connect?redirect_uri=https://yourapp.com/callback&state=USER_ID

Redirect your user to this URL. After they grant consent, Google redirects to Vennio's callback endpoint. Vennio exchanges the code for tokens internally, then redirects your user back to redirect_uri:

# Vennio receives the redirect from Google at:
GET https://api.vennio.app/v1/calendars/google/callback?code=AUTH_CODE&state=STATE_TOKEN

# No action required — Vennio exchanges the token, stores the connection,
# and redirects the user to your redirect_uri with a success flag:
https://yourapp.com/callback?google_connected=true

For Microsoft calendars, use /v1/calendars/microsoft/connect and /v1/calendars/microsoft/callback instead.

Listing connected calendars

After connection, list calendars to confirm what's synced and which are selected for availability:

curl https://api.vennio.app/v1/calendars \
  -H "Authorization: Bearer $VENNIO_API_KEY"
{
  "calendars": [
    {
      "id": "cal_abc123",
      "name": "Work",
      "provider": "google",
      "primary": true,
      "selected": true
    },
    {
      "id": "cal_def456",
      "name": "Personal",
      "provider": "google",
      "primary": false,
      "selected": false
    }
  ],
  "providers": [
    { "name": "google", "connected": true, "calendar_count": 2 }
  ]
}

Triggering a manual sync

Vennio syncs calendars automatically on a schedule. Trigger a manual sync when you need up-to-date availability immediately:

curl -X POST https://api.vennio.app/v1/calendars/sync \
  -H "Authorization: Bearer $VENNIO_API_KEY"
{
  "events_synced": 24,
  "last_sync": "2026-04-11T10:05:00Z"
}
Sync frequency

Automatic syncs run every 15 minutes. Manual sync is throttled — if called within 60 seconds of the last sync, Vennio returns the cached last_sync time instead of triggering again.

Disconnecting a calendar

curl -X DELETE "https://api.vennio.app/v1/calendars/disconnect?provider=google" \
  -H "Authorization: Bearer $VENNIO_API_KEY"

This removes all tokens and stops syncing for that provider.


Webhook setup

Webhooks let your app react to booking events in real time — no polling required.

Register a webhook

curl -X POST https://api.vennio.app/v1/webhooks \
  -H "Authorization: Bearer $VENNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/vennio",
    "events": ["booking.created", "booking.cancelled", "booking.updated"],
    "description": "Production booking events"
  }'
{
  "id": "wh_abc123",
  "url": "https://yourapp.com/webhooks/vennio",
  "events": ["booking.created", "booking.cancelled", "booking.updated"],
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxx",
  "created_at": "2026-04-11T10:00:00Z"
}
Save the secret immediately

The webhook signing secret is shown once. Store it as VENNIO_WEBHOOK_SECRET in your environment. You cannot retrieve it after creation — only rotate it.

Verify webhook signatures

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

import crypto from 'crypto'
import express from 'express'

const app = express()

// IMPORTANT: Use raw body, not parsed JSON
app.post('/webhooks/vennio', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-webhook-signature']
  const secret = process.env.VENNIO_WEBHOOK_SECRET

  const expected = crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex')

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  const event = JSON.parse(req.body)

  switch (event.type) {
    case 'booking.created':
      console.log('New booking:', event.data.booking.id)
      // Provision meeting room, notify team, update CRM...
      break
    case 'booking.cancelled':
      console.log('Booking cancelled:', event.data.booking.id)
      break
    case 'booking.updated':
      console.log('Booking rescheduled:', event.data.booking.id)
      break
  }

  res.json({ received: true })
})

Event payload structure

All events share a consistent envelope:

{
  "id": "evt_abc123",
  "type": "booking.created",
  "created_at": "2026-04-11T10:00:00Z",
  "data": {
    "booking": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "confirmed",
      "customer_name": "Jane Doe",
      "customer_email": "customer@example.com",
      "start_time": "2026-05-01T09:00:00Z",
      "end_time": "2026-05-01T09:30:00Z"
    }
  }
}

Available events

Event Trigger
booking.created A booking was made
booking.updated A booking was rescheduled
booking.cancelled A booking was cancelled
consent.granted A user granted calendar access
consent.revoked A user revoked calendar access

Retry schedule

Vennio retries failed deliveries (non-2xx or timeout) with exponential backoff:

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

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

Local testing

Use ngrok or Cloudflare Tunnel to expose a local endpoint during development:

# Start your local handler on port 3000
node webhook-handler.js

# In another terminal, expose it
ngrok http 3000

# Register the ngrok URL as your webhook endpoint
curl -X POST https://api.vennio.app/v1/webhooks \
  -H "Authorization: Bearer $VENNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://abc123.ngrok.io/webhooks/vennio", "events": ["booking.created"]}'

Timezone handling

All times in the Vennio API are ISO 8601 strings. Times stored and returned are always in UTC. Timezone conversion is your responsibility at the presentation layer.

Always send UTC times

When creating a booking, send start_time and end_time in UTC:

# Correct — UTC
curl -X POST https://api.vennio.app/v1/bookings \
  -H "Authorization: Bearer $VENNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "business_id": "YOUR_BUSINESS_ID",
    "customer_email": "customer@example.com",
    "customer_name": "Jane Doe",
    "start_time": "2026-05-01T14:00:00Z",
    "end_time": "2026-05-01T14:30:00Z"
  }'

Query availability in a specific timezone

The availability endpoint accepts a timezone parameter. Returned slots use the local timezone offset for display, but the underlying times are always UTC-equivalent:

curl "https://api.vennio.app/v1/availability/slots?business_id=YOUR_BUSINESS_ID&duration_minutes=30&timezone=America%2FNew_York" \
  -H "Authorization: Bearer $VENNIO_API_KEY"
{
  "slots": [
    { "start": "2026-05-01T09:00:00-04:00", "end": "2026-05-01T09:30:00-04:00" },
    { "start": "2026-05-01T10:00:00-04:00", "end": "2026-05-01T10:30:00-04:00" }
  ]
}

You can pass the returned start and end values directly to POST /v1/bookings — the offset is preserved.

Converting user-local times to UTC in Node.js

// Using the Temporal API (Node.js 18.14+ / polyfill required in older versions)
import { Temporal } from '@js-temporal/polyfill'

function toUtcString(localDateTimeString, ianaTimezone) {
  const zdt = Temporal.ZonedDateTime.from(`${localDateTimeString}[${ianaTimezone}]`)
  return zdt.toInstant().toString() // "2026-05-01T14:00:00Z"
}

const startUtc = toUtcString('2026-05-01T10:00:00', 'America/New_York')
// → "2026-05-01T14:00:00Z"
// Using date-fns-tz (stable alternative)
import { zonedTimeToUtc } from 'date-fns-tz'

const startUtc = zonedTimeToUtc('2026-05-01 10:00:00', 'America/New_York').toISOString()
// → "2026-05-01T14:00:00.000Z"

Displaying times to end users

Always store and compare times in UTC. Convert to the user's local timezone only when displaying:

import { formatInTimeZone } from 'date-fns-tz'

function displayBookingTime(utcString, userTimezone) {
  return formatInTimeZone(new Date(utcString), userTimezone, 'MMM d, yyyy h:mm a zzz')
}

displayBookingTime('2026-05-01T14:00:00Z', 'America/New_York')
// → "May 1, 2026 10:00 AM EDT"

displayBookingTime('2026-05-01T14:00:00Z', 'Europe/London')
// → "May 1, 2026 3:00 PM BST"
Recommended library

date-fns-tz is a reliable, tree-shakeable option for timezone conversion. The native Temporal API is available in Node.js 18.14+ via the @js-temporal/polyfill package and is preferred for greenfield projects.


Related