openapi: 3.2.0 info: title: Withlocals Partner Webhooks API version: 1.0.0 description: 'A single contract for OTA partners and other commercial integrations. Covers products, availability, and the create -> amend -> cancel booking lifecycle. `POST /bookings` creates a `CONFIRMED` booking.' contact: name: Withlocals Partner Integrations email: partners@withlocals.com x-logo: url: ./assets/logo.svg altText: Withlocals href: https://www.withlocals.com backgroundColor: '#ffffff' servers: - url: https://test-api.withlocals.com/v1/partner description: Test / Sandbox security: - bearerAuth: [] tags: - name: Webhooks description: 'Out-of-band events delivered by Withlocals to a partner-registered URL. Register at `PUT /webhooks`. See the *Webhooks* section for the delivery contract and the events Withlocals sends.' paths: /webhooks: get: tags: - Webhooks operationId: getWebhook summary: Get current webhook registration description: 'Returns the partner''s currently registered webhook URL. The signing `secret` is **not** returned — it is only revealed once, on first registration. Possible errors: `NOT_FOUND` (no webhook registered), `UNAUTHORIZED`.' responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Webhook' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' put: tags: - Webhooks operationId: setWebhook summary: Register or update the webhook URL description: 'Registers a webhook URL on first call (`201`), or updates an existing registration (`200`). On first registration, the response includes a `secret` — save it. Withlocals will not return it again. Subsequent reads and updates omit the secret. To rotate, `DELETE /webhooks` and re-register. The URL must be HTTPS and reachable from the public internet. Possible errors: `BAD_REQUEST` (URL invalid or not HTTPS), `UNAUTHORIZED`.' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WebhookRegistration' responses: '200': description: Updated content: application/json: schema: $ref: '#/components/schemas/Webhook' '201': description: Registered (first time — `secret` included in response) content: application/json: schema: $ref: '#/components/schemas/Webhook' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' delete: tags: - Webhooks operationId: deleteWebhook summary: Remove the webhook registration description: 'Unregisters the partner''s webhook. No further events will be delivered until a new URL is registered via `PUT /webhooks`. Possible errors: `NOT_FOUND` (nothing registered), `UNAUTHORIZED`.' responses: '204': description: Removed '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' webhooks: bookingCancelled: post: tags: - Webhooks operationId: bookingCancelledWebhook summary: Booking cancelled description: 'Sent to the partner''s registered webhook URL when a booking transitions to `CANCELLED`, regardless of who initiated the cancellation (guest, host, admin, or system no-show). **Delivery semantics:** - HTTP `POST` with `Content-Type: application/json`. - **At-least-once.** Retries on non-2xx for ~24h with exponential backoff. Partners MUST dedupe on `eventId`. - **Acknowledge fast.** Any 2xx response stops retries. Process the event asynchronously if work is slow — Withlocals retries after 10s. **Signature verification:** Every delivery includes an `X-Withlocals-Signature` header of the form `t={unix-seconds},v1={hex-hmac}`. Compute `HMAC-SHA256(secret, "{t}.{raw-body}")` and constant-time-compare to `v1`. Reject if the timestamp is older than 5 minutes (replay window).' security: [] parameters: - name: X-Withlocals-Signature in: header required: true description: 'HMAC-SHA256 signature with timestamp. Format: `t={unix-seconds},v1={hex-digest}`. ' schema: type: string example: t=1750000000,v1=5257a869e7ecebeda32affa62cdca3fa... requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BookingCancelledEvent' responses: 2XX: description: Acknowledged — Withlocals stops retrying. components: responses: Unauthorized: description: Missing or invalid Bearer token. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: UNAUTHORIZED errorMessage: Missing or invalid API token requestId: req_7c2b18d1 BadRequest: description: The request is malformed or fails validation. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: BAD_REQUEST errorMessage: '`date` is required.' requestId: req_7c2b18d2 NotFound: description: The resource does not exist or is not visible to this partner. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: NOT_FOUND errorMessage: No product with that id. requestId: req_7c2b18d3 schemas: Error: type: object description: 'Error envelope. ' required: - error - errorMessage properties: error: type: string description: Stable error code. Partners are expected to switch on this value. enum: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - CONFLICT - PRECONDITION_FAILED - INTERNAL_ERROR errorMessage: type: string description: Human-readable message. Not stable; do not parse. example: Hold expired before confirmation. requestId: type: string description: Trace id for support requests. example: req_5f3a9b71 WebhookRegistration: type: object description: 'Request body for `PUT /webhooks`. Only `url` is accepted — the signing `secret` is generated by Withlocals and returned (once) in the `201` response, never supplied by the partner. ' required: - url properties: url: type: string format: uri description: HTTPS URL where webhook events are POSTed. example: https://partner.example.com/withlocals/webhook BookingCancelledEvent: type: object description: 'Payload of the `booking.cancelled` webhook. Thin by design — partner refetches the full booking via `GET /bookings/{bookingId}` if needed. `status` and `cancellationReason` mirror the booking API: `status` is the lifecycle state (always `CANCELLED` for this event) and `cancellationReason` identifies who initiated the cancellation. ' required: - eventId - eventType - occurredAt - bookingId - status - cancellationReason properties: eventId: type: string format: uuid description: 'Unique per delivery. Partners SHOULD dedupe on this to tolerate at-least-once delivery. ' example: 9c3b5d8e-1f4a-4c2b-8d6e-7f0a1b2c3d4e eventType: type: string enum: - booking.cancelled example: booking.cancelled occurredAt: type: string format: date-time description: When the cancellation happened. example: '2026-06-15T09:42:11Z' bookingId: type: string format: uuid description: Withlocals booking id. Refetch via `GET /bookings/{bookingId}` for full state. example: 7c9e6679-7425-40de-944b-e07fc1f90ae7 status: type: string enum: - CANCELLED description: Booking lifecycle state. Always `CANCELLED` for this event. example: CANCELLED cancellationReason: type: string enum: - CANCELLEDBYGUEST - CANCELLEDBYHOST - CANCELLEDBYADMIN - HOSTNOSHOW description: 'Present when `status=CANCELLED`. Partner-initiated cancellations (`DELETE /bookings/{bookingId}`) always set `CANCELLEDBYGUEST`; the other values appear on bookings cancelled internally by the host, admin, or marked as a no-show. ' Webhook: type: object description: 'Webhook registration. A single URL receives all event types; partners dispatch by the `eventType` field in each delivered payload. The `secret` is returned **only on first registration** (the `201` response of `PUT /webhooks`). Subsequent reads and updates exclude it — save it on receipt. To rotate, `DELETE /webhooks` then `PUT /webhooks` again. ' required: - url properties: url: type: string format: uri description: HTTPS URL where webhook events are POSTed. example: https://partner.example.com/withlocals/webhook secret: type: string description: 'HMAC-SHA256 signing secret. Used to verify `X-Withlocals-Signature` on delivered events. Returned only on first registration. ' example: whsec_a1b2c3d4e5f6a7b8c9d0e1f2 securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: opaque description: "Per-partner opaque API token issued by Withlocals. Send on every\nrequest as:\n\n Authorization: Bearer \n"