Everything the WhenMeet.me UI does is available programmatically:
a documented HTTP API and a remote MCP server, both on this origin.
Machine-readable spec: /openapi.json (OpenAPI 3.1).
LLM-friendly product summary: /llms.txt.
Remote MCP server (Streamable HTTP, stateless JSON responses, protocol versions 2025-06-18, 2025-03-26 and 2024-11-05) at:
https://whenmeet.me/mcp
Add https://whenmeet.me/mcp as a custom connector — Claude.ai: Settings → Connectors → Add custom
connector; ChatGPT: Settings → Connectors → Create (developer mode) with authentication
OAuth. The client registers itself (Dynamic Client Registration), sends you to WhenMeet.me to sign
in and approve, and receives tokens bound to /mcp. Disconnect any time in
Settings → Connected apps. Full guide: MCP for AI agents.
« Claude Code »
claude mcp add --transport http whenmeet https://whenmeet.me/mcp \
--header "Authorization: Bearer $WHENMEET_TOKEN"
Create the token in Settings → API tokens & MCP.
15 tools. Every /mcp request needs a token (OAuth or a personal access token) —
there is no anonymous use. Tools with scope write change data and need a token with the write scope
(a read-only token gets an insufficient_scope tool error). Every tool carries
MCP annotations (title, readOnlyHint, destructiveHint,
idempotentHint, openWorldHint), an outputSchema and returns
structuredContent.
| Tool | Scope | What it does |
|---|---|---|
find_common_slots | read | Ranked times when all participants (up to 50) are free: live Google/Microsoft Free/Busy, added public iCloud feeds and hand-marked availability. Each read of someone else’s calendar is shown to its owner; 30 new addresses per hour, 50 per day. |
create_meeting | write | Writes the calendar event with a Meet link (Google) or Teams link (Teams-enabled Microsoft 365 work/school), emails invites with an ICS, returns the share URL. Up to 50 participants, at most 60 days ahead. |
get_meeting | read | Meeting details + proposed alternate times (the share-link UUID is the capability). |
get_meeting_availability | read | Aggregated group heat-map for an existing meeting (counts only — no individual calendars). |
suggest_time | write | Propose a different time using one of your account addresses; the host is emailed. |
respond_to_suggestion | write | Host accepts (moves the event + re-invites) or declines. |
set_manual_availability | write | Publish hand-marked free slots for an email. Your own addresses go live; anyone else’s wait until that person confirms by email ({pending: true}; confirmation emails per day: 3 per address, 20 per account). |
get_manual_availability | read | Read hand-marked free slots. |
list_my_meetings | read | Your group meetings and bookings in one paginated list (upcoming or past). |
list_my_event_types | read | Your own booking link, link types and team links (provisions the default link on first use). |
get_booking_slots | read | Bookable times on anyone’s booking link (whenmeet.me/@nickname[/slug] — pass the nickname or the whole link). Also shows which times suit your calendar. |
book_slot | write | Book one of those times: event in the host’s calendar with a Meet link (Google host) or Teams link (Teams-enabled Microsoft 365 work/school host), invites to everyone, a manage link back. Name/email default to your account. |
list_my_bookings | read | Bookings on your links (upcoming or past), with who booked, answers and whether you may change them. |
cancel_booking | write | Cancel a booking: the calendar event is deleted and everyone is emailed. Needs the write scope. |
reschedule_booking | write | Move a booking to a new start (or, without startsAt, list the bookable times for it). Needs the write scope. |
A request without credentials (including initialize and tools/list) gets HTTP 401 with
WWW-Authenticate: Bearer resource_metadata="https://whenmeet.me/.well-known/oauth-protected-resource/mcp", which starts
the OAuth flow in connectors. A token that is malformed, unknown, revoked or expired gets HTTP 401 with JSON-RPC error -32001
invalid_token and WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://whenmeet.me/.well-known/oauth-protected-resource/mcp"
— never a silent anonymous call. Server card:
/.well-known/mcp.json.
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-protected-resource[/mcp] | RFC 9728 resource metadata: resource https://whenmeet.me/mcp, authorization server, scopes read write. |
GET /.well-known/oauth-authorization-server | RFC 8414 server metadata (PKCE S256, grants authorization_code + refresh_token, public clients). |
POST /oauth/register | Dynamic Client Registration (RFC 7591), public clients only; redirect URIs must be https (http only for 127.0.0.1 / localhost); 20 registrations per hour per IP. |
GET /oauth/authorize | Sign in + consent (client name, redirect host, scopes). PKCE S256 required; state passed through; resource (RFC 8707) must be https://whenmeet.me/mcp. |
POST /oauth/token | authorization_code (single-use code, 60 s) and refresh_token (rotated on every use; reusing an old one revokes the grant). Access tokens are mn_…, valid 1 hour and only at /mcp; refresh tokens 60 days, sliding. |
POST /oauth/revoke | RFC 7009 revocation (a refresh token revokes the whole grant). |
POST /api/tokens with your browser session, body { name?, scopes?: ["read"] | ["read","write"], expiresInDays?: 30 | 90 | 365 | null }); the plaintext mn_… token is shown once.Authorization: Bearer mn_…Scopes: read covers read-only tools and GET endpoints (plus the read-only slot/ranking
POSTs); write adds creating, cancelling and rescheduling meetings and bookings, marking
availability and every other change. A call outside the token’s scope gets 403 insufficient_scope.
New tokens expire after 90 days by default (30 / 90 / 365 days or never); an expired token gets
401 invalid_token. Tokens created before scopes existed keep full access and never expire.
Revoke any time with DELETE /api/tokens/:id.
On the HTTP API, guest-side actions (reading a meeting, suggesting times, marking availability) need no
token — the share-link UUID or the email address is the capability, same as in the UI; MCP always needs a token. Marked availability is
published for everyone only for a proven address (your own with a token, or confirmed by email); times
marked through your signed ask-link are live for you only; anything else waits for that person to
confirm by email.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/me | token/session | You + connected calendars (status: "needs_reauth" when one must be signed in again). |
PUT /api/me/timezone | token/session | Set your account time zone ({ timezone }, IANA); also updates your booking profile’s zone. |
GET /api/me/meetings?scope=upcoming|past | token/session | Group meetings you organize or were invited to + bookings you host; past ones stay listed for 30 days. |
POST /api/availability | token/session | Ranked slots for up to 50 emails, timezone-aware per participant (everyone-free first, then compromise slots). Without a connected host calendar, falls back to hand-marked availability only (participants with none come back as source: "host", may look free) — never fabricated. |
POST /api/meetings | token/session | Create meeting + event + invites (up to 50 participants, at most 60 days ahead). |
GET /api/meetings/:id | none | Meeting + suggestions. |
POST /api/meetings/:id/availability | none | Aggregated group availability. |
POST /api/meetings/:id/suggest | none (Turnstile) or token | Propose another time; the host is emailed (10 per hour per meeting and email). |
POST /api/meetings/:id/suggestions/:sid | token/session (host) | Accept / decline. |
POST /api/meetings/:id/cancel | token/session (host) | Cancel; calendar event deleted, ICS CANCEL mailed. Idempotent. |
POST /api/meetings/:id/reschedule | token/session (host) | Move to startsAt (≤ 60 days); updated ICS mailed. Two simultaneous reschedules: one gets 409 conflict, and the calendar event may keep the losing time (reschedule again). |
GET·POST /api/manual-availability | none | Read / publish hand-marked free slots. Live for everyone for your own address; with a host’s ask-link k, live for that host only (scope: "host"); otherwise 202 {pending: true} and a confirmation email to that address. |
POST /api/manual-availability/share | none ({email, k, payload}) | “Let anyone who schedules with you see this”: queues a host-scoped save for everyone, confirmed by email → 202 {pending: true}. |
GET /api/manual-availability/confirm?t= | none (emailed token) | The confirmation link: opens /free with a Confirm button (writes nothing, so mail scanners can't confirm). |
POST /api/manual-availability/confirm | none ({t}) | The Confirm click: publishes the pending save for everyone → { ok, email, k }; 400 invalid_token. |
POST /api/ask-links | token/session | Signed /free?email=…&k=… links (≤ 50): what people mark there is live for you straight away, without the confirmation email. |
POST·GET /api/tokens, DELETE /api/tokens/:id | session | Manage access tokens. |
POST /api/auth/logout-all | session | Sign out of all devices: every session cookie issued before now is rejected. Does not revoke access tokens. |
POST /api/account/deletion-request | session | Ask us to delete the account (handled by hand; confirmation emailed). 1 per 24 h, else 429. |
GET·DELETE /api/connections/:id | session | Preview, then disconnect a calendar (deletes its tokens; warnings: last_connection, booking_links). |
GET /api/me/availability-lookups | token/session | Who read your connected calendar in the last 30 days. |
GET·POST /api/me/availability-blocks, DELETE …/:uid | token/session | Block an account from reading your availability (it then sees you as unavailable) / unblock. |
Times are unix milliseconds; tz is an IANA zone name used for business-hours
filtering and email formatting. Full schemas: /openapi.json.
Errors are JSON { "error": "<code>" }. The ones agents meet most:
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | No session or token on a route that needs one. |
| 401 | invalid_token | The Bearer token is malformed, unknown or revoked (every /api/* route and /mcp). |
| 400 | bad_window | The window is empty or backwards; availability reads reach more than 60 days back/ahead; or a new meeting starts more than 60 days from now. |
| 400 | invalid_participants | No participants, more than 50, or an invalid email. |
| 429 | lookup_limit | You read more than 30 other people’s connected calendars in an hour or 50 in 24 h (only new addresses count; repeats are free). |
| 403 | turnstile_required | Browser sessions only (never tokens or MCP): /api/availability would add new addresses past 15 in an hour. Resend with turnstileToken; a valid one exempts the session for an hour. |
| 403 / 429 | turnstile_failed / rate_limited | Anonymous calls without a Turnstile token; calendar/mail actions past their caller or recipient budget. |
Every read of another user’s connected calendar is shown to that user (Settings → Privacy), who can block
you; a blocked or unreadable calendar comes back as calendarStatus: "unavailable" and busy for
the whole window — never as free.
Every user who connects a calendar gets a live booking link at https://whenmeet.me/@<nickname> — no
setup: a 30-minute meeting, Mon–Fri 09:00–17:00 in their zone, a Meet link (Google host) or Teams link (Teams-enabled Microsoft 365 work/school host) on every booking, reminder
emails 24 h and 1 h before (reminders, any of 15 min–2 days, or off).
More link types live at /@<nickname>/<slug>. A team link is one link
backed by several people: mode: "all" needs everyone free (a two-person interview panel),
mode: "any" books whoever is free, in turn — each teammate inside their own working hours. A team
link can claim its own address with teamHandle (https://whenmeet.me/@acme-sales; nicknames and team
handles share one namespace). Public endpoints return times and counts only — never anyone’s calendar, hours or
member emails. Full schemas in /openapi.json.
| Endpoint | Auth | Purpose |
|---|---|---|
POST /api/booking-profile/ensure | token/session | Zero-config: create your profile + default link if missing (idempotent). |
GET·PUT /api/booking-profile | token/session | Your link, link types, nickname, hours, timezone. |
GET /api/handles/:handle[?link=] | none | Is a nickname (or, with link, a team handle) free? (+ a suggestion) |
POST /api/event-types, PATCH·DELETE /api/event-types/:id | token/session | Create / edit / delete link types (delete answers 409 has_future_bookings with count unless ?force=1); pass team: { mode, members, includeMe } for a team link, teamHandle / teamName for its own address, reminders for reminder emails. Team links report team.health and each member’s hours. |
POST /api/event-types/:id/members/:email/resend | token/session | Re-send a team invite. |
GET /api/bookings?scope=upcoming|past | token/session | Bookings you own or attend. |
GET /api/team-invites/:token, POST …/accept, POST …/decline | none / session to accept | The /join/<token> invite flow. |
GET /api/public/:handle, GET /api/public/:handle/:slug | none | Public page data: name, link types, team names. A team handle returns its one link plus aliasOf (owner handle + slug for slots and book). |
POST /api/public/:handle/:slug/slots | none (token adds your side) | Bookable times; signed-in visitors also get othersFree for themselves + guests. |
POST /api/public/:handle/:slug/book | none (Turnstile) or token | Book: calendar event + Meet (Google host) or Teams (Teams-enabled Microsoft 365 work/school host) link + invites; returns manageUrl. |
GET /api/bookings/:id?t=, POST …/slots, …/cancel, …/reschedule | invitee token or owner session | Cancel / reschedule. |
GET /api/integrations, DELETE /api/integrations/:provider | token/session | HubSpot / Salesforce / Pipedrive / Slack / webhook status + sync log (Salesforce adds installUrl for the admin’s one-time approval); disconnect. |
GET /api/integrations/hubspot|salesforce|slack/connect | session | One-click OAuth (“Connect HubSpot”, “Connect Salesforce” — PKCE, ?sandbox=1 for a sandbox org — “Add to Slack”). |
PUT /api/integrations/pipedrive | token/session | Paste a Pipedrive personal API token (no app): bookings become a person + meeting activity. |
PUT /api/integrations/slack, PUT /api/integrations/webhook, POST …/:provider/test | token/session | Paste a Slack webhook URL; set the signed webhook; send a test (Slack / webhook only). |
POST /api/import/calendly, POST /api/import/hubspot | token/session | Preview, then apply: true — move your Calendly / HubSpot Meetings links over. |
Set a URL with PUT /api/integrations/webhook (the signing secret is shown once). Every
booking.created, booking.cancelled and booking.rescheduled (plus test from
POST /api/integrations/webhook/test) is POSTed as JSON with x-whenmeet-event,
x-whenmeet-delivery (a unique id per delivery, for de-duplication) and x-whenmeet-signature: t=<unix s>,v1=<hex>, where
v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Zapier, Make and n8n can take it from there
(a sheet, a CRM without a native row…). HubSpot, Salesforce and Pipedrive are native (Salesforce: external orgs can’t connect until the customer install package ships). Payload schema:
BookingEventPayload in the spec.
« verify (Node) »
const [t, v1] = sig.split(',').map((p) => p.split('=')[1]);
const want = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
const ok = v1?.length === want.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1));
if (!ok || Date.now() / 1000 - Number(t) > 300) throw new Error('bad signature');
« a two-week window from now (the end may be at most 60 days ahead) »
FROM=$(date +%s000); TO=$(( $(date +%s) + 14*86400 ))000
« find slots »
SLOT=$(curl -s https://whenmeet.me/api/availability \
-H "Authorization: Bearer $WHENMEET_TOKEN" -H "content-type: application/json" \
-d '{"participants":["alex@example.com","sam@example.org"],
"from":'$FROM',"to":'$TO',"durationMin":30,"tz":"Europe/Warsaw"}' \
| jq '.slots[0]')
« book the top slot »
curl -s https://whenmeet.me/api/meetings \
-H "Authorization: Bearer $WHENMEET_TOKEN" -H "content-type: application/json" \
-d '{"participants":["alex@example.com","sam@example.org"],
"startsAt":'$(echo "$SLOT" | jq .start)',"endsAt":'$(echo "$SLOT" | jq .end)',
"title":"Cross-company sync","conference":"meet","tz":"Europe/Warsaw"}'
« times on whenmeet.me/@alex/intro (no auth needed) »
START=$(curl -s https://whenmeet.me/api/public/alex/intro/slots -H "content-type: application/json" \
-d '{"from":'$FROM',"to":'$TO',"tz":"Europe/Warsaw"}' | jq '.slots[0].start')
« book it (a token skips the bot check) »
curl -s https://whenmeet.me/api/public/alex/intro/book \
-H "Authorization: Bearer $WHENMEET_TOKEN" -H "content-type: application/json" \
-d '{"startsAt":'$START',"name":"Sam Example","email":"sam@example.org",
"guests":["finance@example.org"],"notes":"Pricing questions"}'
Last updated 2026-10-09 · openapi.json · llms.txt · llms-full.txt · MCP for AI agents · WhenMeet.me