Adrasis™

Package booking

Sell two or more services as one atomic, single-price booking: compose, prebook, book, then cancel in full or by item, with exact repricing and operation-scoped idempotency.

A package finalizes two or more services drawn from accommodation, transfer, tour, and charter flight as one booking with one inclusive price. Either every service confirms, or nothing does.

Flight components use the charter flight API. Connected-supplier airline fares booked in Console are not accepted by the Partner package API.

product searches → package/search → package/prebook → package/book
                                → bookings/{id} → cancellation-quote → cancel

All package endpoints require the distribution:booking scope. Lifecycle (read, quote, cancel) stays on the canonical /api/v1/bookings/{public_id} resource; there is no parallel package lifecycle URL family.

Step 1: Compose (POST /api/v1/package/search)

Package search is a composer over offers you already selected, not a discovery endpoint. Run the product searches first, pick the offers, then submit each product's canonical selection handle as a component:

  • travellers[]: 1–30 entries, each with a unique ref (^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$), a type (ADULT/CHILD/INFANT) and a date of birth where the type requires it. Ages are resolved separately at each component's service date.
  • components[]: 2–5 entries spanning at least two product types. Each carries its component_ref, product_type, a non-empty traveller_refs subset, and exactly one product-specific authority branch. Party is derived from the selected traveller references.
  • An accommodation component (product_type: 1) carries the opaque selection_token copied verbatim from single-property offer detail. That proof is the only caller-supplied authority: do not send allocation_id or a reconstructed selection alongside it. The composer verifies its sealed exact slots and context directly.
  • Transfer, tour, and flight components retain their search allocation's allocation_id and exact product-specific replay selection. The selection is the search you ran, not the offer you picked. A transfer searched with service_class: 0 and no vehicle filter keeps both of those in its selection even if the chosen offer reports a shared service class, and a tour searched across every modality keeps modality_public_id absent. Both fields are bound into the component's allocation_id, so narrowing them here describes a different search, and the composer refuses the component with 410 SOLD_OUT rather than substituting another offer.
  • One currency_code and one language for the whole package; jurisdiction_code selects the organiser's compliance profile. Each non-accommodation selection repeats the same currency and language because both are part of its exact search replay boundary. language is a BCP 47 tag such as tr-TR; a malformed tag answers 400, and the server compares canonical casing.

For example, an accommodation and transfer composition uses the sealed accommodation proof while retaining the transfer's replay fields:

{
  "travellers": [
    { "ref": "adult-1", "kind": "ADULT" },
    { "ref": "adult-2", "kind": "ADULT" }
  ],
  "components": [
    {
      "component_ref": "acc-1",
      "product_type": 1,
      "selection_token": "sel1.active.b3BhcXVl",
      "traveller_refs": ["adult-1", "adult-2"]
    },
    {
      "component_ref": "transfer-1",
      "product_type": 2,
      "allocation_id": "alc_9c1248a5e3d760bf21483a0c6e527d19",
      "selection": {
        "from_zone_public_id": "zn_Airport01",
        "to_zone_public_id": "zn_Hotel77",
        "direction": 1,
        "service_date": "2026-06-10",
        "service_class": 0,
        "currency_code": "EUR",
        "language": "en"
      },
      "traveller_refs": ["adult-1", "adult-2"]
    }
  ],
  "currency_code": "EUR",
  "language": "en",
  "jurisdiction_code": "TR"
}

The response returns ordered component offers, one inclusive package_total_amount, the structured compliance disclosure block, and a package_allocation_id (palc_<32hex>) whose stored composition is valid for 15 minutes, reported as expires_at. Nothing is ever silently dropped: a component that cannot be composed comes back as an explicit error.

The package total includes exclusive tax collected with an accommodation component. A tax payable at the property remains separate. Use the returned package total without adding the component's tax breakdown again.

The package allocation is immutable. Adding, removing or swapping a component means a new package/search and a new package allocation.

Step 2: Revalidate (POST /api/v1/package/prebook)

Send only the package_allocation_id. Every component is requoted from its stored exact descriptor or product replay boundary:

  • Unchanged → match_status: MATCHED.
  • Any price difference at canonical currency precision, with no tolerance band, produces match_status: PRICE_CHANGED with refreshed per-item and total amounts and a newly signed token.
  • A mandatory or selected optional component that is no longer available → SOLD_OUT. Components are never dropped for you.

The signed prebook_token expires in 30 minutes and pins the quote, the item selection and the compliance disclosure version.

Step 3: Finalize (POST /api/v1/package/book)

Book requires your partner account to settle on account with the seller. Without those terms it returns 422 PAYMENT_INTENT_NOT_SUPPORTED before writing the booking. An optional payment_intent_kind may confirm the resolved INVOICE intent; it cannot select different terms. Omit it to use your account's terms. Search results and a prebook token do not replace this book-time check. See Booking payment terms.

Each package component keeps its own payment obligations toward its supplier. A prepaid stay inside a package is not converted to pay at the property.

Requires the Idempotency-Key header (1–128 characters). Send the prebook_token, the booking contact, named traveller details keyed by the prebook travellers[].ref set (the keys must match exactly) and, when the resolved compliance profile requires it, the disclosure delivery evidence.

There is no price field: book never reprices a signed snapshot. The booking confirms every service together, or fails without a partial booking.

The response is the canonical booking envelope with product_type: 5, items[] (each with its own public_id), the booked inclusive total and the aggregate cancellation summary.

Idempotency

Package book and package cancel are operation-scoped: the same key can serve one book and one cancel without collision. Within one operation, the Stripe-style contract from Idempotency applies: same key + same payload replays the stored result; same key + different payload returns 409 IDEMPOTENCY_KEY_CONFLICT.

Reading and cancelling: canonical endpoints, item-aware

GET /api/v1/bookings/{public_id} returns the package with items[], active_item_count, active_selling_rate_total_amount and realized_penalty_total_amount.

POST /api/v1/bookings/{public_id}/cancellation-quote and POST /api/v1/bookings/{public_id}/cancel accept an optional item_public_ids[]:

  • Omitted → the quote/cancel targets all live items.
  • Present → 1–5 item public ids of that booking. Already-cancelled targets return an idempotent per-item result; non-cancellable targets are rejected per item while valid targets proceed.
  • The aggregate penalty is the sum of the targeted items' penalties, each evaluated from its own frozen policy snapshot. Components are never netted against each other.
  • Cancelling an accommodation item refunds its collected exclusive tax on top of any accommodation refund. The penalty excludes that exclusive tax, including for a non-refundable stay.
  • Surviving items keep their booked prices. Partial cancellation never reprices what remains.

Package cancel requires the Idempotency-Key header; the per-item result set is stored and replayed on retry.

Stable errors

HTTP Code
404 RATE_NOT_AVAILABLE, PACKAGE_COMPLIANCE_PROFILE_NOT_FOUND, BOOKING_NOT_FOUND, BOOKING_ITEM_NOT_FOUND
409 PREBOOK_EXPIRED, IDEMPOTENCY_KEY_CONFLICT, BOOKING_NOT_CANCELLABLE
410 SOLD_OUT
422 PACKAGE_COMPOSITION_INVALID, PACKAGE_PROVIDER_MISMATCH, PACKAGE_CURRENCY_MISMATCH, PACKAGE_COMPONENT_UNSUPPORTED, PACKAGE_TRAVELLER_INVALID, SELLING_RATE_BELOW_SUPPLIER_NET, SELLING_RATE_OVERRIDE_NOT_ENROLLED, SOURCE_MARKET_BLOCKED, GUEST_AGE_NOT_ACCEPTED, COMPLIANCE_DELIVERY_REQUIRED

SOLD_OUT covers situations that read alike from outside. A counted allotment can have run out; an accommodation proof's exact sealed slots can become unavailable; or a transfer, tour, or flight replay can stop returning its allocation_id. The message names which one because they need different answers: the first asks the supplier for allotment, while the others mean the selected component must be picked again.

GUEST_AGE_NOT_ACCEPTED means the accommodation component no longer accepts a child of its party. See Guest age refusals.

All errors use the standard envelope from Errors.

Composition rules

  • 2–5 components, at least two distinct product types; a package never contains another package.
  • One provider's supply and one currency per package. Cross-provider composition and package-level FX (foreign exchange) are not supported.
  • Every component carries at least one traveller; every traveller rides at least one component.
  • Optional components can be removed during composition; once inside the signed token, they are part of the atomic book.

Not the same as a trip

Trip bundling groups bookings you already confirmed into one itinerary. It is money-free: every member keeps its own total, its own cancellation policy and its own lifecycle. A trip carries a link_type whose value 1 is named Package because it records the regulatory relationship, but grouping never creates a package contract. Only /package/book does.