Browse documentation

REST API

Everything the booking page can do, over HTTP, with an API key. The same handlers and the same validation, so the API cannot behave differently from the app.

Authentication

Create a key in Settings → API keys. It is shown once and only a hash is stored, so copy it then. Send it as a bearer token:

curl https://calfree.dev/v1/event-types \
  -H "Authorization: Bearer cf_live_..."

A key acts as you, and scopes everything to your own data. Revoking a key in the same screen takes effect immediately, which is the reason to give an agent its own key rather than sharing one.

No cross-origin headers are sent unless the operator has listed your origin in API_CORS_ORIGINS, so a browser on another domain cannot read a response by default. The API is designed for servers and agents either way: an API key shipped to a web page is a leaked API key.

Endpoints

The full document lives at /v1/openapi.json and is generated from the same Zod schemas the endpoints validate with, so it is never out of date.

Event types

  • GET/v1/event-typesList your event types
  • POST/v1/event-typesCreate an event type
  • PATCH/v1/event-types/{id}Update an event type

Slots

  • GET/v1/slotsList open slots

Bookings

  • GET/v1/bookingsList your bookings
  • POST/v1/bookingsCreate a booking
  • POST/v1/bookings/{id}/cancelCancel a booking
  • POST/v1/bookings/{id}/rescheduleMove a booking to a new slot

Availability

  • GET/v1/availabilityRead your weekly hours and date overrides
  • PUT/v1/availabilityReplace your weekly hours and date overrides

Meta

  • GET/v1/openapi.jsonThis API's OpenAPI document

Idempotency

Every write accepts an Idempotency-Key header of 8 to 128 characters. Repeating a request with the same key within 24 hours returns the original response instead of acting twice — which is what makes a retry after a timeout safe.

curl -X POST https://calfree.dev/v1/bookings \
  -H "Authorization: Bearer cf_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type_id": "...",
    "start": "2030-01-07T10:00:00-05:00",
    "invitee_name": "Sam",
    "invitee_email": "[email protected]"
  }'

A replayed response carries Idempotent-Replayed: true, so you can tell a repeat from a fresh write. Keys are 8 to 128 printable characters without spaces, and are scoped to your key — two API keys can use the same string without colliding.

Reusing a key with a different body returns 422 idempotency_conflict, because that is a bug rather than a retry. A request still in flight under the same key returns 409 request_in_progress with a Retry-After header.

Rate limits

  • With a key: 300 requests a minute, per key.
  • Public calls: 60 a minute, per IP.
  • Booking attempts: 10 a minute, per IP.

Over the limit returns 429 with rate_limited. Behind a proxy, the limit is per client IP, so the proxy must set X-Forwarded-For or every caller will look like one very busy client.

Errors

Errors are JSON with a stable code, so you can branch on the code rather than parse a sentence:

{
  "error": {
    "code": "slot_unavailable",
    "message": "That time was just taken. Fetch slots again and pick another."
  }
}

For validation_failed, a details array says which fields were wrong. The complete set of codes:

  • validation_failed
  • unauthorized
  • not_found
  • slot_unavailable
  • link_unavailable
  • rate_limited
  • conflict
  • booking_cancelled
  • booking_started
  • bookings_paused
  • calendar_unavailable
  • verification_required
  • idempotency_conflict
  • request_in_progress
  • method_not_allowed
  • internal_error

Times

Send and expect ISO 8601 with an offset, such as 2030-01-07T10:00:00-05:00. Slot queries take a time_zone so the results are expressed in the zone you care about. Pass a start time straight back from /v1/slots to /v1/bookings without reformatting it.

Next

If the caller is an AI agent rather than your own code, the MCP server wraps these same endpoints as tools and is less work to wire up.