# Headless access — Kitemill Meeting Poll

You are a coding assistant. The user wants you to create and manage meeting
scheduling polls via the Kitemill Meeting Poll API. Follow this brief; it is
self-contained — you do not need any other documentation.

**About:** Doodle-style scheduling. A poll proposes 1-20 time slots for one
meeting. Invitees vote works/maybe/no per slot through an open link. The
organizer picks the winning slot. Times are stored UTC and displayed in
Europe/Oslo by the web UI.

**Base URL:** `https://meet.kitemill.com`

## 1. Authentication — none. Capability tokens instead.

There are no accounts, API keys, or OAuth. Authorization is per-poll via two
unguessable 128-bit tokens returned ONCE when a poll is created:

- **voteToken** — share with invitees. Grants: read poll, submit votes.
  Web page: `/p/<voteToken>`
- **adminToken** — keep private to the organizer. Grants: read votes with
  emails, decide the winning slot, close, delete. Stored only as a hash
  server-side; if lost it CANNOT be recovered — the poll can then never be
  decided or deleted (it expires with the 6-month retention policy).
  Web page: `/a/<adminToken>`

Rules for agents:
- Show the adminToken to your user immediately after creating a poll and
  tell them to save it. You will not be able to retrieve it again.
- Never put the adminToken in anything shared with invitees.
- Both tokens are URL path segments; no Authorization header is used.

## 2. Architecture

Azure Static Web Apps: static frontend + managed Functions API under `/api`.
Data in Azure Table Storage. All requests/responses are JSON
(`Content-Type: application/json`) except the ICS endpoint (text/calendar).

Limits (server-enforced):
- Poll creation: ~10 polls/hour per source IP (HTTP 429 beyond that)
- Request bodies: max 16 KB (HTTP 413)
- 1-20 slots per poll; slot duration 5-1440 minutes
- title ≤ 200 chars, organizer/name ≤ 100, description ≤ 2000, email ≤ 200

## 3. Endpoints

### Create a poll

```bash
curl -X POST 'https://meet.kitemill.com/api/polls' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "FJORD project sync",
    "description": "Agenda in invite. Teams link follows.",
    "organizer": "Thomas",
    "slots": [
      {"start": "2026-06-20T08:00:00Z", "durationMinutes": 60},
      {"start": "2026-06-21T12:30:00Z", "durationMinutes": 45}
    ]
  }'
# 201 -> {"voteToken": "...", "adminToken": "..."}
```

`start` must be an ISO-8601 instant (UTC recommended). Remember Oslo is
UTC+2 in summer: a meeting at 10:00 Oslo time is `T08:00:00Z`.

After creating, give the user BOTH links:
- Invite link: `https://meet.kitemill.com/p/<voteToken>`
- Admin link (private): `https://meet.kitemill.com/a/<adminToken>`

### Read a poll (invitee view — voter emails are hidden)

```bash
curl 'https://meet.kitemill.com/api/polls/<voteToken>'
# 200 -> {title, description, organizer, slots:[{start,durationMinutes}],
#         status: "open"|"decided"|"closed", decidedSlot: -1|index,
#         votes:[{name, responses:{"0":"yes",...}, votedAt}]}
```

### Submit or update a vote

```bash
curl -X POST 'https://meet.kitemill.com/api/polls/<voteToken>/votes' \
  -H 'Content-Type: application/json' \
  -d '{"name": "Anna", "email": "anna@example.com",
       "responses": {"0": "yes", "1": "maybe"}}'
# 200 -> {"ok": true, "confirmationEmailSent": true|false}
```

- `responses` MUST contain an entry for EVERY slot index (as string keys
  "0".."n-1"); allowed values: `yes`, `maybe`, `no`. Missing any -> 400.
- Semantics: `yes` = works for me; `maybe` = "if need be" — the voter has a
  conflict they are willing to move (a soft yes, not uncertainty); `no` =
  not possible. When reporting results to a user, count `yes + maybe` as
  feasible attendance but prefer slots with more plain `yes`.
- `email` is optional; only the organizer (admin view) ever sees it. If
  given, it must be a plausible address (else 400) and the voter receives a
  one-click confirmation email (link valid 7 days). The admin view shows
  `emailVerified` per voter. Re-voting with the same unverified address
  re-sends the confirmation at most once per hour; confirmation emails are
  capped at ~10/hour per source IP. `confirmationEmailSent` in the response
  tells you whether a mail went out on this call.
- The confirmation link (`GET /api/confirm/<token>`) is meant for the
  voter's browser, not for agents — never fetch it on the voter's behalf;
  that would defeat the verification.
- Votes upsert by lowercase `name`: voting again under the same name
  replaces the previous vote. There is no per-voter token — anyone with the
  vote link could overwrite a vote by reusing a name. Acceptable by design
  for friendly scheduling; do not use for contested decisions.
- Voting on a `decided` or `closed` poll -> 409.

### Calendar file for the decided slot

```bash
curl 'https://meet.kitemill.com/api/polls/<voteToken>/ics'
# 200 text/calendar after the poll is decided; 404 (JSON error) before.
```

### Admin: full poll (includes voter emails and the voteToken)

```bash
curl 'https://meet.kitemill.com/api/manage/<adminToken>'
```

### Admin: decide the winning slot

```bash
curl -X POST 'https://meet.kitemill.com/api/manage/<adminToken>/decide' \
  -H 'Content-Type: application/json' -d '{"slotIndex": 0}'
# 200 -> {"ok": true, "decidedSlot": 0}   (sets status to "decided")
```

### Admin: close without deciding

```bash
curl -X POST 'https://meet.kitemill.com/api/manage/<adminToken>/close'
# 200 -> {"ok": true}   (sets status to "closed"; no further votes)
```

### Admin: delete poll and all votes

```bash
curl -X DELETE 'https://meet.kitemill.com/api/manage/<adminToken>'
# 200 -> {"ok": true}   (permanent)
```

## 4. Troubleshooting

| Symptom | Diagnosis | Action |
|---|---|---|
| 404 with EMPTY body | Wrong URL path — the platform never routed to the API | Check the base URL and path spelling; note admin routes live under `/api/manage/`, not `/api/admin/` |
| 404 with JSON `{"error": "Poll not found"}` | Path is right, token is wrong or the poll was deleted/expired | Re-check the token; tokens are case-sensitive base64url |
| 400 `Missing response for slot N` | Vote body must answer every slot | Include all indices "0".."n-1" in `responses` |
| 409 `Poll is decided/closed` | Voting after the poll ended | Read the poll first; only `status: "open"` accepts votes |
| 429 on create | Per-IP rate limit (~10/hour) | Wait, or reuse an existing poll |
| ICS 404 | Poll not decided yet | Call decide first, or use it only after `status == "decided"` |

## 5. Rules for agents

- This is a PUBLIC, unauthenticated API by design. Do not invent an auth
  flow; there is none.
- Treat the adminToken like a password. Surface it to your user once,
  prominently, right after creation.
- The retention policy is 6 months of inactivity, then deletion. Don't use
  polls as long-term storage.
- Personal data: voter names (and optional emails) are stored for
  scheduling only. Don't submit more personal data than the fields ask for.
- When suggesting slot times to a user in Norway, present them in Oslo
  time but SEND them as UTC instants.

## 6. When to use

Use this API when the user wants to: schedule a meeting, find a time that
works for a group, create a scheduling poll, check poll results, pick/decide
a meeting time, or export a decided meeting to a calendar.

---
Source: https://github.com/kitemill/kitemill-meeting-poll (Kitemill AS).
This brief is served at `/api-brief.md` on the app host — re-fetch it any
time; it is updated together with the API.
