Complete checkout and take payment with the Booking API

Last verified
Last verified Sep 28, 2026

Use this guide to turn a filled cart into a paid order, a reservation or a booking inquiry. It covers the checks before checkout, legal consent, the hosted payment handoff, payment status and ticket delivery. The Booking API quickstart shows the shortest path; this guide explains each decision along the way.

Before you start

  • A cart with at least one item and its cart token.
  • The customer's name, email address and billing address.

Check the cart before checkout

Read the cart with request(id: <cart token>) and act on these fields. A request authenticated by a cart or invoice token can select only one top-level field, so read the cart, the legal documents and the shop in separate requests.

FieldMeaning and action
checkoutPolicy.targetStateSelects the checkout mutation: BOOKED completes with requestPaymentInitiate, RESERVED with requestReserve and REQUESTED with requestRequest.
checkoutPolicy.compatible and conflictsWhen compatible is false, remove the lines in conflicts or check them out in a separate cart.
requiresShippingWhen true, the customer needs a shipping address in one of shop.allowedShippingCountries.
customFieldsCompletion.allRequiredCompleteWhen false, collect the required answers through the Booking API before checkout. If your integration cannot collect them, use the hosted shop for the booking instead.
requestItems.requiresWithdrawalEarlyStartConsentWhen true for any line, ask for the early-start consent described below.
checkoutHold.secondsRemainingTime left before an unpaid order is canceled. Keep it visible until the payment succeeds. The countdown is a guide; KORONA Event checks the hold again when checkout is submitted.

To collect and save booking and attendee answers before checkout, follow Collect booking and attendee details with the Booking API.

customFieldsCompletion counts the questions due before payment. Only allRequiredComplete decides whether checkout can go ahead: allComplete is also false while optional questions are unanswered.

requestItems includes lines the customer removed from the cart. Read requestItems(discarded: [false]) to get only the current lines.

The hosted shop's address for an offer is its domain, the two-letter language code and the offer's shopSlug.slugTranslated, for example https://tickets.example.com/en/guided-tour.

Payment-method eligibility

A cart token gives access to that cart; it does not authorize every payment method. KORONA Event checks eligibility when the cart is created or changed and again before checkout completes. A previously saved method can be rejected if its eligibility has since been withdrawn.

Invoice payment must be enabled by the applicable offer or shop checkout policy. For online checkout, on-site payment must be enabled by that policy or the venue's public on-site payment setting. The legacy kiosk path below instead requires the venue to allow on-site payment at stationary points of sale. Delivery notes require an applicable policy and the venue's delivery-note capability. Collective invoice requires a signed-in contact of the billing customer, enabled reseller access and a valid reseller agreement. A customer ID in the payload is not a substitute for signing in. Staff-approved customer workflows can carry approval for their unchanged order; an ordinary cart token cannot create that approval.

Do not infer eligibility from a saved paymentMethod, a client-selected sales channel or an app header. If checkout returns paymentMethod: unauthorized, use an eligible checkout method or ask the venue to review the applicable configuration. Do not retry by changing the client identity or token. Treat the rejected attempt as incomplete: it does not authorize ticket delivery or a billing commitment.

POS selection and legacy kiosk compatibility

The pos list and numeric or automatic pointOfSale lookups require an authenticated staff user or API credential with permission to read points of sale. As a temporary compatibility path, pointOfSale accepts an exact POS UUID without a staff credential. POS associations on price rules and quotas require POS-read permission. Public shop checkout remains bound to the shop's online point of sale. Legacy kiosk cart creation must supply the exact UUID of a physical point of sale in the same tenant; POS numbers and automatic physical-POS selection are not supported. A known POS UUID is not proof of kiosk identity.

Add the customer

Send the customer with requestUpdate, as shown in the quickstart. Mark the billing address with billing: true and, when the cart requires shipping, a shipping address with shipping: true. The same address can have both.

Load the documents customers must accept with legalDocuments(required: [true]). The list is paginated: while pageInfo.hasNextPage is true, request the next page with after set to pageInfo.endCursor, so you show every required document. For each document, show a checkbox with checkboxLabelTranslated, or nameTranslated when no label is set. Show the document text from contentTranslated; when isUrl is true, the content is a link to the document.

Send one acceptance per required document with checkout. legalDocumentVersion must be the publishedAt value the customer saw; never substitute the current time. When a document changes between display and checkout, checkout fails with an error for legalDocumentAcceptances; reload the documents and ask the customer again.

Some services can only be sold when the customer agrees that the service starts before the legal withdrawal period ends. When a cart line has requiresWithdrawalEarlyStartConsent, show a consent checkbox with your consent text and send the result with checkout:

json
{
  "withdrawalEarlyStartConsent": {
    "accepted": true,
    "requestItemIds": ["<cart line id>"],
    "text": "I agree that the service starts before the withdrawal period ends."
  }
}

text must be the exact text the customer saw. Send the consent only after the customer gives it; accepting the legal documents does not count as consent.

Review EU goods information

When EU goods information is enabled for the shop and the cart contains goods, public checkout requires the fingerprint of the information shown to the customer. This also applies in warning-only mode and to goods without an additional manufacturer guarantee.

Fetch the review using the cart token and the same Accept-Language header you will use for checkout:

graphql
query GoodsReview($id: IdOrNumberOrToken!) {
  request(id: $id) {
    euGoodsReview {
      enabled
      hasGoods
      ready
      fingerprint
      information {
        productId
        name
        state
        brand
        model
        duration
        labelUrl
        statement
        statementUrl
      }
    }
  }
}

When enabled and hasGoods are both true:

  1. Show the official legal guarantee notice and the returned product information before the final order action. For each ready product, display its GARAN label from labelUrl and make its statement and optional statementUrl available. Render statements as plain text.
  2. If the review's ready is false, do not complete checkout: the merchant must complete the declared guarantee information first. In warning-only mode, ready can be true with an empty information list because incomplete guarantees are omitted; the notice and fingerprint are still required.
  3. Send the displayed review's fingerprint as input.euGoodsReview with requestPaymentInitiate, alongside the cart token and other required inputs. Do not generate the fingerprint yourself.
  4. Refetch and redisplay the review after changes to the cart, language or guarantee information. If checkout returns an euGoodsReview error, fetch the review again and let the customer review it before retrying. Do not silently retry with a newly fetched fingerprint.

If enabled or hasGoods is false, no fingerprint is required. The same review input applies to requestReserve and requestRequest when those operations match the cart's checkout policy.

See EU guarantee information for goods for shop applicability, the official notice and guarantee configuration.

Complete checkout

Call requestPaymentInitiate with the cart token, the legal acceptances and, when needed, the early-start consent and EU goods review fingerprint. The quickstart shows the full mutation.

ResultWhat to do
invoice.paymentState is REQUIRES_PAYMENTRedirect the customer to paymentLink.
invoice.paymentState is PAIDNothing is left to pay, for example for a free order or when a voucher covers the full amount. The order is complete; do not redirect, even though paymentLink is set.
invoice is nullThe offer is paid at the venue. The order is booked and nothing is paid online.
errorsShow messageTranslated. The cart is unchanged; fix the input and try again.

Decide from invoice.paymentState. Neither an invoice, its number, a BOOKED state nor a paymentLink means that money is still due.

Reserve or submit a booking inquiry

Some offers are not paid at checkout. The venue sets this per offer, and checkoutPolicy.targetState tells you which mutation completes the cart:

  • RESERVED: requestReserve holds the places without payment. The customer receives a reservation email, and the venue follows up to arrange payment or booking.
  • REQUESTED: requestRequest sends the order to the venue as a booking inquiry. Nothing is booked or paid until staff accept it.

Both take the same legal acceptances and early-start consent as requestPaymentInitiate:

graphql
mutation ReserveCart($input: RequestReserveInput!) {
  requestReserve(input: $input) {
    request {
      number
      state
      reservationExpiresAt
    }
    errors {
      key
      message
      messageTranslated
    }
  }
}
graphql
mutation SubmitInquiry($input: RequestRequestInput!) {
  requestRequest(input: $input) {
    request {
      number
      state
    }
    errors {
      key
      message
      messageTranslated
    }
  }
}

On success, state is RESERVED or REQUESTED. Show the customer the order number and explain that the venue will contact them. reservationExpiresAt is when an unpaid reservation lapses; it is empty when the venue has set no reservation deadline. When the cart's target state is different, the mutation fails with an error for checkoutPolicy.

Hand payment to the hosted page

paymentLink opens the configured hosted payment page for this invoice, in the language of your Accept-Language header. When the shop payment page is enabled, it uses the shop identified by X-Tenant-Domain. The returned URL may use that shop’s canonical domain rather than the exact domain alias you supplied. Use the returned URL unchanged. The customer chooses a payment method and pays there. The page handles payment retries. It does not collect required booking or attendee answers. If customFieldsCompletion.allRequiredComplete is false, collect them before checkout as described in Collect booking and attendee details with the Booking API. If your integration cannot collect them, use the venue’s hosted online shop for the booking instead.

Keep the invoice token from invoice.accessToken. You need it to follow the payment.

paymentLink contains the invoice token. Treat the link like a password: open it unchanged and keep it out of analytics, logs and shared URLs.

Follow the payment

Read the invoice with the invoice token, for example with the PaymentStatus query from the quickstart. Poll every few seconds while the customer pays and stop when the payment is final.

FieldMeaning
paymentState: PAIDThe payment succeeded.
paymentState: REQUIRES_PAYMENTNo successful payment yet. payments lists the attempts and their state.
paymentIntentCreateErrorWhy a new payment cannot start: already paid, already in progress, or the order was canceled.
payments.providerInitializationFailedThe payment provider could not start this attempt. The customer can open paymentLink again.
request.state: CANCELEDThe order was canceled, for example because the checkout hold ended before payment. Start a new cart.

Rely on the invoice, not on the customer returning to a page: a return from the payment provider is not proof of payment, and payments can succeed after the customer closes the browser. Do not show a confirmation while the payment is pending or failed.

Deliver tickets

After payment, request.shareablePublicTicketsUrl(format: PDF) returns a customer-facing link to the tickets page. Ticket activation can still be pending when this URL is present; the page makes the tickets available when activation finishes. Use format: PKPASSES for Apple Wallet passes when they are available.

Handle an expired checkout hold

When checkoutHold.secondsRemaining reaches zero before payment, KORONA Event cancels the order and releases its places. Call requestCancelExpiredCheckoutHold with the cart token as requestId, or the invoice token as invoiceToken, to release them immediately, then offer the customer a new cart. KORONA Event decides whether the hold has ended: while it is still running, the call fails with checkoutHold not_expired.

Troubleshooting

ProblemWhat to check
Checkout fails with legalDocumentAcceptancesEvery required document is accepted with its current publishedAt as legalDocumentVersion.
A cart change fails with checkoutHoldThe hold ended and the order was canceled. Start a new cart.
A response is missing after checkoutRead the cart with its token before trying again. When state is BOOKED, checkout already succeeded; read the invoice token from invoices { accessToken }.
A token request fails with single top level fieldThe request selects more than one top-level field. Send each field in its own request.
Checkout fails with checkoutPolicyThe mutation does not match checkoutPolicy.targetState, or the cart mixes offers with different target states.

Ready to Get Started?

Book a free demo or reach out — we’d love to hear from you.