Create checkout

Supports guest users.

Creates a checkout (reservation) for one or more products at a specific merchant.
The checkout expires if not confirmed within its TTL. Supports idempotency via
the Idempotency-Key header.

Events fired

EventTriggerDescription
checkouts.CheckoutStartedsyncFired when the checkout is successfully created.
Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…
📘

Eventscheckouts.CheckoutStarted

Why this endpoint exists

Two things happen here that only the platform can do.

It reserves inventory. The ticket provider is told to hold these products for this date, so the customer cannot lose their slot while filling in a form.

It prices the basket authoritatively. Per-day pricing, per-ticket booking costs, the flexible-date surcharge, and every discount the customer is entitled to — loyalty wallet, club membership, vouchers — are resolved server-side and returned as a complete priced basket. This response is what your review page renders. It is the only place the real total exists before payment.

Send an end-user token and those customer-specific discounts apply. Omit it and the same basket prices at full price for a guest.

Request

curl -X POST https://api.acc.funtrips.io/v2/checkouts \
  -H "x-campaign-id: $CAMPAIGN_ID" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Authorization: Bearer $END_USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "date": "2026-09-12T10:30:00Z",
    "flexible_date": false,
    "external_reference": "cart-8817",
    "lines": [
      { "product_id": "d4b8f2a1-6c3e-4957-b18d-2f5a9c7e3b04", "quantity": 2 },
      { "product_id": "b2c9e4f7-1a83-4d56-9e0b-7f3a1c8d5b62", "quantity": 1 }
    ],
    "voucher_codes": ["WELCOME2026"]
  }'
FieldTypeRequiredNotes
datedate-timeYesThe visit date. Include the time component when any product needs a timeslot
flexible_datebooleanYestrue buys the flexible-date option, letting the customer change the date later. Adds a per-ticket surcharge
linesarrayYesAt least one line
lines[].product_idUUIDYesFrom the catalog
lines[].quantityintegerYesMinimum 1
external_referencestringNoYour own reference — a cart id. Echoed through the order for reconciliation
voucher_codesstring[]NoUp to 50 unlock-voucher codes. Invalid codes are silently ignored

One basket, one merchant

Every line in a basket must belong to the same merchant.

A basket spanning two merchants is refused with 400 and
urn:qup:error:checkout-basket-spans-merchants. There is a second, narrower rule beneath it:
lines must also share one fulfillment provider (checkout-basket-spans-providers).

The merchant rule is the one to design around, because it is stricter than it looks — two
different venues can share a provider, so a basket can pass the provider check and still be
refused. The reason is settlement: the merchant determines who gets paid, and a mixed basket
would credit one merchant for another's tickets.

If your cart lets customers collect experiences from several venues, split it at submission:
one checkout per merchant, each with its own Idempotency-Key. The customer then completes
several payments. Design the cart UI for that up front — discovering it at the payment step is a
much worse conversation than grouping the cart by venue from the start.

The date field carries the time

date is a full timestamp, not a calendar day, and the time component is not decoration — ticket providers use it to resolve a capacity slot.

  • Product has requires_timeslot: true → send the exact entry_from of the slot the customer picked, from POST /fulfillment/timeslots.
  • Product has no_date: true → send today.
  • Otherwise → the chosen day; the time is not significant.

Never send midnight as a fallback for a timeslotted product.

2026-09-12T00:00:00Z is 02:00 Amsterdam time in summer. Timeslotted providers reject it, and when they do not, the booking lands on the wrong day. If the timeslot list for a date comes back empty for a requires_timeslot product, that date is unavailable — prompt for another one.

voucher_codes fail silently by design

Each valid code unlocks one product unit's campaign-defined discount. Invalid, expired and already-used codes are ignored without an error — the checkout still succeeds, just without that discount.

So you cannot tell from the status code whether a code applied. Compare the discounts array on the returned lines against the codes you sent, and if a customer's code did not land, tell them there rather than after payment. To validate a code before checkout, use POST /vouchers/validate.

Response

200 OK with the priced checkout.

{
  "data": {
    "checkout_id": "7c1b3f9a-2d4e-4a6f-9b1a-3c5e7d2f8a0b",
    "status": "OPEN",
    "date": "2026-09-12T10:30:00Z",
    "locale": "nl-NL",
    "expires_at": "2026-09-03T15:05:00Z",
    "fulfillment_provider": "<provider>",
    "requires_shipping_address": false,
    "flexible_date": false,
    "totals": {
      "subtotal":              { "value": "61.00", "currency": "EUR" },
      "booking_costs":         { "value": "2.97",  "currency": "EUR" },
      "flexible_ticket_costs": { "value": "0.00",  "currency": "EUR" },
      "discount_total":        { "value": "11.00", "currency": "EUR" },
      "grand_total":           { "value": "52.97", "currency": "EUR" }
    },
    "line_items": [
      {
        "merchant_id": "8a2f4c61-9d3e-4b57-a0c8-1e5f7b2d9a30",
        "merchant_title": "Amsterdam Zoo",
        "product_id": "d4b8f2a1-6c3e-4957-b18d-2f5a9c7e3b04",
        "product_title": "Dagticket volwassene",
        "title": "Dagticket volwassene",
        "quantity": 2,
        "sku": "ZOO-ADULT-DAY",
        "unit_price":      { "value": "24.00", "currency": "EUR" },
        "discounts": [
          { "type": "LOYALTY", "amount": { "value": "5.50", "currency": "EUR" }, "source": "wallet" }
        ],
        "effective_price": { "value": "18.50", "currency": "EUR" },
        "line_total":      { "value": "37.00", "currency": "EUR" },
        "product_age_group": "adult",
        "product_conditions": ["Geldig op de gekozen datum."],
        "merchant_logo_uuid": "e91f…",
        "merchant_banner_uuid": "a37c…",
        "fulfillment_provider_codes": { "<provider>": "PRV-4471" }
      }
    ]
  },
  "meta": { "correlation_id": "…" }
}

Fields to act on

FieldWhy it matters
checkout_idNeeded for Confirm and for polling. For guests this is a secret — it is the only access control on the checkout
expires_atThe reservation deadline. Drive a countdown from it
requires_shipping_addresstrue → Confirm must carry a complete address. Authoritative; use this, not the product flag
totals.grand_totalWhat the customer will be charged. Render this
line_items[].discountsTyped per-unit breakdown — LOYALTY, CLUB_MEMBERSHIP or VOUCHER, with the source. Empty when nothing applied
line_items[].effective_pricePer-unit price after discounts: unit_price − Σ discounts.amount
flexible_dateWhether the basket carries the flexible-date option

payment_url is absent here. It only exists after Confirm.

Reading the totals

grand_total = subtotal + booking_costs + flexible_ticket_costs − discount_total

subtotal is the sum of line totals before surcharges. booking_costs and flexible_ticket_costs are per-ticket amounts already multiplied by the ticket count. discount_total is everything the customer saved.

Show the breakdown — customers ask why the total differs from the ticket prices, and booking_costs is usually the answer.

Errors

StatustypeWhat to do
400checkout-basket-emptylines was empty
400checkout-basket-line-invalidA quantity is below 1
400checkout-basket-spans-providersBasket mixes products from different fulfillment providers — split it
400checkout-basket-spans-merchantsBasket mixes products from different merchants — split it, one checkout per merchant
400checkout-date-in-pastPick today or later
400checkout-timeslot-requiredA product needs a time of day; fetch timeslots and resend with a real time
400checkout-locale-invalidAccept-Language is unusable
400Idempotency-Key missing or malformed
401A token was sent and rejected as structurally invalid
404campaign-not-foundThe campaign is not configured — report it to us
409Idempotency collision. See Idempotency
422checkout-product-not-priceableProduct unavailable for that date — offer another date
422checkout-product-not-fulfillableProvider cannot fulfil it right now
502checkout-reservation-placement-failedThe ticket provider could not be reached. Retryable with the same key
502checkout-discount-resolution-failedDiscounts could not be resolved. Retryable
500checkout-basket-pricing-failed, checkout-discount-allocation-failed, checkout-merchant-lookup-failed, checkout-debtor-code-unresolvedServer-side fault. Retry, then quote the correlation id
208Idempotent replay of an earlier success. Treat as 200

The 422 pair is the customer-facing case worth handling well: the basket is fine but that date will not work. Send them back to the date picker with the calendar re-fetched, rather than showing an error.

Notes

  • checkouts.CheckoutStarted is published on success. If you consume platform events, that is your signal a basket was reserved.
  • Abandonment needs no call. An OPEN checkout that is never confirmed expires by itself in 30 minutes and releases its reservation.
  • One retry key per basket. If the customer edits the basket and resubmits, that is a new operation — generate a new Idempotency-Key, or you will get a 409 body mismatch.
Body Params
date-time
required

Desired visit date.

boolean
required
Defaults to false

When true the reservation is not tied to a specific date.

string

Caller-supplied reference (e.g. cart ID).

lines
array of objects
required
length ≥ 1
lines*
voucher_codes
array of strings
length ≤ 50

Optional unlock-voucher codes applied at checkout (max 50). Each valid code unlocks one product unit's campaign-defined discount; invalid codes are ignored. The applied discount is reflected in the response totals.

voucher_codes
Headers
uuid
required

Campaign scope for this request. Filters content to the specified campaign.

uuid
required

Client-generated UUID. Identical keys within the TTL window return the cached response.

string
enum
Defaults to application/json

Generated from available response content types

Allowed:
Responses

Language
Credentials
Header
URL
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
application/json
application/problem+json