WhenMeet.me

WhenMeet.me for developers & AI agents

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.

MCP server (recommended for AI agents)

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

Claude.ai and ChatGPT (OAuth connector)

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, Cursor and scripts (token header)

« 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.

Tools

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.

ToolScopeWhat it does
find_common_slotsreadRanked 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_meetingwriteWrites 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_meetingreadMeeting details + proposed alternate times (the share-link UUID is the capability).
get_meeting_availabilityreadAggregated group heat-map for an existing meeting (counts only — no individual calendars).
suggest_timewritePropose a different time using one of your account addresses; the host is emailed.
respond_to_suggestionwriteHost accepts (moves the event + re-invites) or declines.
set_manual_availabilitywritePublish 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_availabilityreadRead hand-marked free slots.
list_my_meetingsreadYour group meetings and bookings in one paginated list (upcoming or past).
list_my_event_typesreadYour own booking link, link types and team links (provisions the default link on first use).
get_booking_slotsreadBookable 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_slotwriteBook 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_bookingsreadBookings on your links (upcoming or past), with who booked, answers and whether you may change them.
cancel_bookingwriteCancel a booking: the calendar event is deleted and everyone is emailed. Needs the write scope.
reschedule_bookingwriteMove 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.

OAuth 2.1 endpoints

EndpointPurpose
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-serverRFC 8414 server metadata (PKCE S256, grants authorization_code + refresh_token, public clients).
POST /oauth/registerDynamic 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/authorizeSign in + consent (client name, redirect host, scopes). PKCE S256 required; state passed through; resource (RFC 8707) must be https://whenmeet.me/mcp.
POST /oauth/tokenauthorization_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/revokeRFC 7009 revocation (a refresh token revokes the whole grant).

Authentication for agents

  1. Sign in at WhenMeet.me with Google or Microsoft (one-time, human step; the account’s email address must be verified by the provider).
  2. Create a personal access token in Settings → API tokens & MCP (or 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.
  3. Send it on API and MCP calls: 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.

HTTP API quick reference

EndpointAuthPurpose
GET /api/metoken/sessionYou + connected calendars (status: "needs_reauth" when one must be signed in again).
PUT /api/me/timezonetoken/sessionSet your account time zone ({ timezone }, IANA); also updates your booking profile’s zone.
GET /api/me/meetings?scope=upcoming|pasttoken/sessionGroup meetings you organize or were invited to + bookings you host; past ones stay listed for 30 days.
POST /api/availabilitytoken/sessionRanked 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/meetingstoken/sessionCreate meeting + event + invites (up to 50 participants, at most 60 days ahead).
GET /api/meetings/:idnoneMeeting + suggestions.
POST /api/meetings/:id/availabilitynoneAggregated group availability.
POST /api/meetings/:id/suggestnone (Turnstile) or tokenPropose another time; the host is emailed (10 per hour per meeting and email).
POST /api/meetings/:id/suggestions/:sidtoken/session (host)Accept / decline.
POST /api/meetings/:id/canceltoken/session (host)Cancel; calendar event deleted, ICS CANCEL mailed. Idempotent.
POST /api/meetings/:id/rescheduletoken/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-availabilitynoneRead / 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/sharenone ({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/confirmnone ({t})The Confirm click: publishes the pending save for everyone → { ok, email, k }; 400 invalid_token.
POST /api/ask-linkstoken/sessionSigned /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/:idsessionManage access tokens.
POST /api/auth/logout-allsessionSign out of all devices: every session cookie issued before now is rejected. Does not revoke access tokens.
POST /api/account/deletion-requestsessionAsk us to delete the account (handled by hand; confirmation emailed). 1 per 24 h, else 429.
GET·DELETE /api/connections/:idsessionPreview, then disconnect a calendar (deletes its tokens; warnings: last_connection, booking_links).
GET /api/me/availability-lookupstoken/sessionWho read your connected calendar in the last 30 days.
GET·POST /api/me/availability-blocks, DELETE …/:uidtoken/sessionBlock 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 & limits

Errors are JSON { "error": "<code>" }. The ones agents meet most:

StatusCodeWhen
401unauthenticatedNo session or token on a route that needs one.
401invalid_tokenThe Bearer token is malformed, unknown or revoked (every /api/* route and /mcp).
400bad_windowThe 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.
400invalid_participantsNo participants, more than 50, or an invalid email.
429lookup_limitYou read more than 30 other people’s connected calendars in an hour or 50 in 24 h (only new addresses count; repeats are free).
403turnstile_requiredBrowser 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 / 429turnstile_failed / rate_limitedAnonymous 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.

Booking links & team links

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.

EndpointAuthPurpose
POST /api/booking-profile/ensuretoken/sessionZero-config: create your profile + default link if missing (idempotent).
GET·PUT /api/booking-profiletoken/sessionYour link, link types, nickname, hours, timezone.
GET /api/handles/:handle[?link=]noneIs a nickname (or, with link, a team handle) free? (+ a suggestion)
POST /api/event-types, PATCH·DELETE /api/event-types/:idtoken/sessionCreate / 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/resendtoken/sessionRe-send a team invite.
GET /api/bookings?scope=upcoming|pasttoken/sessionBookings you own or attend.
GET /api/team-invites/:token, POST …/accept, POST …/declinenone / session to acceptThe /join/<token> invite flow.
GET /api/public/:handle, GET /api/public/:handle/:slugnonePublic 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/slotsnone (token adds your side)Bookable times; signed-in visitors also get othersFree for themselves + guests.
POST /api/public/:handle/:slug/booknone (Turnstile) or tokenBook: 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, …/rescheduleinvitee token or owner sessionCancel / reschedule.
GET /api/integrations, DELETE /api/integrations/:providertoken/sessionHubSpot / Salesforce / Pipedrive / Slack / webhook status + sync log (Salesforce adds installUrl for the admin’s one-time approval); disconnect.
GET /api/integrations/hubspot|salesforce|slack/connectsessionOne-click OAuth (“Connect HubSpot”, “Connect Salesforce” — PKCE, ?sandbox=1 for a sandbox org — “Add to Slack”).
PUT /api/integrations/pipedrivetoken/sessionPaste a Pipedrive personal API token (no app): bookings become a person + meeting activity.
PUT /api/integrations/slack, PUT /api/integrations/webhook, POST …/:provider/testtoken/sessionPaste a Slack webhook URL; set the signed webhook; send a test (Slack / webhook only).
POST /api/import/calendly, POST /api/import/hubspottoken/sessionPreview, then apply: true — move your Calendly / HubSpot Meetings links over.

Booking webhooks

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');

Example: find a slot and book it

« 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"}'

Example: book through someone’s link

« 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