Developers / REST API

Developers

REST API

Authenticate with API keys and call every operation over HTTP.

The REST API exposes everything an integration needs: route leads, find open times, book and manage meetings, read leads and routing decisions, configure teams, booking links and routers, pull reports and register webhooks. It is the same set of operations the MCP server offers to AI agents, so anything you script here an agent can do too, and the other way round.

Every endpoint lives under /api/v1, takes and returns JSON, and authenticates with a workspace API key. Your installation also has a live reference under Developers → API reference that lists every endpoint with its parameters.

API keys

API keys belong to a workspace and carry permissions (scopes): Standard (everything except deleting and cancelling), Read only, Full access or a custom set. A request that needs a permission the key doesn't have fails with 403 insufficient_scope. Only workspace owners and admins can create or revoke keys. The scopes are listed in MCP server → Permissions; the same API also accepts OAuth access tokens issued to MCP clients.

Create a key

Go to Developers → API keys and click Create key. Name it after where it will be used ("Website backend", "Zapier", "Claude Desktop") and choose when it expires: never, in 30 days, in 90 days or in 1 year.

Copy it once

The full key is shown only once, right after you create it. It looks like cf_live_ followed by 40 random characters. Store it in your secret manager — Cauliflower keeps only a SHA-256 hash and the first 14 characters, which the key list shows so you can tell keys apart.

Revoke when you're done

The key list shows when each key was last used. Click Revoke to disable a key immediately. Expired and revoked keys are rejected with 401.

Changes made with a key show up in Settings → Audit log under the key's name.

Authentication

Send the key as a bearer token:

HTTP
GET /api/v1/me HTTP/1.1
Host: cal.example.com
Authorization: Bearer cf_live_...

X-Api-Key: cf_live_... works too, for tools that can't set an Authorization header. A missing, invalid, expired or revoked key returns:

JSON
{ "error": { "code": "unauthorized", "message": "Missing or invalid API key. Use `Authorization: Bearer cf_live_...`" } }

Keep keys on the server

The API answers cross-origin requests, but a key in browser code gives anyone its access to your workspace. To route leads from a website, use the widget or the JavaScript API, which call public endpoints that need no key.

Requests and responses

  • Base URL: https://cal.example.com/api/v1 — your APP_URL plus /api/v1.
  • Parameters: send a JSON object as the body of POST, PATCH and PUT requests. GET and DELETE take query string parameters; true and false in the query string become booleans. Path parameters always win over body and query values.
  • Field names are snake_case. Times are ISO 8601 strings; responses use UTC (2026-10-20T15:00:00.000Z).
  • IDs carry a prefix that tells you what they are: mt_ booking links, rtr_ routers, team_ teams, mtg_ meetings, lead_ leads, rte_ routing decisions, whk_ webhooks.
  • Single objects are returned as-is. Lists are wrapped: { "data": [ ... ] }.
  • Status codes: 201 for create_* operations and booking a meeting, 200 for everything else. Deletes return { "deleted": true }.

Pagination

Most lists are small and return everything. The three that can grow take paging parameters:

EndpointParametersResponse
GET /meetingspage (from 1), page_size (1–200, default 50){ "data": [...], "page": 1, "page_size": 50, "total": 312 }
GET /leadslimit (1–200, default 50), offset{ "data": [...] }, newest first
GET /routing-eventslimit (1–200, default 50), from, to{ "data": [...] }, newest first; page backwards with to

GET /meeting-types/:id/slots returns at most 500 times and searches at most 62 days per request; ask for the next window with from and to.

Errors

Errors use standard HTTP status codes and one JSON shape:

JSON
{
  "error": {
    "code": "validation_error",
    "message": "invitee.email: Invalid email address",
    "details": [{ "path": "invitee.email", "message": "Invalid email address" }]
  }
}
StatuscodeMeaning
400bad_requestThe request is understood but can't be done, e.g. "Pass meeting_type_id or routing_token" or "This router is paused".
400invalid_json, invalid_bodyThe body isn't valid JSON, or isn't a JSON object.
401unauthorizedMissing, invalid, expired or revoked API key.
404not_foundUnknown endpoint, or the resource doesn't exist in this workspace.
409conflict, no_hostsThe time was just taken, or nobody can host the meeting. Pick another time.
422validation_errorA parameter failed validation. details lists every problem with its path.
429rate_limitedToo many requests.
502apollo_errorApollo returned an error during enrichment.
500internal_errorSomething went wrong on the server. Details are in the server logs.

Rate limits

Each API key can make 600 requests per minute. Every response includes X-RateLimit-Limit and X-RateLimit-Remaining; when you run out you get 429 until the minute window resets. The counter lives in each server process, so on a multi-replica deployment the effective limit is per replica.

Endpoints

All paths are relative to /api/v1. :router accepts a router id or its slug, and :lead accepts a lead id or email address. The MCP tool with the same behavior is listed for each endpoint.

Workspace and members

MethodPathDescriptionMCP tool
GET/meThe workspace this key belongs to: id, name, slug, time zone and public booking page URL.get_workspace
GET/membersPeople in the workspace with user id, name, email, role, title, handle and active status.list_members
GET/members/:user_id/availabilityA member's weekly hours, date overrides and time zone.get_availability
PUT/members/:user_id/availabilityReplace a member's weekly hours and/or overrides, and optionally their timezone.update_availability

Teams

MethodPathDescriptionMCP tool
GET/teamsRound robin teams with members, weights and assignment counts.list_teams
GET/teams/:team_idOne team with members and distribution stats.get_team
POST/teamsCreate a team: name, distribution (round_robin, load_balanced, priority), rr_mode (flexible, strict), member_user_ids.create_team
PATCH/teams/:team_idUpdate name, description, distribution, rr_mode or load_window_days.update_team
PUT/teams/:team_id/membersReplace the roster. Each member has user_id, weight (0–100), priority and is_active.set_team_members
DELETE/teams/:team_idDelete a team. Booking links and routers that use it stop offering times.delete_team
MethodPathDescriptionMCP tool
GET/meeting-typesBooking links with their public URLs.list_booking_links
GET/meeting-types/:meeting_type_idOne booking link.get_booking_link
POST/meeting-typesCreate a booking link. Pass owner_user_id for individual hosting, team_id for round_robin or collective.create_booking_link
PATCH/meeting-types/:meeting_type_idUpdate any field. Omitted fields keep their values.update_booking_link
DELETE/meeting-types/:meeting_type_idDelete a booking link. Existing meetings are kept.delete_booking_link
GET/meeting-types/:meeting_type_id/slotsBookable start times between from and to, optionally for one host_user_id. Pass a routing_token from route_lead to get the times that routed lead can book. Each slot lists the hosts free at that time.find_available_slots

Routers

MethodPathDescriptionMCP tool
GET/routersAll routers.list_routers
GET/routers/:routerA router's form fields, flow (trigger, matching, paths, catch-all) and settings.get_router
POST/routersCreate a router from a flow. See Routers for the flow shape.create_router
PATCH/routers/:routerUpdate a router. Pass the complete flow to replace its paths; get the current one first.update_router
DELETE/routers/:routerDelete a router. Routing history is kept.delete_router
POST/routers/:router/routeRun a lead through the router. Supports dry_run.route_lead

Meetings

MethodPathDescriptionMCP tool
GET/meetingsList meetings. Filter by status (upcoming by default, past, cancelled, all), host_user_id, lead_id, source, q, from, to.list_meetings
GET/meetings/:meeting_idOne meeting with host, invitee, answers, routing info and manage URL.get_meeting
POST/meetingsBook a meeting with a meeting_type_id or a routing_token. Invites are sent to the invitee.book_meeting
POST/meetings/:meeting_id/cancelCancel with an optional reason. Removes the calendar event and notifies everyone.cancel_meeting
POST/meetings/:meeting_id/rescheduleMove to a new start_time with the same hosts.reschedule_meeting
POST/meetings/:meeting_id/statusMark a past meeting completed or no_show, or reset it to confirmed.update_meeting_status

Leads, routing and reports

MethodPathDescriptionMCP tool
GET/leadsLeads, newest first. Filter by q, status (new, routed, booked, assigned, disqualified) or owner_user_id.list_leads
GET/leads/:leadA lead with its last 20 routing decisions and meetings.get_lead
POST/leads/:lead/enrichLook the lead up in Attio and Apollo and store the result. force: true skips the cache.enrich_lead
GET/routing-eventsRouting decisions, newest first. Filter by router, outcome, from, to.list_routing_events
GET/reports/:reportAnalytics for from–to (default: the last 30 days). :report is overview, reps, routers, disqualifications, teams or handoffs.get_report

Webhooks

MethodPathDescriptionMCP tool
GET/webhooksWebhook endpoints with their events, status and consecutive failures. Secrets are not returned.list_webhooks
POST/webhooksRegister an endpoint with url, events and an optional description. The signing secret is returned once.create_webhook
DELETE/webhooks/:webhook_idDelete an endpoint.delete_webhook

See Webhooks for event types, payloads and signatures.

Route a lead

POST /routers/:router/route runs a lead through a router exactly like a website form submission would: lead matching, enrichment, paths, then the steps of the winning path. The lead must include an email; any other keys become fields you can route on.

Terminal
curl -X POST https://cal.example.com/api/v1/routers/inbound/route \
  -H "Authorization: Bearer $CAULIFLOWER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lead": {
      "email": "[email protected]",
      "name": "Jane Cooper",
      "company": "Acme",
      "company_size": "201-1000"
    },
    "page_url": "https://acme.com/demo?utm_source=google"
  }'
JSON
{
  "routing_event_id": "rte_4w9t2m8k1c0d7h3x5q",
  "outcome": "book",
  "matched_path": { "id": "path_8f2k1m0q9c3x7w4t5z", "name": "Mid-market" },
  "matched_rule": { "id": "path_8f2k1m0q9c3x7w4t5z", "name": "Mid-market" },
  "catch_all": false,
  "message": null,
  "disqualify_reason": null,
  "redirect_url": null,
  "redirect_delay_seconds": null,
  "enrichment": { "found": true, "source": "apollo", "title": "Head of Sales", "employees": 520, "industry": "computer software", "enrichedAt": "2026-10-07T14:02:11.000Z" },
  "meeting_type": { "id": "mt_2c7d0x9k4m1q8w3t6z", "title": "Product demo", "slug": "demo", "durationMinutes": 30 },
  "candidate_hosts": [
    { "user_id": "Hq3vN8rT2kLm", "name": "Alex Kim" },
    { "user_id": "Pz7wC1yD5sJe", "name": "Sam Rivera" }
  ],
  "distribution": { "mode": "round_robin", "reason": "Round robin · Sales" },
  "assigned_user": null,
  "lead_id": "lead_7m3q9x2k8c1w0t4d5z",
  "routing_token": "Vb8Kx2...",
  "booking_url": "https://cal.example.com/r/Vb8Kx2...",
  "error": null
}

outcome is one of:

OutcomeWhat happenedWhat to do
bookThe path shows a calendar.Send the lead to booking_url, or book for them with routing_token.
assignedThe path assigned an owner without a meeting.assigned_user has the owner.
disqualifiedThe path disqualified the lead.Show message; disqualify_reason says why.
redirectThe path only redirects.Send the lead to redirect_url.
no_matchNo path matched and the catch-all is empty or only notifies and updates the CRM.Handle it yourself.
errorA step failed, e.g. no available hosts.error explains what went wrong.

The router's trigger must include API & AI agents; otherwise the request fails with 400. Paused routers also return 400. A real (non-dry) run saves the lead and the routing decision, fires lead.* webhooks, and queues the path's notify and CRM steps (on calendar paths, the parts that need the booked rep run once the meeting is booked; see Path steps).

Test with dry_run

Add "dry_run": true to evaluate the router without side effects: no lead or routing decision is saved, no webhooks fire, nobody is notified and round robin counters don't move. The response has the same shape plus a trace array that shows, for every path, whether it matched and the actual value of each condition:

JSON
{
  "outcome": "book",
  "routing_token": null,
  "booking_url": null,
  "trace": [
    {
      "ruleId": "path_1",
      "ruleName": "Enterprise",
      "matched": false,
      "conditions": [{ "field": "enrichment.employees", "operator": "gte", "expected": 1000, "actual": 520, "passed": false }]
    },
    { "ruleId": "path_2", "ruleName": "Mid-market", "matched": true, "conditions": [] }
  ]
}

Dry runs never return a routing_token, so they can't be booked. Lead matching still runs, so a dry run can call Apollo for a lead it hasn't enriched before.

Book a meeting

POST /meetings books a meeting in one of two ways.

With a routing token

After route_lead returns book, you can book on the lead's behalf with the routing_token instead of sending them to booking_url. The token pins the meeting type and the candidate hosts the router chose.

Find a time

Ask for open times on the returned meeting type, passing the same routing_token. With a token, the slots endpoint returns the times this lead can book: only the hosts the router chose, under the booking link's public rules. The token decides the meeting type and hosts, so host_user_id isn't needed.

Terminal
curl "https://cal.example.com/api/v1/meeting-types/mt_2c7d0x9k4m1q8w3t6z/slots?routing_token=Vb8Kx2...&from=2026-10-12T00:00:00Z&to=2026-10-16T00:00:00Z" \
  -H "Authorization: Bearer $CAULIFLOWER_API_KEY"
JSON
{
  "timezone": "UTC",
  "count": 38,
  "slots": [
    { "start": "2026-10-12T14:00:00.000Z", "end": "2026-10-12T14:30:00.000Z", "hosts": ["Hq3vN8rT2kLm", "Pz7wC1yD5sJe"] }
  ]
}

Book it

Terminal
curl -X POST https://cal.example.com/api/v1/meetings \
  -H "Authorization: Bearer $CAULIFLOWER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "routing_token": "Vb8Kx2...",
    "start_time": "2026-10-12T14:00:00Z",
    "timezone": "America/New_York",
    "invitee": { "name": "Jane Cooper", "email": "[email protected]" },
    "notes": "Wants to see Attio sync"
  }'

The response is the full meeting, including host, meeting_url, manage_url and routing_event_id, with status 201.

A routing token can book one meeting and is valid for 7 days. The meeting is recorded as a router booking, so it counts toward the router's conversion in reports, and the meeting type's minimum notice, booking window, required questions and per-invitee limits apply just as on the hosted page. Called with the token, the slots endpoint applies the same minimum notice and booking window and ignores meeting_type_id and host_user_id, so the times it returns match what the lead could pick. Without a token, it uses the more relaxed internal rules described below. Round robin picks among the candidates who are free at the chosen time.

With a meeting type

Pass meeting_type_id (and optionally host_user_id) to book a booking link directly, for example from your own scheduling UI:

Terminal
curl -X POST https://cal.example.com/api/v1/meetings \
  -H "Authorization: Bearer $CAULIFLOWER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "meeting_type_id": "mt_2c7d0x9k4m1q8w3t6z",
    "start_time": "2026-10-20T15:00:00Z",
    "timezone": "America/New_York",
    "invitee": { "name": "Sam Lee", "email": "[email protected]", "phone": "+1 555 0100" },
    "guests": ["[email protected]"],
    "lead": { "company": "TinyCo", "company_size": "11-50" },
    "answers": { "team_size": "12" }
  }'

These bookings are treated as internal, like Handoff: they need only 5 minutes of notice, can be up to at least 120 days ahead, and required questions are optional. The meeting's source is api.

Booking rules

  • start_time should be a time returned by the slots endpoint. Availability is re-checked against fresh calendar data when you book; if the time was taken in the meantime you get 409 and should pick another.
  • Sending the same request twice (same booking link, invitee email and start time within 15 minutes) returns the meeting that was already created instead of booking a second one, so retries are safe.
  • invitee.name and invitee.email are required. guests takes up to 10 email addresses. lead stores extra attributes on the lead record.

Next steps