{
  "openapi": "3.1.0",
  "info": {
    "title": "Meetful booking — book a time, or set a booking page up on a website",
    "version": "1.0.0",
    "description": "Book a time on anyone's Meetful booking link (meetful.com/<handle>) for your person — no account, no key. Read the open times, check the time, name and email with your person, then book. The time is checked again at booking; if it was taken, the error lists the nearest open times. Setting booking up on a website you are building is POST /api/setup/booking: it makes the person's booking page and returns the code for their site, with one claim link for them. The guide: https://meetful.com/add-booking.md"
  },
  "servers": [
    {
      "url": "https://meetful.com"
    }
  ],
  "paths": {
    "/api/book/{handle}": {
      "get": {
        "operationId": "findTimes",
        "summary": "Who, how long, and the open times (epoch milliseconds).",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "The name in the booking link: meetful.com/<handle>.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "minutes",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Another length the page offers."
          }
        ],
        "responses": {
          "200": {
            "description": "The page and its open times.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "page": {
                      "type": "object",
                      "properties": {
                        "handle": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "minutes": {
                          "type": "integer"
                        },
                        "durations": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "tz": {
                          "type": "string"
                        },
                        "questions": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    },
                    "host": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "minutes": {
                      "type": "integer"
                    },
                    "slots": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      },
                      "description": "Open start times, epoch milliseconds."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "bookTime",
        "summary": "Book one of the open times.",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "The name in the booking link: meetful.com/<handle>.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "at",
                  "name"
                ],
                "properties": {
                  "at": {
                    "type": "string",
                    "description": "A start time from the open times: ISO with its offset (2026-11-12T10:00-05:00), UTC (2026-11-12T15:00:00Z), or epoch milliseconds."
                  },
                  "name": {
                    "type": "string",
                    "description": "Who will be in the meeting."
                  },
                  "email": {
                    "type": "string",
                    "description": "Where the confirmation, the calendar file and the private link go. Recommended."
                  },
                  "about": {
                    "type": "string",
                    "description": "What they want to cover. It becomes the first item in the meeting."
                  },
                  "minutes": {
                    "type": "integer",
                    "description": "A length the page offers. Default: its first."
                  },
                  "via": {
                    "type": "string",
                    "description": "The assistant booking it (Grok, ChatGPT, Claude…). The owner is told."
                  },
                  "answers": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Answers to the page's own questions, by question id."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "booking": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "at": {
                          "type": "integer",
                          "description": "Start, epoch milliseconds."
                        },
                        "minutes": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "about": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "join": {
                          "type": "string",
                          "description": "The meeting room. It exists already."
                        },
                        "cal": {
                          "type": "string",
                          "description": "A calendar file (.ics)."
                        },
                        "manage": {
                          "type": "string",
                          "description": "The private link to move or cancel. Give it to the person booked; keep it out of anything public."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Not booked: the time was taken, has passed, or is not open; or a field is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "already": {
                      "type": "boolean"
                    },
                    "open": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "at": {
                            "type": "integer"
                          },
                          "iso": {
                            "type": "string"
                          },
                          "utc": {
                            "type": "string"
                          }
                        }
                      },
                      "description": "The nearest open times."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many bookings from one address. Try again in an hour."
          }
        }
      }
    },
    "/api/book/{handle}/confirm": {
      "get": {
        "operationId": "bookTimeByLink",
        "summary": "Book in one request — for an assistant that can only open addresses. Same fields, in the query.",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "The name in the booking link: meetful.com/<handle>.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "at",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A start time from the open times: ISO with its offset (2026-11-12T10:00-05:00), UTC (2026-11-12T15:00:00Z), or epoch milliseconds."
          },
          {
            "name": "name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Who will be in the meeting."
          },
          {
            "name": "email",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Where the confirmation, the calendar file and the private link go. Recommended."
          },
          {
            "name": "about",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "What they want to cover. It becomes the first item in the meeting."
          },
          {
            "name": "minutes",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "A length the page offers. Default: its first."
          },
          {
            "name": "via",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The assistant booking it (Grok, ChatGPT, Claude…). The owner is told."
          }
        ],
        "responses": {
          "200": {
            "description": "Booked or not, as a page (or JSON with Accept: application/json: {booked, booking | error, open})."
          }
        }
      }
    },
    "/api/setup/booking": {
      "post": {
        "operationId": "setUpBookingPage",
        "summary": "Make a booking page for somebody's website",
        "description": "Without a key, the page waits for its person: show them the claim link (the `tell` sentence carries it). With a developer key minted while signed in (Authorization: Bearer tmk_…), the page is that account's and live at once.",
        "security": [
          {},
          {
            "developerKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key owner's existing page, changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetupReply"
                }
              }
            }
          },
          "201": {
            "description": "Made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetupReply"
                }
              }
            }
          },
          "400": {
            "description": "No name, or an address that cannot be one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "A developer key this server does not know.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The address is taken — see `try`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many from one address in an hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "bookingPageStatus",
        "summary": "Whether the page is live yet",
        "security": [
          {
            "setupKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Its state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "401": {
            "description": "No setup key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "changeBookingPage",
        "summary": "Change the page while it waits for its person",
        "security": [
          {
            "setupKey": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          },
          "409": {
            "description": "It is its owner's now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "takeBackBookingPage",
        "summary": "Take the page back while it waits",
        "security": [
          {
            "setupKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Gone; the address is free again."
          },
          "409": {
            "description": "It is its owner's now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/setup/booking/link": {
      "post": {
        "operationId": "newClaimLink",
        "summary": "A fresh claim link; the old one stops working",
        "security": [
          {
            "setupKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The new link is in `claim`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetupReply"
                }
              }
            }
          },
          "409": {
            "description": "Not waiting any more.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SetupRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Whose booking page this is. Shown on it.",
            "example": "Ada Lovelace"
          },
          "handle": {
            "type": "string",
            "description": "The address wanted: meetful.com/<handle>. 3-32 letters, digits or dashes, not starting with a digit. Made from the name if left out.",
            "example": "ada"
          },
          "timezone": {
            "type": "string",
            "description": "IANA time zone the hours are in. UTC if left out.",
            "example": "Europe/London"
          },
          "hours": {
            "description": "Open hours. A string like \"Mon-Fri 09:00-17:00\" or \"Mon-Thu 9am-5pm; Fri 9am-1pm\", or an object like {\"mon-fri\": \"09:00-17:00\"} or {\"monday\": [[\"09:00\",\"12:00\"],[\"13:00\",\"17:00\"]]}. Monday to Friday, 9 to 5 if left out.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object"
              },
              {
                "type": "array"
              }
            ],
            "example": "Mon-Fri 09:00-17:00"
          },
          "lengths": {
            "type": "array",
            "description": "How long a meeting can be, in minutes. The person booking picks one; the first is shown first. Any other length becomes the nearest of these, with a warning. 30 minutes if left out.",
            "items": {
              "type": "integer",
              "enum": [
                15,
                30,
                45,
                60,
                90
              ]
            },
            "example": [
              30,
              60
            ]
          },
          "kinds": {
            "type": "array",
            "description": "Read the same way as lengths, for an agent that thinks in kinds of meeting: each one's minutes become a length the page offers.",
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "required": [
                    "minutes"
                  ],
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "minutes": {
                      "type": "integer"
                    }
                  }
                },
                {
                  "type": "integer"
                }
              ]
            }
          },
          "title": {
            "type": "string",
            "description": "The page's heading."
          },
          "blurb": {
            "type": "string",
            "description": "The line under the heading."
          },
          "site": {
            "type": "string",
            "description": "The website it goes on. Shown to the person when they claim it.",
            "example": "https://ada.dev"
          },
          "email": {
            "type": "string",
            "description": "The person's address, shown on the claim page so they can see it was made for them. Nothing is ever sent to it."
          },
          "buffer": {
            "type": "integer",
            "minimum": 0,
            "maximum": 60,
            "description": "Minutes kept free after each meeting. 10 if left out."
          },
          "notice": {
            "type": "integer",
            "minimum": 0,
            "maximum": 72,
            "description": "Hours of notice before the earliest bookable time. 4 if left out."
          },
          "horizon": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "description": "How many days ahead can be booked. 21 if left out."
          }
        }
      },
      "SetupReply": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "waiting",
              "live"
            ],
            "description": "waiting: shown on the site, not taking bookings until its person opens the claim link and signs in. live: taking bookings."
          },
          "handle": {
            "type": "string"
          },
          "page": {
            "type": "string",
            "format": "uri"
          },
          "claim": {
            "type": "string",
            "format": "uri",
            "description": "The one link the person opens to make the page theirs. Only when waiting."
          },
          "setup": {
            "type": "string",
            "description": "The setup key, stp_… — shown once. Send it as Authorization: Bearer to check on the page, change it, or take it back."
          },
          "expires": {
            "type": "string",
            "format": "date-time",
            "description": "When an unclaimed page lets its address go."
          },
          "owner": {
            "type": "string",
            "description": "First name of the owner, when live."
          },
          "embed": {
            "$ref": "#/components/schemas/Embed"
          },
          "tell": {
            "type": "string",
            "description": "One sentence to show the person, with the claim link in it."
          },
          "next": {
            "type": "object"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Anything sent that could not be read, and what was used instead."
          },
          "docs": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Status": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "waiting",
              "live",
              "expired",
              "gone"
            ]
          },
          "handle": {
            "type": "string"
          },
          "page": {
            "type": "string",
            "format": "uri"
          },
          "expires": {
            "type": "string",
            "format": "date-time"
          },
          "owner": {
            "type": "string"
          },
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "usesPage": {
            "type": "string",
            "format": "uri",
            "description": "When its person already had a booking page: the page this address now opens."
          },
          "site": {
            "type": [
              "string",
              "null"
            ]
          },
          "embed": {
            "$ref": "#/components/schemas/Embed"
          }
        }
      },
      "Embed": {
        "type": "object",
        "description": "The code that puts the booking page on a site.",
        "properties": {
          "button": {
            "type": "string",
            "description": "One script tag: a button that opens the booking page over the site."
          },
          "inline": {
            "type": "string",
            "description": "The booking page inside the page itself."
          },
          "iframe": {
            "type": "string",
            "description": "For site builders that allow no scripts."
          },
          "react": {
            "type": "string",
            "description": "Loading the script in a React or Next.js component."
          },
          "link": {
            "type": "string",
            "format": "uri"
          },
          "booking": {
            "type": "string",
            "format": "uri"
          },
          "markdown": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "try": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Free addresses, when the one asked for was taken."
          },
          "docs": {
            "type": "string"
          }
        }
      }
    },
    "securitySchemes": {
      "setupKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The stp_… key POST /api/setup/booking returned."
      },
      "developerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A tmk_… key from https://meetful.com/connect, minted while signed in."
      }
    }
  }
}