The basics
- Every endpoint is under
https://whenabouts.me/v1. The address people answer on,whenabouts.link, doesn't serve the API. - Requests and answers are JSON. Send
Accept: application/json. - Call it from a server. It sends no CORS headers, so a browser on another site can't call it, and a key in a web page would be readable by anybody anyway.
- Times are UTC, written
2026-10-10T14:00:00Z. Each page also carries its own time zone, as an IANA name likeAmerica/Toronto. - The
/v1in the address is the version.
Keys
Sign in and open "Keys and the API" from your pages. Give a key a label you'll recognize later, choose what it can do, and press the button. The key is shown once. We keep a fingerprint of it, not the key, so if you lose it, make another.
- "Read what I've made" works on every plan.
- "Read and change things" needs a paid plan, both when you make the key and every time it's used. If the plan ends, the key goes on reading and stops writing.
- What a key can do is fixed when it's made. To go from reading to writing, make a new key.
- You can have 2 keys at once on Free and 10 on a paid plan. Revoked keys don't count.
- "Revoke" stops a key at once. Each key shows when it was last used, to the day, so you can see which ones are idle.
- Each time a key is made, we email your account's address its label and what it can do, never the key, so you'd know if somebody else made it.
Sending the key
Send it in the Authorization header, as a bearer token. A key anywhere else, such as in the address, is ignored, because an address ends up in browser history, referrers and logs.
curl https://whenabouts.me/v1/events \ -H "Authorization: Bearer wa_..." \ -H "Accept: application/json"
A missing, mistyped or revoked key, or the key of an account that's been deleted, gets 401 with WWW-Authenticate: Bearer. A 401 doesn't count toward your daily requests.
Whose pages a key can see
A key sees the pages on its own account, and the pages of any account that has added you to help run them, for as long as that account's plan has a place for you. Pages made without an account can't be reached through the API.
Writing, and the daily count, go by the plan of the account the key belongs to. A page made through the API goes on that account.
Reading
In the paths below, :id is a page's id, its short link ending, or its own link ending if it has one. Endings ignore case. Deleted pages aren't found; archived ones are.
| Request | What comes back |
|---|---|
GET /v1/events | { events }: the 100 pages with the most recent activity that the key can see, newest first. Archived pages, and the single shifts a shift schedule makes, are left out. |
GET /v1/events/:id | { event, slots }. slots are a sign-up sheet's rows; for every other kind it's an empty list. |
GET /v1/events/:id/responses | { responses }: every current answer, oldest first. |
GET /v1/events/:id/results | On a grid, the best times (below). On a sheet, how many answered and each row with how many it's taken. On a head count, how many answered, and nothing more. |
GET /v1/rosters | { rosters }: your own lists of people, each with id, name, category and createdAt. Never the people on them. |
GET https://whenabouts.me/v1/events
Authorization: Bearer wa_...
Accept: application/json
200 OK
RateLimit-Limit: 20000
RateLimit-Remaining: 19874
RateLimit-Reset: 41520
{"events":[{"id":"01K6G7Z3QH8V4N2R5T9W0XJ6MB","kind":"sheet",
"slug":"b7kq2m9xzt","title":"Bake sale, Saturday",
"description":"Bring something wrapped and labelled.",
"category":"school","organizerName":"Priya",
"timezone":"America/Toronto","status":"open","responseCount":12,
"createdAt":"2026-09-28T15:04:11Z","closesAt":"2026-10-09T23:00:00Z",
"url":"https://whenabouts.link/e/b7kq2m9xzt"}]}A grid's results look like {"kind":"grid","answered":7,"best":[{"dayIndex":1,"startCell":10,"endCell":13,"count":6,"missing":["Alex"]}]}. There are up to five, best first. startCell and endCell count every time on the grid from the first, day after day, and both are included. Each is the same people free all the way through. missing names who can't make it, and is left empty once the number of times on the grid multiplied by the number of answers passes 20,000.
What comes back
A page has id, kind (grid, sheet, rsvp, or shifts for a single shift fetched by id), slug, title, description, category, organizerName, timezone, status (open, closed or suspended), responseCount, createdAt, closesAt and url, the link people answer on. A page whose closing date has passed still says open, so compare closesAt with the time.
A row has id, position, label, description, startsAt, endsAt, location, capacity and taken.
An answer has id, name, note, createdAt, updatedAt and payload. On a head count the payload is like {"v":1,"answer":"Yes","guests":2,"custom":"Vegetarian"}, with answer one of Yes, No or Maybe. On a grid it carries the times picked in a compact encoded form.
Email addresses are never in anything the API returns, on any plan. Neither are private links, PINs or a page's settings. If you need addresses, download the spreadsheet from the page's console.
Making and changing pages
These need a key made with "Read and change things" and a paid plan. Without both, they answer 403, whether or not the page exists.
| Request | What it does |
|---|---|
POST /v1/events | Makes a grid, a sign-up sheet or a head count on your account. Answers 201 with { event }. No private link comes back; manage it from your dashboard. |
PATCH /v1/events/:id | Changes the title, and only the title: {"title":"…"}. Everything else is changed on the page. |
POST /v1/events/:id/close | Stops it taking answers, the same as "Stop taking responses". It doesn't lock in a time. No body. |
POST /v1/events/:id/reminders | On a head count, asks the addresses you added on the console that haven't been asked or answered. Answers { asked, considered }. Once a day per page, shared with the console's button, and each address is only ever asked once. The body, if any, is {"audience":"not_responded"}. |
POST /v1/events takes the same things the forms do:
- Always:
kind(grid,sheetorrsvp),title(up to 80 characters; longer is cut),organizerName,timezoneandsettings. Optionallydescription,organizerEmail(where the page's emails go; never returned),categoryandclosesAt. - A grid's
settings:mode(datesorweekdays),days(up to 31 dates) andweekdays(0 for Sunday to 6), one of them filled in for the mode and the other an empty list,windowStartandwindowEndasHH:MM,slotMinutes(5 to 240),hideNamesUntilClosedandallowNotes. Up to 1,500 times in all. - A sheet's
settings:slotStyle(times,days,itemsorshifts),requireEmail,allowMultipleClaims,allowNotesandhideNames; andslots, at least one, each with alabeland optionallycapacity(1 to 999),startsAt,endsAt,locationanddescription. Up to 500 rows. - A head count's
settings:startsAt, optionallyendsAt, andallDay,allowGuests,maxGuestsPerResponse(0 to 20),hideNamesandremind24h; optionallylocation,mapUrl,guestCapandcustomQuestion.
POST https://whenabouts.me/v1/events
Authorization: Bearer wa_...
Content-Type: application/json
{"kind":"sheet","title":"Bake sale, Saturday","organizerName":"Priya",
"timezone":"America/Toronto","category":"school",
"settings":{"slotStyle":"items","requireEmail":false,
"allowMultipleClaims":true,"allowNotes":true,"hideNames":false},
"slots":[{"label":"Cookies","capacity":3},
{"label":"Lemonade stand","startsAt":"2026-10-10T14:00:00Z",
"endsAt":"2026-10-10T16:00:00Z","capacity":2}]}
201 Created
{"event":{"id":"01K6...","kind":"sheet","status":"open","responseCount":0,...}}A new page, and a changed title, is checked for scams the same way the forms' are, and a page that looks like one is made paused, with status suspended.
Errors
Every error is the same shape:
{"type":"about:blank","title":"Not found","status":404,
"detail":"We can't find that."}| Status | When |
|---|---|
| 400 | The body of a new page isn't JSON. |
| 401 | No usable key. |
| 403 | Writing without a paid plan, or with a key that can only read. The detail says which. |
| 404 | No such page, or one this key can't see (those two look the same on purpose), or no such endpoint. |
| 409 | The page is paused, a sheet has more rows than the plan allows, or asking for answers can't happen (not a head count, or nobody left to ask). |
| 422 | Something in the body isn't valid. The detail describes the first problem found. |
| 429 | Over today's requests, or asking for answers more than once a day on one page. |
| 500 | Something went wrong at our end. The detail has a reference to quote if you write to us. |
Rate limits
Each account has 1,000 requests a day on Free and 20,000 on a paid plan, shared by all its keys. The day runs from midnight to midnight UTC. Every request with a usable key to an endpoint that exists counts, the ones that end in an error included.
Answers from an endpoint that accepted your key carry three headers. A 500, and the 409 for too many rows, don't.
RateLimit-Limit: the daily number.RateLimit-Remaining: what's left today.RateLimit-Reset: seconds until midnight UTC.
Over the limit, the answer is 429 with Retry-After in seconds. If a plan ends, the account drops to the free number right away.
What it doesn't do yet
- No
Idempotency-Key. Version 1 doesn't support one. If aPOST /v1/eventstimes out and you send it again, you can end up with two pages. Before retrying, checkGET /v1/eventsfor the one you meant to make. - No paging or filters.
GET /v1/eventsstops at the 100 most recently active pages. - No grid settings. Nothing returns a grid's dates, hours or how long each time is, so a grid's results can't be turned into clock times from the API alone.
- No head-count totals in results, and no record of who took which row on a sheet. Answers carry what each person said, so you can count them yourself.
- Little editing. Only a page's title can be changed, and pages can't be deleted, reopened or locked in through the API.
If you're building something and one of these is in the way, write to us. Webhooks tell you when things happen, so you don't have to poll.
Didn't find what you were after? The rest of the help pages might have it, or write to us and somebody will answer.