Skip to main content
A quote is a price you obtain for a specific adventure + rate pack + date + room composition. It is frozen for 30 minutes, which means:
  • The net_amount returned by POST /quotes is what you owe SafarAPI when you book.
  • Even if the underlying rate pack price changes during those 30 minutes, your quote stays at the quoted price.
  • After 30 minutes, the quote_id is rejected by POST /bookings with 409 partner.booking.quote.expired.
This pattern guarantees a stable price during your customer’s checkout flow.

Lifecycle

Quotes are single‑use: once POST /bookings succeeds with a quote_id, the same quote cannot be used twice.

Pricing logic

The net_amount exposed in your quote is the net amount due to SafarAPI — that is, the public Safariat price minus any partner‑specific margin negotiated in your contract. You are free to:
  • Show your customer the gross price (your markup on top, your decision)
  • Show your customer the net price (zero markup)
  • Show your customer a discounted price (you absorbing some of the cost)
SafarAPI does not enforce any specific customer‑facing price — only the net_amount you remit at booking time.

Breakdown

The breakdown array in the quote response shows you how the total is computed:
travelers is always present. room_supplement appears only when the room composition you sent actually carries a supplement.

How travellers are priced

Each traveller is classified by age against the adventure’s age_brackets, then charged the rate that matches their class. A child rate replaces the adult rate, it is never added to it: So a pack at 2 400 MAD with a child_price of 1 440 MAD, quoted for two adults and one child, totals 2 × 2400 + 1 × 1440 = 6 240 MAD on the travelers line.

What the pack tells you before you quote

Four fields on the rate pack say in advance what a composition will do, so you never have to discover a refusal at quote time: Both flags are always present, both caps count across the whole booking rather than per room, and 0 is a meaningful cap that forbids the category outright. Read the two flags before offering a child or infant selector, and the two caps before offering a quantity.

How many travellers fit

Two limits apply, and they count differently. room_types[].max_occupancy caps one room, extra bed included. A room holding more than that is refused with 400 adventure.room.occupancy.exceeded. Infants do not count against it, since they take no bed: two adults and an infant fit in a room whose max_occupancy is 2. capacity caps the departure. Every traveller counts against it, infants included, and the same count is applied again when the booking is created, so a composition that quotes will not be turned away for size at booking time.

How rooms are priced

The room supplement is a separate line, driven by rate_packs[].room_allocation:
  • PER_ROOM — one room_types[].supplement per room booked. Three single rooms cost three single supplements, one triple room costs one triple supplement. child_supplement does not apply in this mode.
  • PER_PERSON — every occupant pays the supplement of the room type they occupy, adults at supplement and children at child_supplement. Used for Umrah, Hajj and charters.
Only a traveller classified as a child pays child_supplement. An infant pays no room supplement at all, and a traveller older than age_brackets.child_max_age pays the adult supplement, the same way they pay the adult rate on the travelers line.
child_price and room_types[].child_supplement are different things. child_price replaces the traveller’s base price. child_supplement adds a room charge for a child, and only under PER_PERSON. A room type whose supplement is 0.00 genuinely costs nothing extra: the operator declared no difference between that room type and the base price.

Unknown fields are rejected

A request body carrying a field the endpoint does not declare is refused with 400 and the code partner.request.field.unknown, naming the field. Nothing is ever ignored silently. This matters most on POST /quotes. The room composition is children, an array of { "age": n } objects. Sending children_ages, or any other near-miss, used to return a perfectly valid quote for the adults alone, with the children dropped and no error anywhere. A wrong field name now fails loudly instead of producing a quote for the wrong party.

Anti‑fraud check

When you book with POST /bookings, SafarAPI validates that payment_confirmation.paid_amount_net strictly equals the quote’s net_amount. If they differ — by even 1 centime — the booking is rejected with 422 payment.amount.mismatch. This is the cornerstone of the trust between SafarAPI and your bank: we trust your bank’s payment confirmation because the amount cannot be tampered with after the quote.