Gaplessly Guest self-service API

The token in a guest's booking link is the whole credential: move or cancel the booking, answer the intake form, or message the venue. One set of endpoints serves both engines, because a link points at a booking, not at a product.

GitHub OpenAPI

Operations 6

GET /api/book/{slug}/walkin/{token} Place in the walk-in line #
DELETE /api/book/{slug}/walkin/{token} Leave the walk-in line #
PATCH /api/book/{slug}/manage/{token} Change a booking, or its dietary preferences and occasion #
PUT /api/book/{slug}/manage/{token} Answer the venue's intake form #
POST /api/book/{slug}/manage/{token} Message the venue #
DELETE /api/book/{slug}/manage/{token} Cancel a booking #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/gaplessly-com:gaplessly-com-guest-self-service-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

gaplessly-com-guest-self-service-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Gaplessly booking Guest self-service API
  version: 1.0.0
  summary: Two booking engines sharing plumbing, never booking logic.
  description: 'Gaplessly runs **two booking engines**: appointments (a client books one service with one staff member for a set duration) and table reservations (a guest books a table for a party inside a service period). They are two products, not one product with a flag. Each organization runs exactly one, chosen when it signs up, and `businessType` on `GET /api/v1/organization` says which.


    This document covers the surfaces built for other software:


    - **`/api/v1/*`**: an organization''s own data with an API key, sent as `Authorization: Bearer sk_live_…`: read it, find free times, and book, change or cancel on a guest''s behalf. Scoped and rate limited.

    - **`/api/book/{slug}/*`**: the public booking endpoints a business''s booking page calls. No authentication.

    - **`/api/book/{slug}/manage/{token}`**: guest self-service. The token in the guest''s booking link is the whole credential.

    - **`/api/mcp`**: a Model Context Protocol endpoint for AI assistants helping a guest find a time. No authentication.


    Guides, a quickstart and the versioning and deprecation policy: /docs.'
  contact:
    name: Gaplessly
    url: mailto:support@gaplessly.com
servers:
- url: https://gaplessly.com
  description: 'Production. There is deliberately no CORS policy: an API key is a secret, so call `/api/v1` from a server, never from browser JavaScript.'
tags:
- name: Guest self-service
  description: 'The token in a guest''s booking link is the whole credential: move or cancel the booking, answer the intake form, or message the venue. One set of endpoints serves both engines, because a link points at a booking, not at a product.'
paths:
  /api/book/{slug}/walkin/{token}:
    get:
      summary: Place in the walk-in line
      description: One guest's status, by the token from joining. The token is the whole credential; an unknown one is a 404. `state` is `waiting` (with `position`, 1 is next, and `quotedMinutes` when the host quoted a wait), `ready` (come to the host stand), `seated`, `left` or `closed`. The status page polls this about every 20 seconds while visible.
      tags:
      - Guest self-service
      operationId: getWalkinStatus
      security: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The organization's public booking slug, i.e. the `{slug}` in /{slug}. Only orgs with status `active` resolve; anything else is a 404.
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: The status token returned when joining the walk-in line.
      responses:
        '200':
          description: The status.
          content:
            application/json:
              schema:
                type: object
                required:
                - companyName
                - state
                - position
                - quotedMinutes
                - waitedMinutes
                - partySize
                - name
                - canLeave
                properties:
                  companyName:
                    type: string
                  state:
                    type: string
                    enum:
                    - waiting
                    - ready
                    - seated
                    - left
                    - closed
                  position:
                    type:
                    - integer
                    - 'null'
                  quotedMinutes:
                    type:
                    - integer
                    - 'null'
                  waitedMinutes:
                    type: integer
                  partySize:
                    type: integer
                  name:
                    type: string
                    description: The first name given when joining.
                  canLeave:
                    type: boolean
        '404':
          description: No walk-in with that token at this venue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: Leave the walk-in line
      description: The guest's own cancel, by the token from joining. Allowed while waiting or after the table was called; everyone behind them moves up one.
      tags:
      - Guest self-service
      operationId: leaveWalkinLine
      security: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The organization's public booking slug, i.e. the `{slug}` in /{slug}. Only orgs with status `active` resolve; anything else is a 404.
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: The status token returned when joining the walk-in line.
      responses:
        '200':
          description: Left the line.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                properties:
                  ok:
                    const: true
        '404':
          description: No walk-in with that token at this venue.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Already seated, gone or closed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The change could not be stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/book/{slug}/manage/{token}:
    patch:
      summary: Change a booking, or its dietary preferences and occasion
      description: 'A guest moves their own booking to another time. One endpoint for both engines: a manage link points at a booking, not at a product, so the engine is worked out from the booking and the caller does not choose.


        Only the time changes: same service, same staff member, same party size. That is what lets a guest reschedule go through without staff review.


        The new time must respect the venue''s notice period too, so a guest cannot sidestep the cutoff by moving to a slot an hour from now. The slot is re-derived server-side, and a booking the venue cancelled mid-flow is not brought back.'
      tags:
      - Guest self-service
      operationId: guestRescheduleBooking
      security: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The organization's public booking slug, i.e. the `{slug}` in /{slug}. Only orgs with status `active` resolve; anything else is a 404.
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: The booking's `manage_token` (a uuid) from its confirmation email. This IS the credential; there is no session. A non-uuid string is rejected before it reaches Postgres and reads as "link not valid".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                startsAt:
                  type: string
                  format: date-time
                  description: Must be at least 60 seconds away, at least `guest_manage_cutoff_hours` away, and match a computed slot exactly.
                partySize:
                  type: integer
                  minimum: 1
                  maximum: 30
                  description: Table bookings only.
                dietaryTags:
                  type: array
                  items:
                    type: string
                  description: Table bookings only. Each one of the widget's fixed dietary tags; replaces the list.
                occasion:
                  type:
                  - string
                  - 'null'
                  maxLength: 80
                  description: Table bookings only. Null or blank clears it.
                reconfirm:
                  const: true
                  description: 'The guest''s "Yes, we''re coming" (F11 step 4). Both engines; never with any other field. Stamps `reconfirmed_at` once: a second press answers with the time already stored. Only a confirmed booking that has not ended can reconfirm (403 otherwise). Not gated on the self-service policy.'
      responses:
        '200':
          description: 'Changed. A change notification is queued after the response is sent. A dietary/occasion save answers `{ details: { dietaryTags, occasion } }` instead, and sends nothing. A reconfirm answers `{ reconfirmedAt }`.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  reconfirmedAt:
                    type: string
                    format: date-time
                  booking:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      starts_at:
                        type: string
                        format: date-time
                      ends_at:
                        type: string
                        format: date-time
                      party_size:
                        type:
                        - integer
                        - 'null'
                  details:
                    type: object
                    properties:
                      dietaryTags:
                        type: array
                        items:
                          type: string
                      occasion:
                        type:
                        - string
                        - 'null'
        '400':
          description: Nothing to change, an unparseable time or party size, a time already past or inside the org's notice period, a party size on an appointment, an unknown dietary tag, an occasion with a web address, dietary tags where the venue does not ask for them, or a change and a preferences edit in one request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: 'The venue''s policy refuses: guest self-service is switched off, the booking is in a status a guest may no longer act on (cancelled, seated, completed, no_show), it has already started, or it is now inside the notice period. The body carries a guest-readable sentence naming the venue.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The slug, the token, or the pairing of the two does not resolve. Deliberately indistinguishable; "That link is no longer valid".
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The requested time (or party size) is no longer free, or was taken between the availability read and the update; the new party size is above the venue's online limit; or the change would owe a larger deposit, or a card, than the booking holds. Carries a guest-readable sentence.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The update errored for a reason other than an overlap.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      summary: Answer the venue's intake form
      description: 'The guest answers whatever questions the venue set up for this booking. Serves both engines: the token points at a booking, and the engine is worked out from it.


        PUT rather than POST because there is exactly one response per booking: a guest reopening their link edits their answers rather than stacking a second submission.


        The form is looked up server-side, not taken from the body, since the venue may have switched or retired forms since the page was drawn. A service-specific form wins over the venue-wide one, and only active forms are served. Answers are validated against it: required questions present, dropdown answers within their `options`, lengths capped.


        Deliberately NOT gated on the venue''s guest self-service policy, unlike PATCH and DELETE: a venue that switched off guest rescheduling still asked these questions, and the hour before a visit is exactly when someone remembers their allergy.'
      tags:
      - Guest self-service
      operationId: guestSubmitIntake
      security: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The organization's public booking slug, i.e. the `{slug}` in /{slug}. Only orgs with status `active` resolve; anything else is a 404.
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: The booking's manage token.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - answers
              properties:
                answers:
                  type: object
                  description: Keyed by each question's stable `key`. Values are strings for `text`, `textarea` and `select`, booleans for `checkbox`. A key the form does not define is a 400 rather than being dropped; silently discarding something a guest typed is the worst available outcome for a form about allergies. Blank optional answers are omitted from storage rather than stored as `""`, so staff read "not answered" instead of a deliberate blank.
                  additionalProperties:
                    type:
                    - string
                    - boolean
      responses:
        '200':
          description: Saved. Staff see it on the booking.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                properties:
                  ok:
                    const: true
        '400':
          description: 'An answer failed validation. The message names the question: a missing required answer, an unticked required checkbox, a dropdown answer outside its choices, a value of the wrong type, an over-length answer (500 characters for a short answer, 2000 for a long one), or a key this form does not define.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: The booking is cancelled, or it has already finished; there is nothing left to prepare for. Carries a guest-readable sentence.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: 'The link does not resolve, **or** this org has no active intake form for this booking. The two are the same status on purpose: from outside, "no such link" and "nothing to fill in" are equally not-a-resource.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: A second submit raced the first; two tabs, or a double tap, and the one-response-per-booking index caught the loser.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The insert or update errored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      summary: Message the venue
      description: 'Allowed regardless of the change cutoff and on any status, unlike PATCH and DELETE. "I am running late" and "why was this cancelled" are exactly the moments someone needs to reach a venue; refusing them because the booking is two hours away would be backwards.


        A manage link is public to anyone holding it, so there is a flood cap: 20 guest messages per booking, after which the guest is told to contact the venue directly. That is a cheap guard, not real rate limiting; there is none anywhere in this API yet.'
      tags:
      - Guest self-service
      operationId: guestMessageVenue
      security: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The organization's public booking slug, i.e. the `{slug}` in /{slug}. Only orgs with status `active` resolve; anything else is a 404.
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: The booking's manage token.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - message
              properties:
                message:
                  type: string
                  minLength: 1
                  maxLength: 2000
      responses:
        '200':
          description: Sent. It appears in the venue's Inbox.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                properties:
                  ok:
                    const: true
        '400':
          description: An empty message, or one over 2000 characters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The link does not resolve.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: This booking already has 20 guest messages against it. The only 429 in the API, and it is a per-booking cap rather than a rate limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The insert failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: Cancel a booking
      description: Sets status to `cancelled` rather than deleting, so the venue keeps the history and the slot or table is released by exactly the rules a staff cancellation uses. Compare-and-set on the current status.
      tags:
      - Guest self-service
      operationId: guestCancelBooking
      security: []
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The organization's public booking slug, i.e. the `{slug}` in /{slug}. Only orgs with status `active` resolve; anything else is a 404.
      - name: token
        in: path
        required: true
        schema:
          type: string
        description: The booking's manage token.
      responses:
        '200':
          description: Cancelled. A cancellation notification is queued after the response is sent.
          content:
            application/json:
              schema:
                type: object
                required:
                - ok
                properties:
                  ok:
                    const: true
        '403':
          description: The policy refuses; same set of reasons as PATCH, with a guest-readable sentence. Staff can still cancel from the dashboard, and any deposit still refunds or forfeits by the usual rule.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The link does not resolve.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Its status changed underneath the request; in practice, it was already cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: The update errored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Error:
      type: object
      required:
      - error
      properties:
        error:
          type: string
          description: A human-readable sentence. This app never returns machine-readable error codes; the HTTP status is the code.
      description: 'The body of every non-2xx response: a human-readable sentence in `error`. `/api/v1` responses add a machine-readable `code`; match on that, never on the sentence.'
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: sk_live_<16 hex key id>_<43 char base64url secret> (sk_test_ for a test key)
      description: 'An organization admin creates keys in the dashboard under **Developer**; API access is included on Pro (Venue Pro for venues). The raw key is shown once, when it is created. Send it as `Authorization: Bearer sk_live_…` and nowhere else: no query parameter, custom header or cookie, because a credential in a URL ends up in logs and browser history.


        - **Scopes are the whole permission model.** A key carries `resource:action` scopes, never a role, and `write` does not imply `read`.

        - **Engine endpoints check the engine.** A key from a table-reservations organization gets `403 wrong_vertical` from an appointments endpoint, and the other way round, even when it holds the scope.

        - **Keys cannot manage keys.** Keys are created and revoked only in the dashboard, so a leaked key cannot mint another.

        - **Errors carry a stable `code`** beside the `error` sentence: `missing_authorization`, `invalid_key`, `key_revoked`, `key_expired`, `organization_inactive` (401); `insufficient_scope`, `wrong_vertical`, `plan_required` (403); `rate_limited` (429). Match on `code`, never on the sentence.

        - **Rate limits are per key**: a fixed 60-second window, 120 requests a minute by default. Every response states the policy in `RateLimit-Policy`, a response to a known key adds what is left in `RateLimit`, and a 429 adds `Retry-After`.

        - **Every list is `{ data, nextCursor }`**, keyset paged and camelCase. `nextCursor` is null exactly when there are no more rows.

        - **`/api/v1/` is a real version.** A breaking change ships as `/api/v2/`, and `v1` keeps answering unchanged for at least 6 months with `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers on every response. Only a security fix can shorten that window, and the notice says why.'
externalDocs:
  description: Versioning and deprecation policy
  url: /docs/conventions#versioning