{
  "openapi": "3.0.3",
  "info": {
    "title": "Messaging API",
    "version": "2.0.0",
    "description": "API for retrieving the operator↔partner conversation stored against a booking.\n\nBookable relays the conversation between the venue operator and the integration partner over email\n(both parties email a per-booking conversation alias and Bookable forwards each message on).\n\nParticipants can take part in two ways:\n- **By email** — reply to the relay email from their normal mail client (attachments supported this way only).\n- **Programmatically** — `POST /bookings/{bookingId}/messages` to send a plain-text message. Bookable stores\n  it and relays it on to the other participant over the same conversation channel. `GET` returns the stored\n  conversation stack.\n\nThe send direction is derived from the caller's role: an Integration Partner sends to the venue operator\n(`partner_to_operator`); a venue group operator sends to the integration partner (`operator_to_partner`).\nEach stored message carries this absolute `direction`.\n\n**Pagination:** Uses page-based pagination via `pageNumber` and `pageSize`. Results are ordered\nby `sentAt` ascending (oldest first).\n\n**Availability:** Real email relay is not available in the Sandbox environment. In Sandbox only the\nwebhook-only test flow is reachable — see the `direction` override on `SendMessageRequest`, which stores\nthe message and fires the `message.received` webhook to your registered sandbox callback without sending\nany email.\n",
    "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\nmessage and relays it over email to the other participant, who can reply from their normal mail client.\n\nThe delivery target is derived from the caller's role — there is no `recipient` field:\n- **Integration Partner** — delivered to the venue operator (`direction: partner_to_operator`).\n- **Venue group operator** — delivered to the integration partner (`direction: operator_to_partner`).\n\nAttachments cannot be sent via this endpoint; add them by replying to the relay email instead.\n",
        "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\n`sentAt` ascending (oldest first). Each message's `direction` states who sent it and who received it.\n\nAccess is scoped to the caller's venue group: Integration Partners see only messages for bookings\nbelonging to their associated venues.\n",
        "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.\n",
            "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.\n",
            "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.\n**Important:** Include the `audience` parameter in your token request.\n```\nPOST https://auth.bookabletech.com/oauth/token\n{\n  \"grant_type\": \"client_credentials\",\n  \"client_id\": \"YOUR_CLIENT_ID\",\n  \"client_secret\": \"YOUR_CLIENT_SECRET\",\n  \"audience\": \"api.bookabletech.com\"\n}\n```\n"
      }
    },
    "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.\nMirrored back in a response header.\n",
        "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.\nThis header must contain a valid partner reference when making requests with the `agent:write` scope.\n",
        "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\"\ntitle: Validation Failed\nstatus: 400\ndetail: One or more validation errors occurred.\ncode: VALID-N-001\nisRetryable: false\ntraceId: \"0HNJ2BG2TU3BU:00000001\"\nerrors.Date[1]: \"Availability date cannot be in the past.\"\n"
          }
        }
      },
      "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\"\ntitle: Invalid User Credentials\nstatus: 401\ndetail: The provided credentials are invalid\ncode: USER-N-003\nisRetryable: false\ntraceId: \"0HNJ2BG2TU3BU:00000002\"\n"
          }
        }
      },
      "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\"\ntitle: Venue Not Mapped to Partner\nstatus: 403\ndetail: The requested venue is not mapped to the current partner\ncode: PMAP-N-001\nisRetryable: false\ntraceId: \"0HNJ2BG2TU3BU:00000003\"\n"
          }
        }
      },
      "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\"\ntitle: Venue Not Found\nstatus: 404\ndetail: The requested venue could not be found\ncode: VENUE-N-001\nisRetryable: false\ntraceId: \"0HNJ2BG2TU3BU:00000004\"\n"
          }
        }
      },
      "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\"\ntitle: Internal Server Error\nstatus: 500\ndetail: An unexpected error occurred while processing the request\ncode: SYS-N-001\nisRetryable: false\ntraceId: \"0HNJ2BG2TU3BU:00000007\"\n"
          }
        }
      },
      "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\"\ntitle: Unprocessable Entity\nstatus: 422\ndetail: The request is well-formed but contains semantic errors\ncode: VALID-N-001\nisRetryable: false\n"
          }
        }
      }
    }
  }
}