openapi: 3.0.3
info:
  title: Messaging API
  version: 2.0.0
  description: >
    API for retrieving the operator↔partner conversation stored against a
    booking.


    Bookable relays the conversation between the venue operator and the
    integration partner over email

    (both parties email a per-booking conversation alias and Bookable forwards
    each message on).


    Participants can take part in two ways:

    - **By email** — reply to the relay email from their normal mail client
    (attachments supported this way only).

    - **Programmatically** — `POST /bookings/{bookingId}/messages` to send a
    plain-text message. Bookable stores
      it and relays it on to the other participant over the same conversation channel. `GET` returns the stored
      conversation stack.

    The send direction is derived from the caller's role: an Integration Partner
    sends to the venue operator

    (`partner_to_operator`); a venue group operator sends to the integration
    partner (`operator_to_partner`).

    Each stored message carries this absolute `direction`.


    **Pagination:** Uses page-based pagination via `pageNumber` and `pageSize`.
    Results are ordered

    by `sentAt` ascending (oldest first).


    **Availability:** Real email relay is not available in the Sandbox
    environment. In Sandbox only the

    webhook-only test flow is reachable — see the `direction` override on
    `SendMessageRequest`, which stores

    the message and fires the `message.received` webhook to your registered
    sandbox callback without sending

    any email.
  contact:
    name: Bookable
    url: https://www.bookabletech.com
    email: hello@bookabletech.com
servers:
  - url: https://api.bookabletech.com
    description: Live
tags:
  - name: Messaging
    description: Booking message operations
paths:
  /bookings/{bookingId}/messages:
    post:
      summary: Send a message for a booking
      description: >
        Sends a plain-text message on the booking's operator↔partner
        conversation. Bookable stores the

        message and relays it over email to the other participant, who can reply
        from their normal mail client.


        The delivery target is derived from the caller's role — there is no
        `recipient` field:

        - **Integration Partner** — delivered to the venue operator (`direction:
        partner_to_operator`).

        - **Venue group operator** — delivered to the integration partner
        (`direction: operator_to_partner`).


        Attachments cannot be sent via this endpoint; add them by replying to
        the relay email instead.
      operationId: sendBookingMessage
      tags:
        - Messaging
      parameters:
        - name: bookingId
          in: path
          required: true
          description: The composite booking ID (e.g. `29|CO|6a3e866a4989b3b73d78f011|B`)
          schema:
            type: string
          example: 29|CO|6a3e866a4989b3b73d78f011|B
        - $ref: '#/components/parameters/CorrelationID'
        - $ref: '#/components/parameters/PartnerReference'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
      responses:
        '201':
          description: Message stored and relayed to the other participant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
              example:
                id: msg_ghi789
                subject: ABR-1042 Message regarding your booking
                body: Could you please accommodate a high chair for us?
                senderEmail: partner@example.com
                senderName: Jane Smith
                direction: partner_to_operator
                sentAt: '2026-06-01T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - Live:
            - venue-booking:messaging:write
    get:
      summary: List messages for a booking
      description: >
        Returns all stored messages for the given booking's operator↔partner
        conversation, ordered by

        `sentAt` ascending (oldest first). Each message's `direction` states who
        sent it and who received it.


        Access is scoped to the caller's venue group: Integration Partners see
        only messages for bookings

        belonging to their associated venues.
      operationId: getBookingMessages
      tags:
        - Messaging
      parameters:
        - name: bookingId
          in: path
          required: true
          description: The composite booking ID (e.g. `29|CO|6a3e866a4989b3b73d78f011|B`)
          schema:
            type: string
          example: 29|CO|6a3e866a4989b3b73d78f011|B
        - $ref: '#/components/parameters/CorrelationID'
        - $ref: '#/components/parameters/PartnerReference'
        - name: pageNumber
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
            nullable: false
          description: Page number (default 1).
        - name: pageSize
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
            nullable: false
          description: Items per page (default 20, max 100).
      responses:
        '200':
          description: Message list retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageListResponse'
              example:
                meta:
                  currentPage: 1
                  pageSize: 20
                  totalItems: 2
                  totalPages: 1
                data:
                  - id: msg_abc123
                    subject: Table for Jane Smith
                    body: Could you please accommodate a high chair for us?
                    senderEmail: partner@example.com
                    senderName: Jane Smith
                    direction: partner_to_operator
                    sentAt: '2026-06-01T10:00:00Z'
                  - id: msg_def456
                    subject: 'Re: Table for Jane Smith'
                    body: Of course, we have noted your request.
                    senderEmail: venue@example.com
                    senderName: Abbey Inn
                    direction: operator_to_partner
                    attachments:
                      - filename: floorplan.pdf
                        contentType: application/pdf
                    sentAt: '2026-06-01T10:15:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - Live:
            - venue-booking:messaging:read
components:
  schemas:
    SendMessageRequest:
      type: object
      required:
        - body
      properties:
        body:
          type: string
          description: >-
            Plain-text message body. Delivered to the other conversation
            participant.
          minLength: 1
          maxLength: 5000
          example: Could you please accommodate a high chair for us?
        direction:
          type: string
          enum:
            - operator_to_partner
            - partner_to_operator
          description: >
            Sandbox-only test override. Forces the absolute message direction so
            a partner can trigger their own `message.received` webhook (send
            `operator_to_partner`) against their registered sandbox callback —
            without a real operator or an inbound email round-trip. **Ignored
            outside the Sandbox environment**, where the direction is derived
            from the caller's role. When set in Sandbox the email relay is
            skipped (webhook-only): the message is stored and the webhook fires,
            but no email is sent.
          example: operator_to_partner
    MessageListResponse:
      allOf:
        - $ref: '#/components/schemas/Page'
        - type: object
          required:
            - data
          properties:
            data:
              type: array
              description: Messages for the current page, ordered by sentAt ascending.
              items:
                $ref: '#/components/schemas/Message'
    Message:
      type: object
      required:
        - id
        - body
        - direction
        - sentAt
      properties:
        id:
          type: string
          description: Unique message identifier
          example: msg_abc123
        subject:
          type: string
          nullable: true
          description: Email subject line
          example: Table for Jane Smith
        body:
          type: string
          description: Message body text
          example: Could you please accommodate a high chair for us?
        senderEmail:
          type: string
          nullable: true
          description: Email address of the sender
          example: partner@example.com
        senderName:
          type: string
          nullable: true
          description: Display name of the sender
          example: Jane Smith
        direction:
          type: string
          enum:
            - operator_to_partner
            - partner_to_operator
          description: >
            Absolute message direction: `operator_to_partner` = the venue
            operator sent it; `partner_to_operator` = the integration partner
            sent it.
          example: partner_to_operator
        attachments:
          type: array
          nullable: true
          description: >-
            Attachment metadata (bytes are exchanged over email, not via this
            API).
          items:
            $ref: '#/components/schemas/MessageAttachment'
        sentAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the message was sent
    MessageAttachment:
      type: object
      required:
        - filename
      properties:
        filename:
          type: string
          description: Attachment file name
          example: floorplan.pdf
        contentType:
          type: string
          nullable: true
          description: MIME content type
          example: application/pdf
    Page:
      type: object
      properties:
        meta:
          type: object
          properties:
            currentPage:
              type: integer
              description: The current page number based on `limit` and `offset`.
            pageSize:
              type: integer
              description: The number of items returned per page.
            totalItems:
              type: integer
              description: The total number of items available.
            totalPages:
              type: integer
              description: The total number of pages.
    ClientError:
      type: object
      properties:
        type:
          type: string
          format: uri
          description: >-
            A URI reference [rfc9457] that identifies the problem type. This
            specification encourages that, when dereferenced, it provides
            human-readable documentation for the problem type (e.g., using HTML
            [W3C.REC-html5-20141028]). When this member is not present, its
            value is assumed to be "about:blank".
        title:
          type: string
          description: >-
            A short, human-readable summary of the problem type. It SHOULD NOT
            change from occurrence to occurrence of the problem, except for
            purposes of localization.
        status:
          type: integer
          format: int32
          description: >-
            The HTTP status code [rfc9457, Section 3.1.12] generated by the
            origin server for this occurrence of the problem.
        detail:
          type: string
          description: >-
            A human-readable explanation specific to this occurrence of the
            problem.
        code:
          type: string
          description: >-
            Error code in the format RESOURCE-X-NNN where X is R (retryable) or
            N (non-retryable). See ErrorCatalog.md for all error codes.
          example: VALID-N-001
        isRetryable:
          type: boolean
          description: >-
            Indicates whether the error is retryable. If true, the request may
            succeed if retried. If false, the request will fail again with the
            same input.
        traceId:
          type: string
          description: Request trace identifier for debugging and correlation purposes.
          example: 0HNJ2BG2TU3BU:00000001
        errors:
          type: object
          nullable: true
          additionalProperties:
            type: array
            items:
              type: string
          description: >-
            Validation errors dictionary mapping field names to arrays of error
            messages. Only present for validation errors (VALID-N-001).
          example:
            Date:
              - Availability date cannot be in the past.
            EndTime:
              - EndTime must be after StartTime.
      required:
        - title
        - status
        - code
        - isRetryable
    ServerError:
      type: object
      properties:
        type:
          type: string
          format: uri
          description: >-
            A URI reference [rfc9457] that identifies the problem type. This
            specification encourages that, when dereferenced, it provides
            human-readable documentation for the problem type (e.g., using HTML
            [W3C.REC-html5-20141028]). When this member is not present, its
            value is assumed to be "about:blank".
        title:
          type: string
          description: >-
            A short, human-readable summary of the problem type. It SHOULD NOT
            change from occurrence to occurrence of the problem, except for
            purposes of localization.
        status:
          type: integer
          format: int32
          description: >-
            The HTTP status code [rfc9457, Section 3.1.12] generated by the
            origin server for this occurrence of the problem.
        detail:
          type: string
          description: >-
            A human-readable explanation specific to this occurrence of the
            problem.
        code:
          type: string
          description: >-
            Error code in the format RESOURCE-X-NNN where X is R (retryable) or
            N (non-retryable). See ErrorCatalog.md for all error codes.
          example: BOOK-N-001
        isRetryable:
          type: boolean
          description: >-
            Indicates whether the error is retryable. If true, the request may
            succeed if retried. If false, the request will fail again with the
            same input.
        traceId:
          type: string
          description: Request trace identifier for debugging and correlation purposes.
          example: 0HNJ2BG2TU3BU:00000001
      required:
        - title
        - status
        - code
        - isRetryable
  securitySchemes:
    Live:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://auth.bookabletech.com/oauth/token
          scopes:
            venue-booking:messaging:read: Grants read access to booking messages
            venue-booking:messaging:write: Grants access to send booking messages
      description: |
        This API uses OAuth2 client credentials flow via Auth0.
        **Important:** Include the `audience` parameter in your token request.
        ```
        POST https://auth.bookabletech.com/oauth/token
        {
          "grant_type": "client_credentials",
          "client_id": "YOUR_CLIENT_ID",
          "client_secret": "YOUR_CLIENT_SECRET",
          "audience": "api.bookabletech.com"
        }
        ```
  parameters:
    CorrelationID:
      in: header
      name: X-Correlation-ID
      required: false
      description: >
        An optional ID which you can use to track transactions across multiple
        systems. It can have any value, but we recommend avoiding `.`
        characters.

        Mirrored back in a response header.
      schema:
        type: string
        example: 11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA
    PartnerReference:
      in: header
      name: X-Partner-Reference
      required: false
      description: >
        Required for Bookable Agents. Specifies the partner reference for whom
        the booking is being made.

        This header must contain a valid partner reference when making requests
        with the `agent:write` scope.
      schema:
        type: string
  headers:
    RateLimitLimit:
      description: Maximum number of requests allowed per window.
      schema:
        type: integer
      example: 200
    RateLimitRemaining:
      description: Number of requests remaining in the current window.
      schema:
        type: integer
      example: 150
    RateLimitReset:
      description: >-
        Unix timestamp (seconds since epoch) when the current rate-limit window
        resets.
      schema:
        type: integer
        format: int64
      example: 1741651200
  responses:
    BadRequest:
      description: Bad Request - The request was invalid or cannot be served
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ClientError'
          example:
            type: https://tools.ietf.org/html/rfc9110#section-15.5.1
            title: Validation Failed
            status: 400
            detail: One or more validation errors occurred.
            code: VALID-N-001
            isRetryable: false
            traceId: 0HNJ2BG2TU3BU:00000001
            errors:
              Date:
                - Availability date cannot be in the past.
        text/toon:
          schema:
            $ref: '#/components/schemas/ClientError'
          example: |
            type: "https://tools.ietf.org/html/rfc9110#section-15.5.1"
            title: Validation Failed
            status: 400
            detail: One or more validation errors occurred.
            code: VALID-N-001
            isRetryable: false
            traceId: "0HNJ2BG2TU3BU:00000001"
            errors.Date[1]: "Availability date cannot be in the past."
    Unauthorized:
      description: Unauthorized - Authentication is required
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ClientError'
          example:
            type: https://tools.ietf.org/html/rfc9110#section-15.5.5
            title: Invalid User Credentials
            status: 401
            detail: The provided credentials are invalid
            code: USER-N-003
            isRetryable: false
            traceId: 0HNJ2BG2TU3BU:00000002
        text/toon:
          schema:
            $ref: '#/components/schemas/ClientError'
          example: |
            type: "https://tools.ietf.org/html/rfc9110#section-15.5.5"
            title: Invalid User Credentials
            status: 401
            detail: The provided credentials are invalid
            code: USER-N-003
            isRetryable: false
            traceId: "0HNJ2BG2TU3BU:00000002"
    Forbidden:
      description: Forbidden - The request is understood but access is denied
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ClientError'
          example:
            type: https://tools.ietf.org/html/rfc9110#section-15.5.4
            title: Venue Not Mapped to Partner
            status: 403
            detail: The requested venue is not mapped to the current partner
            code: PMAP-N-001
            isRetryable: false
            traceId: 0HNJ2BG2TU3BU:00000003
        text/toon:
          schema:
            $ref: '#/components/schemas/ClientError'
          example: |
            type: "https://tools.ietf.org/html/rfc9110#section-15.5.4"
            title: Venue Not Mapped to Partner
            status: 403
            detail: The requested venue is not mapped to the current partner
            code: PMAP-N-001
            isRetryable: false
            traceId: "0HNJ2BG2TU3BU:00000003"
    NotFound:
      description: Not Found - The requested resource was not found
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ClientError'
          example:
            type: https://tools.ietf.org/html/rfc9110#section-15.5.5
            title: Venue Not Found
            status: 404
            detail: The requested venue could not be found
            code: VENUE-N-001
            isRetryable: false
            traceId: 0HNJ2BG2TU3BU:00000004
        text/toon:
          schema:
            $ref: '#/components/schemas/ClientError'
          example: |
            type: "https://tools.ietf.org/html/rfc9110#section-15.5.5"
            title: Venue Not Found
            status: 404
            detail: The requested venue could not be found
            code: VENUE-N-001
            isRetryable: false
            traceId: "0HNJ2BG2TU3BU:00000004"
    InternalServerError:
      description: Internal Server Error - An unexpected error occurred
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ServerError'
          example:
            type: https://tools.ietf.org/html/rfc9110#section-15.6.1
            title: Internal Server Error
            status: 500
            detail: An unexpected error occurred while processing the request
            code: SYS-N-001
            isRetryable: false
            traceId: 0HNJ2BG2TU3BU:00000007
        text/toon:
          schema:
            $ref: '#/components/schemas/ServerError'
          example: |
            type: "https://tools.ietf.org/html/rfc9110#section-15.6.1"
            title: Internal Server Error
            status: 500
            detail: An unexpected error occurred while processing the request
            code: SYS-N-001
            isRetryable: false
            traceId: "0HNJ2BG2TU3BU:00000007"
    UnprocessableEntity:
      description: >-
        Unprocessable Entity - The request is well-formed but contains semantic
        errors
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ClientError'
          example:
            type: https://api.bookabletech.com/problems/unprocessable-entity
            title: Unprocessable Entity
            status: 422
            detail: The request is well-formed but contains semantic errors
            code: VALID-N-001
            isRetryable: false
        text/toon:
          schema:
            $ref: '#/components/schemas/ClientError'
          example: |
            type: "https://api.bookabletech.com/problems/unprocessable-entity"
            title: Unprocessable Entity
            status: 422
            detail: The request is well-formed but contains semantic errors
            code: VALID-N-001
            isRetryable: false
