openapi: 3.2.0 info: title: Withlocals Partner Bookings 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: Bookings description: Reserve, confirm, amend, cancel, and read bookings. paths: /bookings: get: tags: - Bookings operationId: listBookings summary: List bookings description: 'Returns bookings matching the supplied filters. Filters combine with `AND`. All filters are optional; with none supplied, a default date window is applied (roughly the last month through the next year). Use `date` for an exact day, or `fromDate` + `toDate` for a range — `date` is mutually exclusive with the range pair (supplying both returns `400`). Possible errors: `BAD_REQUEST`, `UNAUTHORIZED`.' parameters: - name: partnerReference in: query required: false description: Partner's own booking id. schema: type: string example: XYZ-001 - name: date in: query required: false description: 'Single booking date (`YYYY-MM-DD`). Mutually exclusive with `fromDate` / `toDate`. ' schema: type: string format: date example: '2026-07-15' - name: fromDate in: query required: false description: Range start, inclusive. Pair with `toDate`. schema: type: string format: date example: '2026-07-01' - name: toDate in: query required: false description: Range end, inclusive. Pair with `fromDate`. schema: type: string format: date example: '2026-07-31' responses: '200': description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Booking' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' post: tags: - Bookings operationId: createBooking summary: Create a confirmed booking description: 'Creates a `CONFIRMED` booking. Use `Idempotency-Key` to make retries safe. Possible errors: `BAD_REQUEST`, `UNAUTHORIZED`, `NOT_FOUND` (product unknown or not bookable), `CONFLICT` (slot no longer available).' parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateBookingRequest' examples: golden: $ref: '#/components/examples/create-booking-request' responses: '201': description: Created (`CONFIRMED`) content: application/json: schema: $ref: '#/components/schemas/Booking' examples: golden: $ref: '#/components/examples/booking-confirmed' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' /bookings/{bookingId}: get: tags: - Bookings operationId: getBooking summary: Get a single booking by id description: 'Reads a single booking by its Withlocals id. Ownership-scoped: a partner can only fetch bookings belonging to its own company. A booking owned by another partner returns `404` (not `403`), so existence is not revealed. Possible errors: `NOT_FOUND`, `UNAUTHORIZED`.' parameters: - $ref: '#/components/parameters/BookingId' responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/Booking' examples: confirmed: $ref: '#/components/examples/booking-confirmed' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' patch: tags: - Bookings operationId: amendBooking summary: Amend a booking (date / time and / or party size) description: 'Partial update. May trigger an internal host transfer if the original host is unavailable on the new date. Possible errors: `BAD_REQUEST`, `NOT_FOUND`, `PRECONDITION_FAILED` (past cut-off), `UNAUTHORIZED`.' parameters: - $ref: '#/components/parameters/BookingId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AmendRequest' examples: golden: $ref: '#/components/examples/amend-request' responses: '200': description: Amended content: application/json: schema: $ref: '#/components/schemas/Booking' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '412': $ref: '#/components/responses/PreconditionFailed' delete: tags: - Bookings operationId: cancelBooking summary: Cancel a booking description: 'Cancels a booking. The booking **persists** with `status=CANCELLED` and a `cancellationReason` of `CANCELLEDBYGUEST` (partner-initiated cancellations are always attributed to the guest); a subsequent `GET` still returns it. The cancellation `reason` may be sent in the request body, or as the `reason` query parameter for clients that cannot send `DELETE` bodies. When supplied, it must be one of the `GuestCancellationReason` values, otherwise the request returns `400`. Possible errors: `BAD_REQUEST` (invalid `reason`), `NOT_FOUND`, `UNAUTHORIZED`.' parameters: - $ref: '#/components/parameters/BookingId' - name: reason in: query required: false description: Fallback for clients that cannot send a `DELETE` body. schema: $ref: '#/components/schemas/GuestCancellationReason' requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/CancelRequest' responses: '200': description: Cancelled content: application/json: schema: $ref: '#/components/schemas/Booking' examples: golden: $ref: '#/components/examples/cancel-response-cancelled' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' components: schemas: GuestCancellationReason: type: string description: 'Why the guest is cancelling. Sent on partner-initiated cancellations (`DELETE /bookings/{bookingId}`). | Value | Meaning | |---|---| | `MY_PLANS_CHANGED` | My plans changed | | `FOUND_BETTER_OPTION` | Found better option | | `HOST_ASKED_TO_CANCEL`| Host asked to cancel | | `HOST_NOT_RESPONDING` | Host not responding | | `OTHER` | Other | ' enum: - MY_PLANS_CHANGED - FOUND_BETTER_OPTION - HOST_ASKED_TO_CANCEL - HOST_NOT_RESPONDING - OTHER example: MY_PLANS_CHANGED BookingStatus: type: string description: 'Partner-facing booking lifecycle. Translated from the internal `TripStatus` by a single mapper (`PartnerBookingStatus.fromTripStatus`). v1 lifecycle (no holds — `POST /bookings` creates a CONFIRMED booking directly): | Partner status | From internal `TripStatus` | |---|---| | `CONFIRMED` | `CONFIRMED`, `COMPLETED` | | `CANCELLED` | `CANCELLEDBYGUEST` / `HOST` / `ADMIN` / `HOSTNOSHOW` / `COUPON` (with `cancellationReason`) | Internal statuses that belong to the guest negotiation flow (`PROPOSAL`, `PAIDPROPOSAL`, `UNCONFIRMED`, `REJECTED`, `DELETED`) are not partner-visible. `RESERVATION` / `TIMEDOUT` / `EXPIRED` cannot occur in v1 because there is no reserve step. ' enum: - CONFIRMED - CANCELLED CreateBookingRequest: type: object description: 'Body of `POST /bookings` — creates a `CONFIRMED` booking. ' required: - productId - date - time - numberOfAdults - mainGuest properties: productId: type: string format: uuid date: type: string format: date example: '2026-07-15' time: type: string pattern: ^[0-2][0-9]:[0-5][0-9]$ example: '10:00' numberOfAdults: type: integer minimum: 1 example: 2 numberOfChildren: type: integer minimum: 0 default: 0 mainGuest: $ref: '#/components/schemas/Guest' otherGuests: type: array items: $ref: '#/components/schemas/Guest' partnerReference: type: string description: Partner's own booking id. example: XYZ-001 specialRequest: type: string description: Free-text note from the guest, forwarded to the host. tourLanguage: type: string description: ISO-639-1 language code. example: en Guest: type: object description: A single guest on a booking. required: - firstName properties: firstName: type: string example: Anna lastName: type: string example: Kowalska email: type: string format: email example: anna@example.com phone: type: string example: '+31201234567' AmendRequest: type: object description: 'Body of `PATCH /bookings/{bookingId}`. Any subset of fields may be supplied. ' properties: date: type: string format: date time: type: string pattern: ^[0-2][0-9]:[0-5][0-9]$ numberOfAdults: type: integer minimum: 1 numberOfChildren: type: integer minimum: 0 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 CancelRequest: type: object description: Optional body of `DELETE /bookings/{bookingId}`. properties: reason: $ref: '#/components/schemas/GuestCancellationReason' MeetingPoint: type: object description: Where the experience starts. required: - name properties: name: type: string description: Short human-readable name. example: Hyakumanben Chion-ji Temple address: type: string description: Formatted address line. example: Japan, 〒600-8012 Kyoto, Shimogyo Ward, 四条大橋西詰 lat: type: number format: double description: Latitude in decimal degrees (WGS84). example: 35.0298797 lon: type: number format: double description: Longitude in decimal degrees (WGS84). example: 135.7807599 instructions: type: string description: Free-text guidance for finding the meeting spot. Booking: type: object description: 'A partner booking. Returned by create, amend, cancel, and read. When `status=CANCELLED`, `cancellationReason` is set and the record persists for partner refund handling. ' required: - id - status - productId - title - date - time - timeZone - createdAt properties: id: type: string format: uuid description: Withlocals booking id. partnerReference: type: string description: Partner's own booking id (internally `external_id`). example: XYZ-001 productId: type: string format: uuid title: type: string description: Denormalized product title at the time of booking. example: A Relaxed Morning at Hyakumanben Craft Market date: type: string format: date example: '2027-10-15' time: type: string pattern: ^[0-2][0-9]:[0-5][0-9]$ description: Local clock time at the experience location. example: 09:00 timeZone: type: string description: IANA time zone for `date` / `time`. example: Asia/Tokyo status: $ref: '#/components/schemas/BookingStatus' 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. ' cancellationDeadline: type: string format: date-time description: Latest moment the booking can be cancelled for a full refund. example: '2027-10-08T09:00:00Z' createdAt: type: string format: date-time description: When the booking was created. example: '2026-05-24T17:11:17Z' meetingPoint: $ref: '#/components/schemas/MeetingPoint' tourLanguage: type: string description: ISO-639-1 language code. example: en specialRequest: type: string description: Free-text note from the guest, forwarded to the host. mainGuest: type: object required: - firstName description: 'The main guest (traveller) on the booking. This is the lead traveller supplied as `mainGuest` on create. Absent when no main guest is recorded. ' properties: firstName: type: string example: Maria lastName: type: string example: Rossi phoneNumber: type: string description: Absent when the main guest has no phone number on their profile. example: '+31201234567' host: type: object required: - firstName description: Minimal host details for day-of identification. properties: firstName: type: string example: Ren phoneNumber: type: string description: Absent when the host has no phone number on their profile. example: '+31201234567' examples: booking-confirmed: summary: CONFIRMED booking returned by POST /bookings value: id: 7c9e6679-7425-40de-944b-e07fc1f90ae7 partnerReference: XYZ-001 productId: 1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed title: Hidden food gems of Amsterdam date: '2026-07-15' time: '10:00' timeZone: Europe/Amsterdam status: CONFIRMED cancellationDeadline: '2026-07-08T08:00:00Z' createdAt: '2026-05-28T13:42:11Z' meetingPoint: name: Café Brecht address: Weteringschans 157, 1017 SE Amsterdam, Netherlands lat: 52.3617 lon: 4.8907 tourLanguage: en mainGuest: firstName: Anna lastName: Jansen phoneNumber: '+31201234567' host: firstName: Carla phoneNumber: '+31209876543' amend-request: summary: Reschedule one day later, same start time value: date: '2026-07-16' time: '10:00' create-booking-request: summary: Book 2 adults at 10:00 on 15 Jul 2026 value: productId: 1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed date: '2026-07-15' time: '10:00' numberOfAdults: 2 numberOfChildren: 0 mainGuest: firstName: Anna lastName: Kowalska email: anna@example.com phone: '+31201234567' partnerReference: XYZ-001 tourLanguage: en cancel-response-cancelled: summary: Cancelled booking (persists with reason) value: id: 7c9e6679-7425-40de-944b-e07fc1f90ae7 partnerReference: XYZ-001 productId: 1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed title: Hidden food gems of Amsterdam date: '2026-07-15' time: '10:00' timeZone: Europe/Amsterdam status: CANCELLED cancellationReason: CANCELLEDBYGUEST createdAt: '2026-05-28T13:42:11Z' parameters: BookingId: name: bookingId in: path required: true description: Withlocals booking id (returned by `POST /bookings`). schema: type: string format: uuid IdempotencyKey: name: Idempotency-Key in: header required: false description: 'Client-generated key making the request safe to retry. Repeated requests with the same key return the original response. ' schema: type: string maxLength: 255 responses: Conflict: description: 'Conflicting state. Common causes: the hold has expired, the booking has already been confirmed or cancelled, or a concurrent change occurred. ' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: CONFLICT errorMessage: Hold expired before confirmation. requestId: req_7c2b18d5 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 PreconditionFailed: description: 'Precondition for the action is not met (e.g. the booking is past its amendment cut-off). ' content: application/json: schema: $ref: '#/components/schemas/Error' example: error: PRECONDITION_FAILED errorMessage: Booking is no longer amendable (cut-off passed). requestId: req_7c2b18d6 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"