Developer documentation

Calemander is an OAuth 2.1 authorization server. A token is bound to one workspace, chosen by the person who authorises it, and acts as that person with their permissions. Back to the portal.

Registering

  1. Sign in and open /developers.
  2. Give a name, redirect URIs, the permissions you want, and whether your client can keep a secret.
  3. You get a client ID immediately, and a client secret once if you chose confidential. The secret is never shown again — rotate it if you lose it.
  4. Your application starts as pending. Nobody can connect it until Calemander approves it; the portal shows its status.

Discovery

The authorization server issuer is https://auth.calemander.com/api/auth. Its RFC 8414 metadata document is authoritative; the URLs below are what it currently contains.

metadata
https://auth.calemander.com/.well-known/oauth-authorization-server/api/auth
authorize
https://auth.calemander.com/api/auth/oauth2/authorize
token
https://auth.calemander.com/api/auth/oauth2/token
revoke
https://auth.calemander.com/api/auth/oauth2/revoke
register (DCR)
https://auth.calemander.com/api/auth/oauth2/register

Each workspace publishes its own RFC 9728 protected-resource metadata, which names this authorization server:

https://<workspace>.calemander.com/.well-known/oauth-protected-resource/mcp
https://<workspace>.calemander.com/.well-known/oauth-protected-resource/api/v1

Send the matching resource identifier as the RFC 8707 resource parameter. Identifiers are compared as exact strings, and a token issued for one is refused on the other.

A token issued for one identifier is refused on the other, and /mcp refuses a token that carries no resource at all. The REST API still accepts one.

There is no /.well-known/openid-configuration. Access tokens are opaque and there is no ID token, so there is no OpenID Provider to describe.

Redirect URIs

  • Matched exactly. No wildcards, no prefix matching.
  • https only, except loopback: http://127.0.0.1/… and http://localhost/… are accepted for native apps.
  • For loopback, the port is ignored at authorization time, so a native app may listen on an ephemeral port. Register the path, not the port.
  • Register either localhost or 127.0.0.1: we store both spellings, and an incoming localhost redirect is normalised to 127.0.0.1 so the port rule applies to it too.
  • No fragments — the fragment never reaches your server.

Permissions

Scopes are resource:action strings with no implication ladder: bookings:write does not grant bookings:read. Ask for both if you need both. What a connection actually grants is bounded by the connecting person's role, so a member grants less than an admin; the consent screen shows them the difference.

Bookings

  • bookings:read — List your bookings, including each attendee’s name, email and notes.
  • bookings:write — Create bookings on your event types, change their status (confirm, cancel), and mark no-shows.

Event types

  • event_types:read — List your event types and their settings.
  • event_types:write — Create event types and edit their name, slug, duration, seats, location and check-in settings.

Availability

  • availability:read — Read open slots for your event types by date.

Contacts

  • contacts:read — List your contacts: names, emails, phone numbers, companies and booking counts.

Scheduling links

  • scheduling_links:read — List your single-use scheduling links.
  • scheduling_links:write — Create single-use scheduling links for your event types.

Webhooks

  • webhooks:read — List your webhook subscriptions and their URLs.
  • webhooks:manage — Create and delete webhook subscriptions for your own bookings. — Organization-wide subscriptions (every host’s bookings) can only be created by a workspace admin.

Check-in

  • check_in:read — Read today’s group sessions and who has checked in.
  • check_in:write — Check attendees in, add walk-ins, and undo a check-in.
  • check_in:manage — Mark no-shows and close or reopen a session.

Booking page appearance

  • theme:read — Read your booking page’s colors, font and layout settings.
  • theme:write — Change your booking page’s colors, font, background and button shape, and read a website you name to propose them. Cannot write custom CSS.

Booking requirements

  • requirements:read — Read what a booking is still waiting on, and the requirements set on an event type.
  • requirements:write — Clear, waive, verify or send back a booking’s requirements, and set which requirements an event type asks for. A booking confirms itself when its last requirement clears.

offline_access is not a permission: it asks for a refresh token. Refresh tokens rotate, and replaying one outside a short retry window revokes the whole family.

Rate limits and budgets

Two limits apply. A per-credential limit of 60 requests a minute bounds one token; the per-application limits below bound your whole application across every workspace that connected it. Both answer with 429 and a Retry-After header; the per-application one names which budget it was in the JSON body.

CategoryRequests / minWrites / dayConsentApproval
first_partyno limitno limitrememberedrequired
verified_partner600no limitrememberedrequired
third_party60200every timerequired
cimd60200every timedomain allowlist
dynamic60200every timerequired

Everything registered through this portal starts as third_party. Only writes count against the daily budget; reads do not.

The MCP endpoint

https://<workspace>.calemander.com/mcp is a hosted Model Context Protocol server: Streamable HTTP, stateless, POST only (GET and DELETE answer 405).

It is read-only, and each workspace has to switch it on. A workspace admin turns it on under Settings → Connected apps; until they do, the endpoint answers 404 with a JSON body saying so. No tool here creates, changes or cancels a booking, and none of them moves money.

Twelve tools are served:

whoami                     list_event_types           find_event_type
list_bookings              get_availability           get_booking_responses
list_booking_requirements  list_one_off_links         list_webhooks
get_webhook                get_theme                  list_unpaid_claims

list_contacts is not served, and contacts:read is not a scope the endpoint asks for. An assistant does not get the customer list.

A result that can carry text somebody outside the workspace typed — an attendee name, an answer to a booking question, a note on a requirement — arrives with an untrusted array of JSON paths naming exactly those values, and the server's instructions tell the assistant they are data and not instructions.

Connecting

Claude.ai: Settings → Connectors → Add custom connector, paste the URL, sign in and approve the permissions. ChatGPT: developer mode, add the same URL. Claude Code:

claude mcp add --transport http calemander https://<workspace>.calemander.com/mcp

Cursor and VS Code need a pre-registered public client_id, because our Dynamic Client Registration requires a platform-minted initial access token that those clients cannot obtain. There is no published public client id yet; this page will carry it when there is.

Limits and audit

60 requests per minute per token at /mcp, and a separate 60 per minute for the /api/v1 calls the tools make — so sixty tool calls a minute, not thirty. A 429 carries Retry-After.

Every tool call is recorded in the workspace: which tool, whether it worked, which app, when. Arguments and results are not recorded. A workspace admin reads the last fifty on the Connected apps page.

MCP clients (CIMD)

An MCP client may identify itself with a Client ID Metadata Document instead of registering: its client_id is the HTTPS URL of that document. Two conditions, both hard:

  • The document must be on an allowlisted domain. Currently: chatgpt.com, claude.ai, cursor.com. Adding a domain is a decision Calemander makes; there is no way to add one from a request.
  • It must be served over HTTPS on port 443, with no redirects, under 5 KB, within 5 seconds, and must satisfy the MCP 2026-07-28 CIMD profile — which makes client_name and redirect_uris mandatory.

CIMD clients are not reviewed by us, and the consent screen says so in those words. They see the full consent screen on every connection.

Dynamic client registration

POST https://auth.calemander.com/api/auth/oauth2/register accepts an RFC 7591 registration when it carries an initial access token issued by Calemander (Authorization: Bearer cma_iat_…). Tokens are single-use and expire after seven days. Ask us for one; registering still leaves the client pending approval.

Testing

Ask for access to the partner-review workspace rather than testing against a live one. It exists for integration review, and it is what we use to verify a partner integration ourselves. See docs/REVIEW-TENANT.md in the repository for what it contains and how to request it.

Facts worth knowing

  • PKCE with S256 is required. No other method is supported.
  • Grants are authorization_code and refresh_token. There is no client_credentials: a token with no user has no workspace, and every token here is refused without one. Use a workspace API key for server-to-server work.
  • Access tokens last one hour. Refresh tokens last 30 days and rotate on use.
  • Tokens are opaque and stored hashed, so a person revoking your app on the Connected apps page stops it on the next request, not at the next expiry.
  • Losing workspace membership stops a token too.
  • Send the token in the Authorization header. Never in a URL.