book-free.openapps.xyz

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.
Owned app · rev f8f1b3a80195
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 7385a6c1c47153b1728160ff
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.
← All files
public/openapi.json
{
 "openapi": "3.1.0",
 "info": {
  "title": "Book by OpenApps",
  "version": "1.1.0",
  "summary": "A free booking page for one-on-one meetings, a free Calendly alternative.",
  "description": "No key and no account. Anyone can read a page's open times and book one. The ownerKey from POST /pages manages a page (header x-owner-key); the cancelKey from each booking reads, moves and cancels it (header x-cancel-key). Both come back once. Book does not read calendars or send emails. Errors are JSON: {\"error\": \"<sentence>\"}. Guide: https://book-free.openapps.xyz/llms.txt"
 },
 "servers": [
  {
   "url": "https://functions.bounded.sh/apps/7385a6c1c47153b1728160ff/api"
  }
 ],
 "externalDocs": {
  "url": "https://book-free.openapps.xyz/llms.txt"
 },
 "components": {
  "securitySchemes": {
   "ownerKey": {
    "type": "apiKey",
    "in": "header",
    "name": "x-owner-key",
    "description": "The page's owner key, from POST /pages."
   },
   "cancelKey": {
    "type": "apiKey",
    "in": "header",
    "name": "x-cancel-key",
    "description": "The booking's cancel key, from POST /pages/{handle}/bookings."
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string"
     }
    },
    "required": [
     "error"
    ]
   },
   "PageInput": {
    "type": "object",
    "properties": {
     "handle": {
      "type": "string",
      "description": "The page address. Leave it out to get one made from the name."
     },
     "name": {
      "type": "string",
      "maxLength": 80,
      "description": "Who people meet."
     },
     "timezone": {
      "type": "string",
      "description": "IANA zone the hours and days off are in.",
      "example": "America/New_York"
     },
     "meetingUrl": {
      "type": "string",
      "description": "An https link to a Google Meet, Zoom or Teams room. Shown only to people who book."
     },
     "title": {
      "type": "string",
      "maxLength": 120,
      "default": "30 minute call"
     },
     "description": {
      "type": "string",
      "maxLength": 1000
     },
     "durationMin": {
      "type": "integer",
      "enum": [
       15,
       30,
       45,
       60
      ],
      "default": 30
     },
     "hours": {
      "type": "object",
      "description": "Weekday (mon..sun, weekdays, weekend, everyday) to ranges \"HH:MM-HH:MM\" in 15-minute steps, as a list or one comma-separated string. Days left out are closed.",
      "additionalProperties": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "array",
         "items": {
          "type": "string"
         }
        }
       ]
      },
      "example": {
       "weekdays": [
        "09:00-12:00",
        "13:00-17:00"
       ]
      }
     },
     "daysOff": {
      "type": "array",
      "maxItems": 50,
      "items": {
       "oneOf": [
        {
         "type": "string",
         "description": "\"YYYY-MM-DD\" or \"YYYY-MM-DD to YYYY-MM-DD\""
        },
        {
         "type": "object",
         "properties": {
          "from": {
           "type": "string",
           "format": "date"
          },
          "to": {
           "type": "string",
           "format": "date"
          }
         },
         "required": [
          "from"
         ]
        }
       ]
      },
      "description": "Dates nobody can book, inclusive, in the page's time zone.",
      "example": [
       "2026-12-24",
       "2026-12-28 to 2026-12-31"
      ]
     },
     "noticeHours": {
      "type": "integer",
      "minimum": 0,
      "maximum": 720,
      "default": 4
     },
     "horizonDays": {
      "type": "integer",
      "minimum": 1,
      "maximum": 365,
      "default": 60
     },
     "bufferMin": {
      "type": "integer",
      "enum": [
       0,
       15,
       30,
       45,
       60
      ],
      "default": 0
     },
     "questions": {
      "type": "array",
      "maxItems": 5,
      "items": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "object",
         "properties": {
          "label": {
           "type": "string",
           "maxLength": 200
          },
          "required": {
           "type": "boolean"
          }
         },
         "required": [
          "label"
         ]
        }
       ]
      }
     }
    },
    "required": [
     "name",
     "timezone",
     "meetingUrl"
    ]
   },
   "PagePatch": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 80,
      "description": "Who people meet."
     },
     "timezone": {
      "type": "string",
      "description": "IANA zone the hours and days off are in.",
      "example": "America/New_York"
     },
     "meetingUrl": {
      "type": "string",
      "description": "An https link to a Google Meet, Zoom or Teams room. Shown only to people who book."
     },
     "title": {
      "type": "string",
      "maxLength": 120,
      "default": "30 minute call"
     },
     "description": {
      "type": "string",
      "maxLength": 1000
     },
     "durationMin": {
      "type": "integer",
      "enum": [
       15,
       30,
       45,
       60
      ],
      "default": 30
     },
     "hours": {
      "type": "object",
      "description": "Weekday (mon..sun, weekdays, weekend, everyday) to ranges \"HH:MM-HH:MM\" in 15-minute steps, as a list or one comma-separated string. Days left out are closed.",
      "additionalProperties": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "array",
         "items": {
          "type": "string"
         }
        }
       ]
      },
      "example": {
       "weekdays": [
        "09:00-12:00",
        "13:00-17:00"
       ]
      }
     },
     "daysOff": {
      "type": "array",
      "maxItems": 50,
      "items": {
       "oneOf": [
        {
         "type": "string",
         "description": "\"YYYY-MM-DD\" or \"YYYY-MM-DD to YYYY-MM-DD\""
        },
        {
         "type": "object",
         "properties": {
          "from": {
           "type": "string",
           "format": "date"
          },
          "to": {
           "type": "string",
           "format": "date"
          }
         },
         "required": [
          "from"
         ]
        }
       ]
      },
      "description": "Dates nobody can book, inclusive, in the page's time zone.",
      "example": [
       "2026-12-24",
       "2026-12-28 to 2026-12-31"
      ]
     },
     "noticeHours": {
      "type": "integer",
      "minimum": 0,
      "maximum": 720,
      "default": 4
     },
     "horizonDays": {
      "type": "integer",
      "minimum": 1,
      "maximum": 365,
      "default": 60
     },
     "bufferMin": {
      "type": "integer",
      "enum": [
       0,
       15,
       30,
       45,
       60
      ],
      "default": 0
     },
     "questions": {
      "type": "array",
      "maxItems": 5,
      "items": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "object",
         "properties": {
          "label": {
           "type": "string",
           "maxLength": 200
          },
          "required": {
           "type": "boolean"
          }
         },
         "required": [
          "label"
         ]
        }
       ]
      }
     }
    },
    "description": "Any fields of PageInput except handle. Fields left out stay as they are; questions and daysOff replace the list."
   },
   "Page": {
    "type": "object",
    "properties": {
     "handle": {
      "type": "string"
     },
     "url": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "description": {
      "type": [
       "string",
       "null"
      ]
     },
     "durationMin": {
      "type": "integer"
     },
     "meeting": {
      "type": "string",
      "description": "The kind of call, like \"Google Meet\"."
     },
     "timezone": {
      "type": "string"
     },
     "hours": {
      "type": "object",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       }
      }
     },
     "hoursSummary": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "daysOff": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "from": {
         "type": "string",
         "format": "date"
        },
        "to": {
         "type": "string",
         "format": "date"
        }
       }
      }
     },
     "noticeHours": {
      "type": "integer"
     },
     "horizonDays": {
      "type": "integer"
     },
     "bufferMin": {
      "type": "integer"
     },
     "questions": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "label": {
         "type": "string"
        },
        "required": {
         "type": "boolean"
        }
       }
      }
     }
    }
   },
   "Booking": {
    "type": "object",
    "properties": {
     "bookingId": {
      "type": "string"
     },
     "start": {
      "type": "string",
      "format": "date-time"
     },
     "end": {
      "type": "string",
      "format": "date-time"
     },
     "label": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "enum": [
       "booked",
       "cancelled"
      ]
     },
     "cancelledBy": {
      "type": [
       "string",
       "null"
      ],
      "enum": [
       "owner",
       "guest",
       null
      ]
     },
     "movedTo": {
      "type": "string",
      "description": "Set when the guest moved the booking: the id of the booking that replaced it."
     },
     "name": {
      "type": "string"
     },
     "email": {
      "type": "string"
     },
     "note": {
      "type": [
       "string",
       "null"
      ]
     },
     "answers": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "q": {
         "type": "string"
        },
        "a": {
         "type": "string"
        }
       }
      }
     }
    }
   },
   "BookingCreated": {
    "type": "object",
    "properties": {
     "bookingId": {
      "type": "string"
     },
     "start": {
      "type": "string",
      "format": "date-time"
     },
     "end": {
      "type": "string",
      "format": "date-time"
     },
     "label": {
      "type": "string"
     },
     "with": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "meetingUrl": {
      "type": "string"
     },
     "icsUrl": {
      "type": "string"
     },
     "googleCalendarUrl": {
      "type": "string"
     },
     "manageUrl": {
      "type": "string",
      "description": "The person's private page to see, move or cancel the booking."
     },
     "cancelKey": {
      "type": "string"
     },
     "movedFrom": {
      "type": "string",
      "description": "Only on a move: the booking this one replaced."
     },
     "note": {
      "type": "string"
     }
    }
   }
  }
 },
 "paths": {
  "/": {
   "get": {
    "operationId": "listRoutes",
    "summary": "List the routes",
    "responses": {
     "200": {
      "description": "The routes",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/pages": {
   "post": {
    "operationId": "createPage",
    "summary": "Make a booking page",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/PageInput"
       },
       "example": {
        "handle": "sam-lee",
        "name": "Sam Lee",
        "timezone": "America/New_York",
        "meetingUrl": "https://meet.google.com/abc-defg-hij",
        "durationMin": 30,
        "hours": {
         "weekdays": "09:00-17:00"
        },
        "daysOff": [
         "2026-12-24"
        ]
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The page, its admin link and keys",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "handle": {
           "type": "string"
          },
          "url": {
           "type": "string"
          },
          "adminUrl": {
           "type": "string"
          },
          "ownerKey": {
           "type": "string"
          },
          "calendarFeedUrl": {
           "type": "string"
          },
          "share": {
           "type": "string"
          },
          "page": {
           "$ref": "#/components/schemas/Page"
          }
         }
        },
        "example": {
         "handle": "sam-lee",
         "url": "https://book-free.openapps.xyz/sam-lee",
         "adminUrl": "https://book-free.openapps.xyz/e/sam-lee#k=OWNER_KEY",
         "ownerKey": "OWNER_KEY",
         "calendarFeedUrl": "https://functions.bounded.sh/apps/7385a6c1c47153b1728160ff/api/pages/sam-lee/calendar.ics?f=FEED_KEY"
        }
       }
      }
     },
     "400": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "409": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/pages/{handle}": {
   "get": {
    "operationId": "getPage",
    "summary": "Read a page (not the meeting link)",
    "parameters": [
     {
      "name": "handle",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"
      },
      "example": "sam-lee"
     }
    ],
    "responses": {
     "200": {
      "description": "The page",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Page"
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   },
   "patch": {
    "operationId": "updatePage",
    "summary": "Change a page",
    "parameters": [
     {
      "name": "handle",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"
      },
      "example": "sam-lee"
     }
    ],
    "security": [
     {
      "ownerKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/PagePatch"
       },
       "example": {
        "daysOff": [
         "2026-12-28 to 2026-12-31"
        ],
        "bufferMin": 15
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "The page and its meeting link",
      "content": {
       "application/json": {
        "schema": {
         "allOf": [
          {
           "$ref": "#/components/schemas/Page"
          },
          {
           "type": "object",
           "properties": {
            "meetingUrl": {
             "type": "string"
            }
           }
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "401": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "403": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/pages/{handle}/availability": {
   "get": {
    "operationId": "getAvailability",
    "summary": "Open start times",
    "parameters": [
     {
      "name": "handle",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"
      },
      "example": "sam-lee"
     },
     {
      "name": "tz",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "Zone to show times in (default: the page's).",
      "example": "Europe/London"
     },
     {
      "name": "from",
      "in": "query",
      "schema": {
       "type": "string",
       "format": "date"
      },
      "description": "First date, in tz (default: today)."
     },
     {
      "name": "days",
      "in": "query",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 62,
       "default": 14
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Open times",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "handle": {
           "type": "string"
          },
          "title": {
           "type": "string"
          },
          "name": {
           "type": "string"
          },
          "durationMin": {
           "type": "integer"
          },
          "timezone": {
           "type": "string"
          },
          "zone": {
           "type": "string"
          },
          "from": {
           "type": "string"
          },
          "days": {
           "type": "integer"
          },
          "slots": {
           "type": "array",
           "items": {
            "type": "string",
            "format": "date-time"
           }
          },
          "book": {
           "type": "string"
          }
         }
        },
        "example": {
         "handle": "sam-lee",
         "durationMin": 30,
         "timezone": "Europe/London",
         "zone": "London time (GMT+1)",
         "from": "2026-10-21",
         "days": 7,
         "slots": [
          "2026-10-21T14:00:00+01:00",
          "2026-10-21T14:30:00+01:00"
         ]
        }
       }
      }
     },
     "400": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/pages/{handle}/bookings": {
   "post": {
    "operationId": "createBooking",
    "summary": "Book an open time",
    "parameters": [
     {
      "name": "handle",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"
      },
      "example": "sam-lee"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "start": {
          "type": "string",
          "description": "One of the open slots. Without an offset it is read in timezone, else in the page's zone."
         },
         "timezone": {
          "type": "string"
         },
         "name": {
          "type": "string",
          "maxLength": 80
         },
         "email": {
          "type": "string",
          "format": "email"
         },
         "note": {
          "type": "string",
          "maxLength": 1000
         },
         "answers": {
          "oneOf": [
           {
            "type": "object",
            "additionalProperties": {
             "type": "string"
            }
           },
           {
            "type": "array",
            "items": {
             "type": "string"
            }
           }
          ]
         }
        },
        "required": [
         "start",
         "name",
         "email"
        ]
       },
       "example": {
        "start": "2026-10-21T15:00:00+01:00",
        "name": "Alex Kim",
        "email": "alex@example.com",
        "answers": {
         "Company": "Acme"
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The booking, the meeting link and the cancel key",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/BookingCreated"
        },
        "example": {
         "bookingId": "dh6syZouMtw5XA55DX7h",
         "start": "2026-10-21T15:00:00+01:00",
         "end": "2026-10-21T15:30:00+01:00",
         "label": "Wed, Oct 21, 3:00 PM to 3:30 PM GMT+1",
         "with": "Sam Lee",
         "title": "Intro call",
         "meetingUrl": "https://meet.google.com/abc-defg-hij",
         "icsUrl": "https://functions.bounded.sh/apps/7385a6c1c47153b1728160ff/api/bookings/dh6syZouMtw5XA55DX7h/ics?c=CANCEL_KEY",
         "googleCalendarUrl": "https://calendar.google.com/calendar/render?action=TEMPLATE&...",
         "manageUrl": "https://book-free.openapps.xyz/b/sam-lee/dh6syZouMtw5XA55DX7h#c=CANCEL_KEY",
         "cancelKey": "CANCEL_KEY",
         "note": "Book does not send emails. Give the person the meeting link, the calendar file and the manageUrl (it is how they move or cancel the booking)."
        }
       }
      }
     },
     "400": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "409": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   },
   "get": {
    "operationId": "listBookings",
    "summary": "The page's bookings (owner)",
    "parameters": [
     {
      "name": "handle",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"
      },
      "example": "sam-lee"
     }
    ],
    "security": [
     {
      "ownerKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "Bookings in the page's time zone",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "handle": {
           "type": "string"
          },
          "timezone": {
           "type": "string"
          },
          "calendarFeedUrl": {
           "type": "string"
          },
          "upcoming": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Booking"
           }
          },
          "past": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Booking"
           }
          },
          "cancelled": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Booking"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "403": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/pages/{handle}/calendar.ics": {
   "get": {
    "operationId": "getCalendarFeed",
    "summary": "The page's bookings as a calendar to subscribe to",
    "parameters": [
     {
      "name": "handle",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"
      },
      "example": "sam-lee"
     },
     {
      "name": "f",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "The feed key, part of calendarFeedUrl. It can only read."
     }
    ],
    "responses": {
     "200": {
      "description": "An iCalendar feed",
      "content": {
       "text/calendar": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "401": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "403": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/bookings/{bookingId}": {
   "get": {
    "operationId": "getBooking",
    "summary": "A booking and its meeting link",
    "parameters": [
     {
      "name": "bookingId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "dh6syZouMtw5XA55DX7h"
     }
    ],
    "security": [
     {
      "cancelKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The booking",
      "content": {
       "application/json": {
        "schema": {
         "allOf": [
          {
           "$ref": "#/components/schemas/Booking"
          },
          {
           "type": "object",
           "properties": {
            "page": {
             "type": "object"
            },
            "meetingUrl": {
             "type": [
              "string",
              "null"
             ]
            },
            "icsUrl": {
             "type": "string"
            },
            "manageUrl": {
             "type": "string"
            }
           }
          }
         ]
        }
       }
      }
     },
     "401": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "403": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/bookings/{bookingId}/ics": {
   "get": {
    "operationId": "getBookingIcs",
    "summary": "A calendar file for the meeting",
    "parameters": [
     {
      "name": "bookingId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "dh6syZouMtw5XA55DX7h"
     },
     {
      "name": "c",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "The cancel key (or send x-cancel-key)."
     }
    ],
    "responses": {
     "200": {
      "description": "An iCalendar file",
      "content": {
       "text/calendar": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "401": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "403": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/bookings/{bookingId}/reschedule": {
   "post": {
    "operationId": "rescheduleBooking",
    "summary": "Move a booking to another open time",
    "description": "Books the new time with the same details and frees the old one in one step. The new booking keeps the cancel key.",
    "parameters": [
     {
      "name": "bookingId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "dh6syZouMtw5XA55DX7h"
     }
    ],
    "security": [
     {
      "cancelKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "start": {
          "type": "string"
         },
         "timezone": {
          "type": "string"
         }
        },
        "required": [
         "start"
        ]
       },
       "example": {
        "start": "2026-10-22T10:00:00+01:00"
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "The new booking",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/BookingCreated"
        },
        "example": {
         "bookingId": "dh6syZouMtw5XA55DX7h",
         "start": "2026-10-21T15:00:00+01:00",
         "end": "2026-10-21T15:30:00+01:00",
         "label": "Wed, Oct 21, 3:00 PM to 3:30 PM GMT+1",
         "with": "Sam Lee",
         "title": "Intro call",
         "meetingUrl": "https://meet.google.com/abc-defg-hij",
         "icsUrl": "https://functions.bounded.sh/apps/7385a6c1c47153b1728160ff/api/bookings/dh6syZouMtw5XA55DX7h/ics?c=CANCEL_KEY",
         "googleCalendarUrl": "https://calendar.google.com/calendar/render?action=TEMPLATE&...",
         "manageUrl": "https://book-free.openapps.xyz/b/sam-lee/dh6syZouMtw5XA55DX7h#c=CANCEL_KEY",
         "cancelKey": "CANCEL_KEY",
         "note": "Book does not send emails. Give the person the meeting link, the calendar file and the manageUrl (it is how they move or cancel the booking).",
         "movedFrom": "OLD_BOOKING_ID"
        }
       }
      }
     },
     "400": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "401": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "403": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "409": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  },
  "/bookings/{bookingId}/cancel": {
   "post": {
    "operationId": "cancelBooking",
    "summary": "Cancel a booking and free its time",
    "parameters": [
     {
      "name": "bookingId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "dh6syZouMtw5XA55DX7h"
     }
    ],
    "security": [
     {
      "cancelKey": []
     },
     {
      "ownerKey": []
     }
    ],
    "responses": {
     "200": {
      "description": "The cancelled booking",
      "content": {
       "application/json": {
        "schema": {
         "allOf": [
          {
           "$ref": "#/components/schemas/Booking"
          },
          {
           "type": "object",
           "properties": {
            "freed": {
             "type": "boolean"
            }
           }
          }
         ]
        }
       }
      }
     },
     "401": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "403": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "404": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     },
     "409": {
      "description": "A plain sentence saying what to change",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        },
        "example": {
         "error": "That time is not open. GET .../pages/sam-lee/availability lists the open times."
        }
       }
      }
     }
    }
   }
  }
 }
}

Change history

Add favicon.ico and link tag
f8f1b3a801 · 2 hours ago
Quality pass: guest reschedule, days off, calendar feed, API docs
c827e5d073 · 5 hours ago
Bounded source sync
68e67b4092 · 1 day ago
Bounded source sync
ab2e006f9b · 1 day ago
Bounded source sync
bb10824b54 · 1 day ago
Bounded source sync
d22f765fb7 · 1 day ago
Bounded source sync
30b4b06513 · 1 day ago
Bounded source sync
c3d297477a · 1 day ago
Bounded source sync
7b18317417 · 1 day ago