holaOlas logo
holaOlas logo

AI agents • booking assistants • integrators

HolaOlas Agent & Integrator Booking API

Public booking endpoints for agentic commerce in tourism: discover providers/experiences, check availability, validate live pricing, create bookings, and hand off payment on HolaOlas. JSON payloads, CORS enabled. The agent orchestrates; the human pays.

Core flow: provider → experiences → availability → pricing → booking → payment handoff to human on HolaOlas.

GEThttps://holaolas.app/api/embed/provider?providerSlug=adorablesailing

Provider lookup

Resolve a provider and list its experiences (with ids and slugs).

Example response

{
  "provider": {
    "slug": "adorablesailing"
  },
  "experiences": [
    {
      "id": "exp-uuid",
      "slug": "el-clasico-excursion-privee-en-voilier-sur-la-lagune-de-bacalar-bacalar"
    }
  ]
}
GEThttps://holaolas.app/api/embed/experience?experienceId=exp-uuid

Experience lookup

Get experience detail (name, slugs, pricing mode) to drive downstream calls.

Example response

{
  "id": "exp-uuid",
  "type": "excursion",
  "booking_mode": "shared",
  "slug": "el-clasico-excursion-privee-en-voilier-sur-la-lagune-de-bacalar-bacalar",
  "timeSlots": [
    {
      "id": "morning-slot-1",
      "startTime": "09:00",
      "duration": 210
    }
  ]
}
POSThttps://holaolas.app/api/check-availability

Check availability

Check availability (dates + time slots) for an experience.

Payload (JSON)

{
  "experienceId": "uuid",
  "date": "2026-03-12",
  "timeSlotId": "morning-slot-1",
  "participantCount": 4
}

Example response

{
  "available": true,
  "type": "excursion",
  "reason": null
}
POSThttps://holaolas.app/api/validate-booking-price

Validate booking price

Compute and validate price. Returns pricingToken to lock price for create_booking.

Payload (JSON)

{
  "experienceId": "uuid",
  "date": "2026-03-12",
  "timeSlotId": "morning-slot-1",
  "adults": 2,
  "children": 2,
  "options": {
    "snorkel-kit": 2
  }
}

Example response

{
  "currency": "MXN",
  "subtotal": 2400,
  "discount": 0,
  "fees": 0,
  "deposit": 720,
  "total": 2400,
  "expiresAt": "2026-03-09T23:10:00Z",
  "pricingToken": "eyJwIjoi...",
  "unitBreakdown": [
    {
      "label": "Base",
      "amount": 2400
    }
  ]
}
POSThttps://holaolas.app/api/embed/booking/create

Create booking

Create a booking. With pricingToken: no price recalc, idempotent. Returns paymentUrl for human.

Payload (JSON)

{
  "experience_id": "uuid",
  "pricingToken": "from validate-booking-price",
  "date": "2026-03-12",
  "time_slot_id": "morning-slot-1",
  "participant_data": {
    "adults": 2,
    "children": 0,
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.com",
    "phone": "+52..."
  }
}

Example response

{
  "bookingId": "uuid",
  "status": "pending",
  "currency": "MXN",
  "total": 2400,
  "amountDueNow": 720,
  "paymentUrl": "https://holaolas.app/en/booking/uuid?payment=true",
  "expiresAt": "2026-03-09T23:25:00Z"
}
GEThttps://holaolas.app/api/booking/{bookingId}

Get booking

Fetch booking details. Add ?locale=en for paymentUrl locale.

Example response

{
  "bookingId": "uuid",
  "status": "pending",
  "paymentUrl": "https://holaolas.app/en/booking/uuid?payment=true",
  "amountDueNow": 720,
  "amountPaid": 0,
  "amountRemaining": 2400
}

Pre-fill booking form (deep links)

Append query parameters to the public experience URL (path /…/p/providerSlug/e/experienceSlug). The page loads the experience, then applies one of two parsers: hotel if type === "hotel", otherwise excursion (excursion, activity, default). Same parameter names can mean different things (e.g. dateFrom/dateTo are multi-day on excursion, check-in/out on hotel).

Machine-readable spec — excursion / activity / default

{
  "urlPattern": "https://holaolas.app/{locale}/p/{providerSlug}/e/{experienceSlug}",
  "experienceTypes": [
    "excursion",
    "activity"
  ],
  "experienceTypesNote": "Any loaded experience whose type is not hotel uses BookingFormExcursion and this parser (including activity and default branch).",
  "parser": "parseExcursionPrefillFromSearchParams",
  "sourceFile": "src/utils/booking-prefill-url.ts",
  "internalDoc": "docs/booking-prefill-vs-formulaires.md section 3.A",
  "dateFormat": "YYYY-MM-DD",
  "precedence": [
    "If categoryCounts (or pricingCategories) yields at least one valid row after filtering, it replaces adults/children/childrenAges prefill for that load.",
    "payment is parsed before deposit; if both are set, payment wins when it maps to full or deposit.",
    "options is parsed before selectedOptions; categoryCounts before pricingCategories.",
    "specialRequests: first non-empty among specialRequests, notes, remarks (then trim, max 8000 chars)."
  ],
  "paymentAndDepositSynonyms": {
    "mapsToFull": [
      "full",
      "total",
      "100"
    ],
    "mapsToDeposit": [
      "deposit",
      "depot",
      "partial"
    ],
    "caseInsensitive": true,
    "params": [
      "payment",
      "deposit"
    ]
  },
  "queryParams": {
    "date": "Single-day YYYY-MM-DD (regex). Multi-day uses startDate+endDate when both valid and experience supportsMultiDay.",
    "startDate": "Multi-day start. Alias: dateFrom.",
    "endDate": "Multi-day end. Alias: dateTo.",
    "slot": "Slot id or time 11:00 / 11h00. Alias: timeSlotId (slot read first).",
    "categoryCounts": "URL-encoded JSON: array [{ categoryId or category_id, count }] or object { categoryId: count }. Alias param name: pricingCategories (categoryCounts read first). Counts 0–99; form keeps count>0 and known category ids + auto-adult + free-children.",
    "adults": "Integer 1–99. Omitted if not in URL. Ignored when categoryCounts prefill applies.",
    "children": "Integer 0–99. Omitted if not in URL. Ignored when categoryCounts prefill applies.",
    "childrenAges": "Comma or semicolon separated ages 0–120. Alias: childAges.",
    "options": "URL-encoded JSON object { optionId: quantity }; quantities 0–999. Alias: selectedOptions (options read first).",
    "payment": "See paymentAndDepositSynonyms.",
    "deposit": "Same parser as payment; lower precedence if payment also set.",
    "specialRequests": "Aliases: notes, remarks. Max 8000 chars after trim.",
    "firstName": "Alias: prenom.",
    "lastName": "Alias: nom.",
    "email": "string",
    "phone": "Alias: tel.",
    "phoneCountry": "e.g. FR, MX, CA"
  },
  "notParsedOnExcursionParser": [
    "checkin",
    "checkout"
  ]
}

Machine-readable spec — hotel

{
  "urlPattern": "https://holaolas.app/{locale}/p/{providerSlug}/e/{experienceSlug}",
  "experienceTypes": [
    "hotel"
  ],
  "parser": "parseHotelPrefillFromSearchParams",
  "sourceFile": "src/utils/booking-prefill-url.ts",
  "internalDoc": "docs/booking-prefill-vs-formulaires.md section 3.B",
  "dateFormat": "YYYY-MM-DD",
  "routing": "The public experience page picks this parser only when the loaded experience type is hotel; otherwise the excursion spec applies.",
  "precedence": [
    "Valid unitId matching config.units[].id plus categoryCounts (after filter to that unit’s pricing categories and auto-adult/free-children) overrides adults/children/childrenAges for that load.",
    "Valid unitId plus adults and/or children: merge into initializeCategoryCounts for that unit; unit-change category reset is skipped once (skipInitAfterPrefillRef).",
    "Without valid unitId: adults, children, childrenAges, categoryCounts from URL are not applied; checkin/checkout, contact, options, payment, specialRequests, and extras booleans still apply when present.",
    "payment before deposit; options before selectedOptions; categoryCounts before pricingCategories.",
    "specialRequests: first non-empty among specialRequests, notes, remarks (trim, max 8000 chars)."
  ],
  "paymentAndDepositSynonyms": {
    "mapsToFull": [
      "full",
      "total",
      "100"
    ],
    "mapsToDeposit": [
      "deposit",
      "depot",
      "partial"
    ],
    "caseInsensitive": true,
    "params": [
      "payment",
      "deposit"
    ]
  },
  "booleanQueryValues": {
    "true": [
      "1",
      "true",
      "yes",
      "on"
    ],
    "false": [
      "0",
      "false",
      "no",
      "off"
    ],
    "note": "For extraBed, withPet, earlyCheckin, lateCheckout: true applies only if the matching hotelSettings flag is enabled; false always clears the checkbox."
  },
  "queryParams": {
    "checkin": "YYYY-MM-DD. Aliases: checkIn, dateFrom (on hotel pages dateFrom means check-in, not excursion multi-day).",
    "checkout": "YYYY-MM-DD. Aliases: checkOut, dateTo.",
    "unitId": "Room/unit id from experience config. Aliases: unit, room (read in that order).",
    "adults": "Integer 1–99. Ignored without valid unitId; ignored when categoryCounts prefill applies.",
    "children": "Integer 0–99. Same constraints as adults.",
    "childrenAges": "Comma or semicolon separated 0–120. Alias: childAges.",
    "categoryCounts": "URL-encoded JSON array [{ categoryId or category_id, count }] or object. Alias: pricingCategories. Filtered to selected unit’s categories.",
    "options": "URL-encoded JSON { optionId: quantity }. Alias: selectedOptions (options first).",
    "payment": "See paymentAndDepositSynonyms.",
    "deposit": "Same as payment; lower precedence if payment also set.",
    "specialRequests": "Aliases: notes, remarks. Max 8000 chars after trim.",
    "firstName": "Alias: prenom.",
    "lastName": "Alias: nom.",
    "email": "string",
    "phone": "Alias: tel.",
    "phoneCountry": "e.g. FR, MX, CA",
    "extraBed": "boolean query; requires hotelSettings.extraBed.enabled for true to stick.",
    "withPet": "boolean; requires hotelSettings.petFee.enabled for true.",
    "earlyCheckin": "boolean. Alias: early_checkin. Requires hotelSettings.earlyCheckin.enabled for true.",
    "lateCheckout": "boolean. Alias: late_checkout. Requires hotelSettings.lateCheckout.enabled for true."
  },
  "collisionNote": "dateFrom/dateTo mean checkin/checkout on hotel pages only; on excursion pages the excursion parser uses them as startDate/endDate aliases."
}

Human-readable list — excursion / activity

  • Core: date, adults, children, slot or timeSlotId, firstName (or prenom), lastName (or nom), email, phone (or tel), phoneCountry
  • Multi-day (only if the experience has multi-day enabled): startDate + endDate, or dateFrom + dateTo (both YYYY-MM-DD)
  • childrenAges or childAges — comma/semicolon-separated (e.g. 5,8,12)
  • categoryCounts or pricingCategories — URL-encoded JSON: array [{"categoryId":"…","count":n}] or object {"categoryId":n} (ids from experience config). Overrides adults / children / childrenAges when present.
  • options or selectedOptions — URL-encoded JSON {"optionId":quantity}
  • payment or deposit: full or deposit
  • specialRequests, notes, or remarks
  • Not read by the excursion parser: checkin, checkout (use the hotel spec on hotel experiences).

Human-readable list — hotel

  • Dates: checkin + checkout (aliases checkIn/checkOut, or dateFrom/dateTo for arrival/departure — not the excursion multi-day meaning).
  • Room: unitId (aliases unit, room) must match config.units[].id. Without a valid id, adults, children, childrenAges, and categoryCounts from the URL are ignored; other params still apply.
  • Same as excursion where shared: childrenAges/childAges, categoryCounts/pricingCategories, options/selectedOptions, payment/deposit, contact fields, specialRequests/notes/remarks.
  • Extras (boolean: 1/true/yes/on vs 0/false/no/off): extraBed, withPet, earlyCheckin (or early_checkin), lateCheckout (or late_checkout). true only sticks if enabled in hotel settings.

Example — excursion (minimal)

https://holaolas.app/en/p/adorablesailing/e/el-colectivo-shared-sailing-excursion-on-the-bacalar-lagoon-bacalar?date=2026-03-10&adults=3&children=0&slot=11:00&firstName=Jean&lastName=Dupont&email=jean@example.com&phone=0612345678

Example — excursion (ages + options JSON — encode the JSON for the URL)

In code: options=%7B%22snorkel-kit%22%3A2%7D

https://holaolas.app/fr/p/adorablesailing/e/example-slug?date=2026-04-13&adults=2&children=2&childrenAges=6,9&slot=11:00&firstName=Marie&lastName=Faubert&options=%7B%22snorkel-kit%22%3A1%7D&payment=deposit

Example — hotel (replace unit UUID and slugs)

https://holaolas.app/fr/p/myprovider/e/my-hotel-slug?checkin=2026-06-01&checkout=2026-06-05&unitId=YOUR-UNIT-UUID-FROM-CONFIG&adults=2&children=1&payment=deposit&extraBed=1

Recommended agent flow

  1. Resolve experienceId from URL (via /api/embed/provider if needed).
  2. POST /api/check-availability with date + timeSlotId.
  3. POST /api/validate-booking-price to get the total.
  4. POST /api/embed/booking/create then redirect the human to /{locale}/booking/{id}?payment=true.
  5. Or: generate a pre-fill URL (see above) for the user to open and pay directly.

Advanced parameters (private/shared excursions)

  • bookingMode: private or shared (can also be defined per timeSlot).
  • adults, children[].age: enables age-based pricing.
  • options: keys = option IDs, values = quantities.
  • date and timeSlotId drive seasonal pricing and slot capacity.

The validate-booking-price and booking/create endpoints automatically apply seasonal rules, capacity, private/shared mode, and options/age pricing.

Common error responses

Slot full

{
  "available": false,
  "reason": "slot_full"
}

Price mismatch

{
  "error": "price_mismatch",
  "message": "Submitted total does not match current pricing."
}

Experience not found

{
  "error": "Experience not found"
}

Payment handoff

After booking/create, redirect the user to /{locale}/booking/{id}?payment=trueto complete payment on HolaOlas. The agent orchestrates; the human pays.

End-to-end example

{
  "providerSlug": "adorablesailing",
  "experienceId": "exp-uuid",
  "steps": [
    "GET /api/embed/provider?providerSlug=adorablesailing -> list experiences -> exp-uuid",
    "POST /api/check-availability { experienceId: exp-uuid, date, timeSlotId }",
    "POST /api/validate-booking-price { experienceId, bookingMode, adults, children, options, date, timeSlotId }",
    "POST /api/embed/booking/create { experience_id, date, time_slot_id, booking_mode, adults, children, options, total_price }",
    "Redirect user to /fr/booking/{id}?payment=true"
  ]
}