Developers & agents

Everything you can do on this site you can do over plain JSON. Taking part in someone else's event needs no account and no key.

How authorization works

Capability URLs are the credential. There are no API keys on the participant side:

Give an agent a share link and it can answer on its owner's behalf. There is no OAuth flow, no consent screen and nothing to provision.

Tokens are shown once. The admin token and edit tokens are returned in the response body of the request that creates them and are never recoverable afterwards. Store them the moment you receive them, and treat them like passwords: anyone holding an admin token is the organizer.

Creating an event

Creating an event needs a credential, to prevent abuse. Voting, viewing and finalizing do not. Two ways to pass:

Turnstile rejects non-browsers, so agents use a token instead. Mint one from your account page.

curl -sS https://whenwhen.io/api/events \
  -H 'Authorization: Bearer ww_...' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 6f1c9a2e-b4d5-4a71-9c33-8e2f7a10b5cd' \
  -d '{
    "title": "Design review",
    "durationMin": 60,
    "tz": "America/Chicago",
    "options": [1785312000, 1785398400, 1785484800]
  }'

{"eventId":"k3Ln8pQx2vRa","adminToken":"...","expiresAt":1786176000}

options are proposed start times in epoch seconds. Duplicates are collapsed server-side; each must fall between 24 hours ago and the caller's scheduling horizon, which is 60 days ahead with no account, 180 days on a free account and 400 days on Pro. Share https://whenwhen.io/e/{eventId} with participants and keep the admin token to yourself.

A token creates on its account's tier, exactly as the browser would. Without a token, send turnstileToken in the body instead and the event lands on the no-account tier. Slot counts and event lifetimes per tier are on the pricing page.

Events you created before you had a token

Post the admin tokens of events you created without an account and they join your account. Each is re-measured on your tier, counted from when it was created rather than from now, so a 10-day event becomes a 30-day one on a free account or a year on Pro.

curl -sS https://whenwhen.io/api/events/claim \
  -H 'Authorization: Bearer ww_...' \
  -H 'Content-Type: application/json' \
  -d '{"adminTokens": ["...", "..."]}'

{"claimed":2}

Expired, unknown and already-owned tokens are skipped rather than refused, so re-sending the same list is safe. Lifetimes only ever extend: moving up a tier lengthens the events you already have, and cancelling a subscription shortens nothing.

Tokens are shown once and hashed at rest. Only a SHA-256 is stored, so a lost token is replaced rather than recovered. Revoke from the same page; revocation takes effect on the next request. A token cannot mint or revoke tokens, and cannot delete its own account. Those need a browser session.

Voting

Read the event, answer each option, submit. The share link is the authorization; no other credential is needed.

curl -sS https://whenwhen.io/api/events/k3Ln8pQx2vRa

curl -sS https://whenwhen.io/api/events/k3Ln8pQx2vRa/votes \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Ada",
    "votes": { "<optionId>": 2, "<optionId>": 1, "<optionId>": 0 }
  }'

{"participantId":"...","editToken":"..."}

Vote values are 2 = Yes, 1 = If needed, 0 = No. Options you leave out default to No, unless the event was created with requireVotes, in which case every option must carry an explicit answer. Come back later with PUT /api/events/{id}/votes/{participantId} and the edit token to change your mind.

Changing the times

Add or remove time options after the event is shared. Votes on the remaining options are kept. Requires an account; an event created without one returns 400 validation until you claim it.

curl -sS -X PATCH https://whenwhen.io/api/admin/<adminToken> \
  -H 'Content-Type: application/json' \
  -d '{"addOptions": [1786000000], "removeOptionIds": ["<optionId>"]}'

{"ok":true,"added":1,"removed":1,"votesRemoved":4,"optionCount":3}

Removing an option deletes the votes cast on it. votesRemoved reports how many were deleted.

Adding an option leaves no entry in the votes map for participants who have already voted. A missing key means they have not answered that option. Do not read it as 0.

Editing the times returns 409 closed on a finalized event; call reopen first. It is also refused once the event is past its lifetime. GET /api/admin/{adminToken} returns canEditOptions.

Password-protected events

A Pro organizer can put a password in front of an event. Reading it, downloading its calendar file and voting on it all answer 403 with "code": "locked" until you prove you know the password once.

curl -sS https://whenwhen.io/api/events/k3Ln8pQx2vRa
{"error":"This event is password protected - POST the password to ...","code":"locked"}

curl -sS https://whenwhen.io/api/events/k3Ln8pQx2vRa/unlock \
  -H 'Content-Type: application/json' \
  -d '{"password": "harbour-lantern"}'

{"viewToken":"..."}

curl -sS https://whenwhen.io/api/events/k3Ln8pQx2vRa \
  -H 'X-View-Token: <viewToken>'

Hold the viewToken and send it as X-View-Token from then on. The password crosses the wire once, so an agent that stores the token is not storing the password. The token stays valid until the organizer changes the password, which is how access is withdrawn.

Holding the share link and no password gets you the 403 and nothing else: no title, no times, no participants. The calendar download also takes the token as a ?viewToken= query parameter, because a browser following a download link cannot set a header. Everything else should use the header, which stays out of request logs.

Wrong passwords are budgeted: 20 per IP per hour, and 60 per event per calling network. Correct ones are refunded, so a whole invited group opening the same event spends neither budget. The admin token bypasses all of this, and GET /api/admin/{adminToken} returns the password itself along with canSetPassword.

Finalizing

The organizer locks in the winner with the admin token. Finalizing and the calendar file are included on every tier.

curl -sS https://whenwhen.io/api/admin/<adminToken>/finalize \
  -H 'Content-Type: application/json' \
  -d '{"optionIds": ["<optionId>"]}'

curl -sS https://whenwhen.io/api/events/k3Ln8pQx2vRa/calendar.ics

optionIds accepts more than one id, to schedule several slots at once Pro. The plan is checked when you call finalize, so upgrading enables this on events you already have.

The .ics carries one VEVENT per locked-in slot. UIDs are stable per slot, so re-downloading after a change updates the imported entry rather than adding another. POST .../reopen undoes a finalize while the event is within its lifetime.

Conventions

Idempotency

POST /api/events accepts an optional Idempotency-Key header of up to 200 printable ASCII characters. Retry with the same key and the same body inside 24 hours and you get the original 201 back - same event id, same admin token - marked Idempotency-Replayed: true, instead of a second event.

The case this covers is a create that succeeded with a response you never received. A blind retry fails outright, because Turnstile tokens are single-use.

Reusing a key with a different body is 409 conflict, never someone else's event. Keys are scoped to the caller: your API token if you sent one, otherwise your IP address. Anonymous callers behind one office or CGNAT address therefore share a key namespace, so use an unguessable key (a UUID), not retry-1. Token callers have a namespace to themselves.

Rate limits

OperationLimitWindow
Create an event (no token)20 per IPUTC day
Create an event (free token)20 per accountUTC day
Create an event (Pro token)250 per accountUTC day
Submit or edit votes120 per IPUTC hour
Wrong password on a protected event20 per IPUTC hour
Wrong password on a protected event60 per event per networkUTC hour
Read an event or the organizer view60 per IP60 seconds
Rebuild the response for one event300 per event60 seconds
Participants per event100 / 250 / 1000 by tierper event
Time slots per event10 / 25 / 100 by tierper event
Participants x time slots20,000per event
Scheduling horizon60 / 180 / 400 days by tierper time slot
Events running at once100 free / 1000 Proper account

A token meters against its account's budget instead of the per-IP one, so a fleet of agents behind one egress address is not throttled at the anonymous rate. The budget belongs to the account, so several tokens share it and revoking one does not reset it.

The two password rows count wrong guesses. A correct password refunds both budgets, so a whole invited group opening one protected event spends nothing. The per-event row is keyed on the calling network as well as the event, so a guesser cannot lock the rest of the world out of an event by getting it wrong forty times.

The two read rows work differently from the rest. The per-IP one is charged before anything is looked up. The per-event one is charged only when the response has to be rebuilt, so a conditional read or a cached one never spends it and a link in a busy group chat does not throttle itself. Both are counted per Cloudflare data centre, which bounds how fast one event can be read from one place rather than setting a global quota.

Polling without being throttled

GET /api/events/{id} and GET /api/admin/{adminToken} return a weak ETag. Send it back as If-None-Match and an unchanged event answers 304 with no body, at the cost of one row read whatever the size of the event. Build your polling loop on this.

The tag changes whenever anything in the response changes, including two things that are not edits to the event: the organizer changing plan, which moves maxApprovals, and the event passing expiresAt, which flips status to expired. A 304 therefore means the whole payload is still current.

Responses carry Cache-Control: private, no-cache, so keep a copy if you like and revalidate it against the ETag rather than reusing it directly.

There is no cap on how many events you create in total. The "Events running at once" row above is the ceiling on how many may be open at the same time, and it is higher on Pro Pro. Creating past it returns 409 conflict until an older event expires or you delete it. At 20 creates a day against a 30-day lifetime, an agent reaches the free ceiling in about a week.

Participants multiplied by time slots is capped per event as well. Below Pro the two per-tier caps never multiply past it. On Pro it means 100 time slots come with room for 200 people, and 1000 people come with room for 20 time slots. A participant arriving at a full event gets 409 conflict, and adding slots that would put an event over gets 400 validation; both messages name the number you have room for.

Questions

If you are building against this API and something is missing or unclear, send it through the support form on your account page - you get a ticket id back, and we can see which plan and limits you are working against without asking. If you do not have an account yet, write to support@whenwhen.io instead.