Venn Docs

API

Venn's JSON API is served under /api by both the Cloudflare Worker and the Node server. Writes must be sent as application/json. Times are UTC milliseconds.

Endpoints

Method Path Notes
POST /api/events Create a poll → { id, adminToken }. Specific times: { mode: "options", options: [{ start, end }] } (UTC ms; up to 14 days long). Whole days: slotMinutes: 1440, with durationMinutes a multiple of 1440 to look for several days in a row. Hours that differ by day: windows: [{ date, start, end }] (minutes after midnight in the poll's time zone), with slotMinutes (default 30) and durationMinutes. Parts of the day: layout: "parts", each option a morning (9–12), afternoon (12–5) or evening (6–10). Optional location and emoji, the privacy settings below, and for the next one of a plan seriesOf. hostAnswers: true saves the organizer's own answer and returns it as participant: { id, editToken }: hostAvailability (option start → 2 or 1) if given, otherwise yes to every time offered. (The app sends it, from how the organizer marked each option. Without it, the organizer's first answer sent with x-admin-token becomes their own.)
GET /api/events/:id Poll and responses. Sending the x-admin-token header sets isAdmin; x-edit-token gets your own answer back in full.
PUT /api/events/:id Change the poll: the same body as creating one. Organizer only, and only while nobody but the organizer has answered (409 after that). The organizer's own answer keeps the times still in the poll.
DELETE /api/events/:id Organizer only
PUT /api/events/:id/settings { requireEmail?, hideNames?, hideResults? }: each privacy setting true or false → all three. Organizer only.
POST /api/events/:id/participants { name, availability, email?, copyTo? } → { id, editToken }. email is required when the poll has requireEmail. copyTo emails the person a copy.
PUT / DELETE /api/events/:id/participants/:pid Needs x-edit-token. The organizer can also delete.
POST /api/events/:id/finalize { start }, or { start: null } to clear. Organizer only.
GET /api/events/:id/event.ics Calendar file for the picked time
PUT / DELETE /api/events/:id/image The poll's own picture, in place of its emoji: { icon, card? }, each base64 or a data: URL, → { image: { v, card, type } }. DELETE goes back to the emoji. Organizer only. See The poll's picture.
GET /api/events/:id/icon.jpg, card.jpg The poll's picture and its link-preview card, for anyone with the link. Add ?v= with image.v to have it cached for good.
GET / PUT /api/events/:id/notifications Email and webhook settings. Organizer only. See Notifications.
POST /api/events/:id/notifications/test Send a test webhook. Organizer only.
GET /api/config Which optional features (email, calendars, AI, usage stats) this server has turned on
POST /api/usage A usage count from the browser: { name, props }. Only known events are kept; see Usage stats
GET /api/stats?days=30 Usage stats, with authorization: Bearer <STATS_TOKEN>. See Usage stats
POST /api/ai/draft-poll { prompt, timezone } → a draft poll. See AI features.
POST /api/events/:id/ai/:kind summary and message (organizer only), answer (anyone)

Privacy settings

Organizers can switch these on when creating a poll (POST /api/events) or later (PUT /api/events/:id/settings). The server enforces them, so hidden data never reaches other guests:

Setting What it does
requireEmail Everyone who answers must send an email. Only the organizer sees it: each response has an email field for the organizer, and for the person who gave it.
hideNames Other guests see each response named Guest 1, Guest 2… The answers still show, so everyone can see which dates work.
hideResults Other guests get only their own response, plus responses: how many people have answered. The picked date still shows once the organizer picks it.

The next one of a plan

Book clubs, run clubs and game nights come round again. Once the date is picked, the organizer's "Plan the next one" opens /new/pick?copy=<id>&next (or /new/free): the poll copied, with dates that have passed dropped, or all moved on by whole weeks when none are left, at the same local times whatever daylight saving does. The code is in public/copy-poll.js. A plain copy (?copy=<id>, "Duplicate poll") does the same without &next.

Field What it does
seriesOf The id of the poll this one follows on from, sent by the app for &next. If that poll no longer exists it's left out. Kept when the poll is edited.

Its organizer can then bring last time's group back from the new poll's page. Polls used to say how often they repeated (repeat), with an email to the organizer when it was time; that was taken out, and repeat is now ignored.

Availability

availability maps each option's start time (UTC ms) to 2 (yes) or 1 (if needed). Anything missing means no.

{ "name": "Ana", "availability": { "1793804400000": 2, "1793818800000": 1 } }

The poll's picture

An organizer can give a poll any emoji (emoji when creating or changing it: one emoji, nothing else) or a picture of their own. The web app makes the picture in the browser: the middle square at 512 × 512, and a 1200 × 630 card with the poll's name for link previews, both JPEG. It uploads them with PUT /api/events/:id/image once the poll exists, and again when the poll is edited (so the card has the new name) or copied.

Authentication

There are no accounts. Two secrets, both returned once when they're created, prove who you are:

The server stores only SHA-256 hashes of these tokens. In the web app they live in the URL fragment and the browser's local storage.

Errors

Errors come back as JSON with an HTTP status: { "error": "This poll does not exist or was deleted" }. Validation problems are 400, missing permissions 403, and unknown polls 404.

Edit this page on GitHub