Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 3e1cb86700a5e96238d7a2f1
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
public/openapi.json
{
"openapi": "3.1.0",
"info": {
"title": "When by OpenApps",
"version": "1.1.0",
"summary": "Free group scheduling polls: suggest times, collect yes, if need be or no from everyone, and pick the final time.",
"description": "When is a free Doodle alternative with no account, no key and no limit on polls. A poll's id is its secret, exactly like its link: anyone with it can read the poll, answer it and comment. The ownerKey returned when a poll is made lets the caller edit it, close it and pick the final time; send it in the x-owner-key header and keep it private. Times without an offset are read in the poll's time zone. Errors are JSON with an `error` sentence that says what to change. CORS is open.",
"contact": {
"name": "OpenApps",
"url": "https://free-apps.openapps.xyz/"
},
"license": {
"name": "Free to use"
}
},
"externalDocs": {
"description": "Guide for assistants",
"url": "https://when-free.openapps.xyz/llms.txt"
},
"servers": [
{
"url": "https://functions.bounded.sh/apps/3e1cb86700a5e96238d7a2f1/api"
}
],
"components": {
"securitySchemes": {
"ownerKey": {
"type": "apiKey",
"in": "header",
"name": "x-owner-key",
"description": "The ownerKey from POST /polls (also inside the adminUrl after #k=)."
}
},
"schemas": {
"Error": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "A sentence that says what went wrong and what to change."
}
},
"required": [
"error"
]
},
"Answer": {
"type": "string",
"enum": [
"yes",
"maybe",
"no"
]
},
"Option": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"start": {
"type": "string",
"description": "Start in the poll's time zone, for timed options."
},
"end": {
"type": "string",
"description": "End in the poll's time zone, when the option has one."
},
"date": {
"type": "string",
"format": "date",
"description": "The date, for all-day options."
},
"allDay": {
"type": "boolean"
},
"label": {
"type": "string",
"description": "Readable, with the time zone named."
},
"yes": {
"type": "integer"
},
"maybe": {
"type": "integer"
},
"no": {
"type": "integer"
},
"best": {
"type": "boolean",
"description": "True for the option(s) with the most yes, then the most if need be."
}
},
"required": [
"id",
"label",
"yes",
"maybe",
"no",
"best"
]
},
"PollSummary": {
"type": "object",
"properties": {
"poll": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"timezone": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri",
"description": "The link to send to participants."
},
"status": {
"type": "string",
"enum": [
"open",
"closed",
"decided"
]
}
}
},
"options": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Option"
}
},
"participants": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"answers": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Answer"
},
"description": "Option id to answer. A time added after someone answered has no answer from them."
}
}
}
},
"best": {
"type": "array",
"items": {
"type": "string"
},
"description": "Option ids of the best time(s); empty until someone can make a time."
},
"chosen": {
"oneOf": [
{
"type": "null"
},
{
"allOf": [
{
"$ref": "#/components/schemas/Option"
},
{
"type": "object",
"properties": {
"ics": {
"type": "string",
"format": "uri"
},
"googleCalendar": {
"type": "string",
"format": "uri"
},
"outlookCalendar": {
"type": "string",
"format": "uri"
}
}
}
]
}
],
"description": "The final time once the organizer picks it, with calendar links."
},
"comments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"text": {
"type": "string"
},
"at": {
"type": "string",
"format": "date-time"
}
}
},
"description": "Oldest first."
}
},
"required": [
"poll",
"options",
"participants",
"best",
"chosen",
"comments"
]
}
}
},
"paths": {
"/": {
"get": {
"operationId": "listRoutes",
"summary": "List the routes and where the docs are",
"responses": {
"200": {
"description": "The routes.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"about": {
"type": "string"
},
"docs": {
"type": "string"
},
"openapi": {
"type": "string"
},
"routes": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"/polls": {
"post": {
"operationId": "createPoll",
"summary": "Make a poll and get its link, admin link and owner key",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"maxLength": 120
},
"description": {
"type": "string",
"maxLength": 2000
},
"location": {
"type": "string",
"maxLength": 200
},
"timezone": {
"type": "string",
"description": "The organizer's IANA time zone. Defaults to UTC.",
"example": "America/New_York"
},
"options": {
"type": "array",
"minItems": 1,
"maxItems": 500,
"items": {
"oneOf": [
{
"type": "string",
"description": "An ISO time (\"2026-10-21T15:00:00-04:00\", or \"2026-10-21T15:00\" read in the poll's time zone), or a date (\"2026-10-23\") for all day."
},
{
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": "string",
"description": "Optional. After start, at most a week later."
}
},
"required": [
"start"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"date": {
"type": "string",
"format": "date"
},
"allDay": {
"type": "boolean",
"const": true
}
},
"required": [
"date"
],
"additionalProperties": false
}
]
}
}
},
"required": [
"title",
"options"
]
},
"example": {
"title": "Team lunch",
"location": "Cafe Rio",
"timezone": "America/New_York",
"options": [
"2026-10-27T12:00",
{
"start": "2026-10-27T15:00",
"end": "2026-10-27T16:00"
},
{
"date": "2026-10-30",
"allDay": true
}
]
}
}
}
},
"responses": {
"201": {
"description": "The poll was made. Give the person url to share and adminUrl to keep; keep ownerKey to manage it.",
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"properties": {
"pollId": {
"type": "string"
},
"url": {
"type": "string"
},
"adminUrl": {
"type": "string"
},
"ownerKey": {
"type": "string"
},
"share": {
"type": "string"
}
},
"required": [
"pollId",
"url",
"adminUrl",
"ownerKey"
]
},
{
"$ref": "#/components/schemas/PollSummary"
}
]
},
"example": {
"pollId": "GckysfetvdBJJor43QN4",
"url": "https://when-free.openapps.xyz/p/GckysfetvdBJJor43QN4",
"adminUrl": "https://when-free.openapps.xyz/p/GckysfetvdBJJor43QN4/admin#k=GZtDsR6zDA4L5XoJ9yCMvFbM",
"ownerKey": "GZtDsR6zDA4L5XoJ9yCMvFbM",
"share": "Send https://when-free.openapps.xyz/p/GckysfetvdBJJor43QN4 to the group. Keep the adminUrl and ownerKey private: they let you edit the poll and pick the final time.",
"poll": {
"id": "GckysfetvdBJJor43QN4",
"title": "Team lunch",
"description": null,
"location": "Cafe Rio",
"timezone": "America/New_York",
"url": "https://when-free.openapps.xyz/p/GckysfetvdBJJor43QN4",
"status": "open"
},
"options": [
{
"id": "NWpoik",
"start": "2026-10-27T12:00:00-04:00",
"label": "Tue, Oct 27, 12:00 PM EDT",
"yes": 0,
"maybe": 0,
"no": 0,
"best": false
},
{
"id": "B7oXVp",
"start": "2026-10-27T15:00:00-04:00",
"end": "2026-10-27T16:00:00-04:00",
"label": "Tue, Oct 27, 3:00 PM to 4:00 PM EDT",
"yes": 0,
"maybe": 0,
"no": 0,
"best": false
},
{
"id": "mXQrNm",
"date": "2026-10-30",
"allDay": true,
"label": "Fri, Oct 30, all day",
"yes": 0,
"maybe": 0,
"no": 0,
"best": false
}
],
"participants": [],
"best": [],
"chosen": null,
"comments": []
}
}
}
},
"400": {
"description": "Bad input.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "options must be a list with at least one time."
}
}
}
}
}
}
},
"/polls/{pollId}": {
"parameters": [
{
"name": "pollId",
"in": "path",
"required": true,
"description": "The poll's id: the last part of its link. Anyone with it can read and answer the poll.",
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9]{8,32}$"
},
"example": "GckysfetvdBJJor43QN4"
}
],
"get": {
"operationId": "getPoll",
"summary": "Read a poll: options with counts, everyone's answers, the best time, the final time and comments",
"responses": {
"200": {
"description": "The poll.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PollSummary"
},
"example": {
"poll": {
"id": "GckysfetvdBJJor43QN4",
"title": "Team lunch",
"description": null,
"location": "Cafe Rio",
"timezone": "America/New_York",
"url": "https://when-free.openapps.xyz/p/GckysfetvdBJJor43QN4",
"status": "open"
},
"options": [
{
"id": "NWpoik",
"start": "2026-10-27T12:00:00-04:00",
"label": "Tue, Oct 27, 12:00 PM EDT",
"yes": 2,
"maybe": 1,
"no": 0,
"best": true
},
{
"id": "B7oXVp",
"start": "2026-10-27T15:00:00-04:00",
"end": "2026-10-27T16:00:00-04:00",
"label": "Tue, Oct 27, 3:00 PM to 4:00 PM EDT",
"yes": 1,
"maybe": 0,
"no": 2,
"best": false
},
{
"id": "mXQrNm",
"date": "2026-10-30",
"allDay": true,
"label": "Fri, Oct 30, all day",
"yes": 1,
"maybe": 1,
"no": 1,
"best": false
}
],
"participants": [
{
"name": "Sam",
"answers": {
"NWpoik": "yes",
"B7oXVp": "yes",
"mXQrNm": "no"
}
},
{
"name": "Priya",
"answers": {
"NWpoik": "yes",
"B7oXVp": "no",
"mXQrNm": "maybe"
}
},
{
"name": "Jordan",
"answers": {
"NWpoik": "maybe",
"B7oXVp": "no",
"mXQrNm": "yes"
}
}
],
"best": [
"NWpoik"
],
"chosen": null,
"comments": [
{
"name": "Priya",
"text": "Tuesday works best for me.",
"at": "2026-10-19T14:00:00Z"
}
]
}
}
}
},
"404": {
"description": "No such poll.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "No poll with that id."
}
}
}
}
}
},
"patch": {
"operationId": "editPoll",
"summary": "Edit a poll (owner)",
"security": [
{
"ownerKey": []
}
],
"description": "options replaces the list; times that stay the same keep their answers. Send null to clear description or location.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"location": {
"type": [
"string",
"null"
]
},
"timezone": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"description": "An ISO time (\"2026-10-21T15:00:00-04:00\", or \"2026-10-21T15:00\" read in the poll's time zone), or a date (\"2026-10-23\") for all day."
},
{
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": "string",
"description": "Optional. After start, at most a week later."
}
},
"required": [
"start"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"date": {
"type": "string",
"format": "date"
},
"allDay": {
"type": "boolean",
"const": true
}
},
"required": [
"date"
],
"additionalProperties": false
}
]
}
}
}
},
"example": {
"location": "Room 4B",
"options": [
"2026-10-27T12:00",
"2026-10-28T12:00"
]
}
}
}
},
"responses": {
"200": {
"description": "The edited poll.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PollSummary"
}
}
}
},
"401": {
"description": "No key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "Send the poll's owner key in the x-owner-key header."
}
}
}
},
"403": {
"description": "Wrong key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "That owner key does not match this poll."
}
}
}
}
}
}
},
"/polls/{pollId}/votes": {
"parameters": [
{
"name": "pollId",
"in": "path",
"required": true,
"description": "The poll's id: the last part of its link. Anyone with it can read and answer the poll.",
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9]{8,32}$"
},
"example": "GckysfetvdBJJor43QN4"
}
],
"post": {
"operationId": "answerPoll",
"summary": "Answer a poll, or change answers made through the API",
"description": "Each call without voteId adds a new participant row and returns its voteId. Send the same voteId to change those answers. Options left out count as no. A closed poll refuses answers.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 60
},
"answers": {
"type": "object",
"additionalProperties": {
"type": "string",
"enum": [
"yes",
"maybe",
"if need be",
"no"
]
},
"description": "Keys are option ids, or start times or dates that match an option."
},
"voteId": {
"type": "string"
}
},
"required": [
"name",
"answers"
]
},
"example": {
"name": "Sam",
"answers": {
"NWpoik": "yes",
"2026-10-27T15:00": "maybe"
}
}
}
}
},
"responses": {
"201": {
"description": "Answers added.",
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"properties": {
"voteId": {
"type": "string"
}
}
},
{
"$ref": "#/components/schemas/PollSummary"
}
]
}
}
}
},
"200": {
"description": "Answers changed.",
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"properties": {
"voteId": {
"type": "string"
}
}
},
{
"$ref": "#/components/schemas/PollSummary"
}
]
}
}
}
},
"400": {
"description": "Bad input.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "\"zzz\" is not one of this poll's options. GET /polls/GckysfetvdBJJor43QN4 lists them."
}
}
}
},
"403": {
"description": "The voteId belongs to a row made in the web app.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "That row was added in the web app, so only its author can change it there."
}
}
}
},
"404": {
"description": "No poll with that id, or no answers with that voteId.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "No answers with that voteId in this poll."
}
}
}
},
"409": {
"description": "Closed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "This poll is closed, so answers can no longer change."
}
}
}
}
}
}
},
"/polls/{pollId}/comments": {
"parameters": [
{
"name": "pollId",
"in": "path",
"required": true,
"description": "The poll's id: the last part of its link. Anyone with it can read and answer the poll.",
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9]{8,32}$"
},
"example": "GckysfetvdBJJor43QN4"
}
],
"post": {
"operationId": "commentOnPoll",
"summary": "Add a comment everyone with the link sees",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 60
},
"text": {
"type": "string",
"maxLength": 1000
}
},
"required": [
"name",
"text"
]
},
"example": {
"name": "Sam",
"text": "Only after 6 for me on weekdays."
}
}
}
},
"responses": {
"201": {
"description": "Comment added.",
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"properties": {
"commentId": {
"type": "string"
}
}
},
{
"$ref": "#/components/schemas/PollSummary"
}
]
}
}
}
},
"400": {
"description": "Bad input.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "text is required."
}
}
}
}
}
}
},
"/polls/{pollId}/choose": {
"parameters": [
{
"name": "pollId",
"in": "path",
"required": true,
"description": "The poll's id: the last part of its link. Anyone with it can read and answer the poll.",
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9]{8,32}$"
},
"example": "GckysfetvdBJJor43QN4"
}
],
"post": {
"operationId": "pickFinalTime",
"summary": "Pick the final time (owner); closes the poll",
"security": [
{
"ownerKey": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"optionId": {
"type": "string",
"description": "An option id, or the option's start time or date."
}
},
"required": [
"optionId"
]
},
"example": {
"optionId": "NWpoik"
}
}
}
},
"responses": {
"200": {
"description": "The poll with chosen filled in.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PollSummary"
},
"example": {
"poll": {
"id": "GckysfetvdBJJor43QN4",
"title": "Team lunch",
"description": null,
"location": "Cafe Rio",
"timezone": "America/New_York",
"url": "https://when-free.openapps.xyz/p/GckysfetvdBJJor43QN4",
"status": "decided"
},
"options": [
{
"id": "NWpoik",
"start": "2026-10-27T12:00:00-04:00",
"label": "Tue, Oct 27, 12:00 PM EDT",
"yes": 2,
"maybe": 1,
"no": 0,
"best": true
},
{
"id": "B7oXVp",
"start": "2026-10-27T15:00:00-04:00",
"end": "2026-10-27T16:00:00-04:00",
"label": "Tue, Oct 27, 3:00 PM to 4:00 PM EDT",
"yes": 1,
"maybe": 0,
"no": 2,
"best": false
},
{
"id": "mXQrNm",
"date": "2026-10-30",
"allDay": true,
"label": "Fri, Oct 30, all day",
"yes": 1,
"maybe": 1,
"no": 1,
"best": false
}
],
"participants": [
{
"name": "Sam",
"answers": {
"NWpoik": "yes",
"B7oXVp": "yes",
"mXQrNm": "no"
}
},
{
"name": "Priya",
"answers": {
"NWpoik": "yes",
"B7oXVp": "no",
"mXQrNm": "maybe"
}
},
{
"name": "Jordan",
"answers": {
"NWpoik": "maybe",
"B7oXVp": "no",
"mXQrNm": "yes"
}
}
],
"best": [
"NWpoik"
],
"chosen": {
"id": "NWpoik",
"start": "2026-10-27T12:00:00-04:00",
"label": "Tue, Oct 27, 12:00 PM EDT",
"yes": 2,
"maybe": 1,
"no": 0,
"best": true,
"ics": "https://functions.bounded.sh/apps/3e1cb86700a5e96238d7a2f1/api/polls/GckysfetvdBJJor43QN4/ics",
"googleCalendar": "https://calendar.google.com/calendar/render?action=TEMPLATE&text=Team+lunch&dates=20261027T160000Z%2F20261027T170000Z",
"outlookCalendar": "https://outlook.live.com/calendar/0/deeplink/compose?path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&subject=Team+lunch&startdt=2026-10-27T16%3A00%3A00Z&enddt=2026-10-27T17%3A00%3A00Z"
},
"comments": [
{
"name": "Priya",
"text": "Tuesday works best for me.",
"at": "2026-10-19T14:00:00Z"
}
]
}
}
}
},
"400": {
"description": "Unknown option.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "optionId must be one of this poll's option ids. GET /polls/GckysfetvdBJJor43QN4 lists them."
}
}
}
},
"401": {
"description": "No key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "Send the poll's owner key in the x-owner-key header."
}
}
}
},
"403": {
"description": "Wrong key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "That owner key does not match this poll."
}
}
}
}
}
}
},
"/polls/{pollId}/close": {
"parameters": [
{
"name": "pollId",
"in": "path",
"required": true,
"description": "The poll's id: the last part of its link. Anyone with it can read and answer the poll.",
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9]{8,32}$"
},
"example": "GckysfetvdBJJor43QN4"
}
],
"post": {
"operationId": "closePoll",
"summary": "Stop new answers (owner)",
"security": [
{
"ownerKey": []
}
],
"responses": {
"200": {
"description": "The closed poll.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PollSummary"
}
}
}
},
"403": {
"description": "Wrong key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "That owner key does not match this poll."
}
}
}
}
}
}
},
"/polls/{pollId}/reopen": {
"parameters": [
{
"name": "pollId",
"in": "path",
"required": true,
"description": "The poll's id: the last part of its link. Anyone with it can read and answer the poll.",
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9]{8,32}$"
},
"example": "GckysfetvdBJJor43QN4"
}
],
"post": {
"operationId": "reopenPoll",
"summary": "Open the poll again and clear the final time (owner)",
"security": [
{
"ownerKey": []
}
],
"responses": {
"200": {
"description": "The open poll.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PollSummary"
}
}
}
},
"403": {
"description": "Wrong key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "That owner key does not match this poll."
}
}
}
}
}
}
},
"/polls/{pollId}/ics": {
"parameters": [
{
"name": "pollId",
"in": "path",
"required": true,
"description": "The poll's id: the last part of its link. Anyone with it can read and answer the poll.",
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9]{8,32}$"
},
"example": "GckysfetvdBJJor43QN4"
}
],
"get": {
"operationId": "getCalendarFile",
"summary": "A calendar file (.ics) for the final time",
"responses": {
"200": {
"description": "A standard calendar file to import into any calendar app.",
"content": {
"text/calendar": {
"schema": {
"type": "string"
}
}
}
},
"404": {
"description": "No final time yet.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "No final time has been picked for this poll yet."
}
}
}
}
}
}
}
}
}
Change history
Quality pass: comments, CSV export, share the final time, calendar menu, fixes
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync