{
  "openapi": "3.0.3",
  "info": {
    "title": "Register to Webhook Events API",
    "version": "1.3.0",
    "description": "The Webhook APIs is a REST API designed to manage webhooks for booking notifications.\nIt allows partners to register, update, and delete webhook endpoints, enabling them\nto receive real-time booking event updates.\n\n## Receiving events\n\nOnce a callback URL is registered, Bookable POSTs a JSON `BookingNotification` to it for each event.\nDelivery is **at-least-once**: the same event may be delivered more than once, so consumers must be\nidempotent (booking events by booking `id`, messages by message `id`).\n\nEach request carries an `X-API-Key` header containing a lowercase hex **HMAC-SHA256 signature** of the\nraw request body, keyed with the `secretKey` returned at registration. Verify it by recomputing the\nHMAC over the received body and comparing.\n\nEvent types (`eventType`):\n- `booking.updated` — a booking was created or changed (payload in `bookings`).\n- `message.received` — one or more messages for a booking (payload in `messages`). Emitted only for\n  `operator_to_partner` messages (the venue operator sent it, delivered to the partner); filter on\n  `subject`/`senderEmail`.\n",
    "contact": {
      "name": "Bookable",
      "url": "https://www.bookabletech.com",
      "email": "hello@bookabletech.com"
    }
  },
  "servers": [
    {
      "url": "https://api.bookabletech.com",
      "description": "Live"
    },
    {
      "url": "https://api-sandbox.bookabletech.com",
      "description": "Sandbox"
    }
  ],
  "tags": [
    {
      "name": "webhook",
      "description": "Operations related to webhook management"
    }
  ],
  "paths": {
    "/webhooks": {
      "post": {
        "summary": "Register a webhook endpoint for booking notifications",
        "operationId": "RegisterWebhook",
        "tags": [
          "webhook"
        ],
        "requestBody": {
          "description": "Webhook registration request",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookRegistrationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook successfully registered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookRegistrationResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ClientError"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "Live": []
          },
          {
            "Sandbox": []
          }
        ]
      },
      "put": {
        "summary": "Update the webhook callback URL of the specific distributor logged",
        "operationId": "UpdateWebhook",
        "tags": [
          "webhook"
        ],
        "requestBody": {
          "description": "Webhook update request. Only the `callbackUrl` can be modified.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookRegistrationRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Webhook successfully updated"
          },
          "4XX": {
            "$ref": "#/components/responses/ClientError"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "Live": []
          },
          {
            "Sandbox": []
          }
        ]
      },
      "delete": {
        "summary": "Delete the webhook for a specific distributor",
        "operationId": "DeleteWebhook",
        "tags": [
          "webhook"
        ],
        "responses": {
          "204": {
            "description": "Webhook successfully deleted"
          },
          "4XX": {
            "$ref": "#/components/responses/ClientError"
          },
          "5XX": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "Live": []
          },
          {
            "Sandbox": []
          }
        ]
      }
    },
    "/webhooks/test": {
      "post": {
        "summary": "Trigger a test webhook event",
        "operationId": "TriggerTestWebhook",
        "deprecated": true,
        "tags": [
          "webhook"
        ],
        "description": "**Deprecated — scheduled for removal in `2.0.0`.**\n\nDelivers a synthetic `booking.updated` notification (a sample booking) to your registered `callbackUrl`, signed with `X-API-Key` exactly like a real event, so you can verify your endpoint and HMAC handling. It is not tied to any real booking.\n\nFor ongoing integration testing use the **sandbox self-echo** flow instead: in the sandbox environment every booking you create, amend, or cancel automatically fires the corresponding webhook to your registered sandbox callback URL — no separate trigger needed.\n",
        "responses": {
          "202": {
            "description": "Test webhook event accepted and delivered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TestWebhookResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ClientError"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "Live": []
          },
          {
            "Sandbox": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "WebhookRegistrationRequest": {
        "type": "object",
        "required": [
          "callbackUrl"
        ],
        "properties": {
          "callbackUrl": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS URL to be called when a booking event occurs",
            "example": "https://partner.example.com/webhooks/bookings",
            "nullable": false
          }
        }
      },
      "WebhookRegistrationResponse": {
        "type": "object",
        "properties": {
          "secretKey": {
            "type": "string",
            "description": "Unique secret key for the registered webhook to verify data",
            "example": "123e4567-e89b-12d3-a456-426655440000"
          },
          "callbackUrl": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS URL to be called when a booking event occurs",
            "example": "https://partner.example.com/webhooks/bookings"
          }
        }
      },
      "TestWebhookResponse": {
        "type": "object",
        "description": "Result of a deprecated `POST /webhooks/test` call.",
        "properties": {
          "message": {
            "type": "string",
            "description": "Human-readable confirmation that the test event was delivered.",
            "example": "Test webhook event delivered to the registered callback URL"
          },
          "eventId": {
            "type": "string",
            "description": "Unique identifier for the test event, recorded in the webhook_events audit log.",
            "example": "evt_test_123e4567"
          }
        }
      },
      "BookingNotification": {
        "type": "object",
        "description": "The payload POSTed to the registered callback URL. Signed with `X-API-Key` (HMAC-SHA256 of the body).\n",
        "required": [
          "timestamp",
          "eventType"
        ],
        "properties": {
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When the notification was generated (UTC, ISO 8601)."
          },
          "eventType": {
            "type": "string",
            "enum": [
              "booking.updated",
              "message.received"
            ],
            "description": "The kind of event. Determines whether `bookings` or `messages` is populated."
          },
          "bookings": {
            "type": "array",
            "description": "Present for `booking.updated`. The affected booking(s).",
            "items": {
              "$ref": "#/components/schemas/Booking"
            }
          },
          "messages": {
            "type": "array",
            "description": "Present for `message.received`. One or more messages for a single booking.",
            "items": {
              "$ref": "#/components/schemas/MessagePayload"
            }
          }
        }
      },
      "Booking": {
        "type": "object",
        "description": "A booking affected by a `booking.updated` event.",
        "required": [
          "id",
          "compositeId",
          "date",
          "time",
          "partySize",
          "status",
          "reference",
          "firstName",
          "lastName",
          "email",
          "duration",
          "createdDate",
          "lastUpdate"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "A globally unique identifier for the reservation.",
            "example": "29|X9|275cc44dd2e2496fba44857c9257443a|B"
          },
          "compositeId": {
            "type": "string",
            "description": "A globally unique identifier for the venue/product.",
            "example": "29|X9|275cc44dd2e2496fba44857c9257443a|d99128c546b34b619c4477b712869f2b"
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The date of the reservation (YYYY-MM-DD).",
            "example": "2026-06-25"
          },
          "time": {
            "type": "string",
            "format": "time",
            "description": "The time of the reservation (HH:MM:SS).",
            "example": "19:30:00"
          },
          "partySize": {
            "type": "integer",
            "minimum": 1,
            "description": "Number of guests for the reservation.",
            "example": 4
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_progress",
              "confirmed",
              "cancelled",
              "deleted",
              "lost"
            ],
            "description": "Current status of the reservation."
          },
          "reference": {
            "type": "string",
            "description": "Reference code for the reservation as originally provided by the distributor.",
            "example": "REF-20260617-001"
          },
          "firstName": {
            "type": "string",
            "description": "Guest's first name.",
            "example": "John"
          },
          "lastName": {
            "type": "string",
            "description": "Guest's last name.",
            "example": "Doe"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address of the guest.",
            "example": "john.doe@example.com"
          },
          "phone": {
            "type": "string",
            "nullable": true,
            "description": "Phone number of the guest.",
            "example": "+1234567890"
          },
          "duration": {
            "type": "integer",
            "description": "Duration of the reservation in minutes.",
            "example": 120
          },
          "createdDate": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the reservation was created."
          },
          "lastUpdate": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp of the last reservation update."
          }
        }
      },
      "MessagePayload": {
        "type": "object",
        "description": "A single message delivered to the partner for one of their bookings.",
        "required": [
          "id",
          "bookingId",
          "body",
          "sentAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable, globally unique message id. Use it to **deduplicate** — the same message may be delivered more than once (at-least-once delivery with retries).\n",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "bookingId": {
            "type": "string",
            "description": "The composite booking id this message belongs to.",
            "example": "29|CO|6a3e866a4989b3b73d78f011|B"
          },
          "body": {
            "type": "string",
            "description": "Message body text."
          },
          "subject": {
            "type": "string",
            "nullable": true,
            "description": "Email subject line — the primary signal for filtering messages from other mail."
          },
          "senderName": {
            "type": "string",
            "nullable": true,
            "description": "Display name of the sender."
          },
          "senderEmail": {
            "type": "string",
            "nullable": true,
            "description": "Email address of the sender."
          },
          "direction": {
            "type": "string",
            "enum": [
              "operator_to_partner"
            ],
            "nullable": true,
            "description": "Absolute message direction. The webhook fires only for `operator_to_partner` messages (the venue operator sent it, delivered to the partner), so this is always `operator_to_partner` when present.\n"
          },
          "replyTo": {
            "type": "string",
            "format": "email",
            "nullable": true,
            "description": "The address to send your reply to. Email it (from anywhere) and Bookable relays your message to the venue operator for this booking. Stable per booking.\n",
            "example": "booking-op7k2@mail.bookabletech.com"
          },
          "attachments": {
            "type": "array",
            "nullable": true,
            "description": "Attachment metadata. The attachment bytes are exchanged over email, not in this webhook.",
            "items": {
              "type": "object",
              "required": [
                "filename"
              ],
              "properties": {
                "filename": {
                  "type": "string",
                  "description": "Attachment file name."
                },
                "contentType": {
                  "type": "string",
                  "nullable": true,
                  "description": "MIME content type."
                }
              }
            }
          },
          "sentAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the message was sent."
          }
        }
      },
      "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"
          }
        },
        "description": "This API uses OAuth2 client credentials flow via Auth0.\n**Important:** Include the `audience` parameter in your token request.\nExample 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"
      },
      "Sandbox": {
        "type": "oauth2",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://auth-sandbox.bookabletech.com/oauth/token"
          }
        },
        "description": "Sandbox equivalent of `Live` — same OAuth2 client credentials flow, against the sandbox\nAuth0 tenant, for use with the `https://api-sandbox.bookabletech.com` server.\n**Important:** Include the `audience` parameter in your token request.\nExample token request:\n```\nPOST https://auth-sandbox.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"
      }
    },
    "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": {
      "ClientError": {
        "description": "Client error",
        "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"
            }
          },
          "text/toon": {
            "schema": {
              "$ref": "#/components/schemas/ClientError"
            }
          }
        }
      },
      "ServerError": {
        "description": "Server error",
        "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"
            }
          },
          "text/toon": {
            "schema": {
              "$ref": "#/components/schemas/ServerError"
            }
          }
        }
      }
    }
  }
}