Complete checkout through Booking v1

Last verified
Last verified Sep 24, 2026

Use /api/graphql/booking/v1 to complete an existing KORONA Event cart and, when payment is required, send the customer to hosted payment. This guide starts after cart creation and customer details have been collected. Use the Booking v1 API reference for the full field and enum definitions.

Before you start

Send the shop's domain in X-Tenant-Domain on every request. Keep the cart's valid request access token; a plain record ID does not grant anonymous access. The cart must contain valid items and the required contact and checkout information. The account must have a suitable payment method configured.

This payment flow supports checkout policies targeting BOOKED. A policy targeting RESERVED or REQUESTED requires another submission flow; do not use payment initiation to bypass it.

Prepare the cart display and required details

Query request with the cart access token and select requiresShipping to decide whether to collect shipping details. Use requestItems { offerableNameTranslated offerableTimeZone } for localized item names and the time zone used when displaying each item's dates and times.

Select customFieldsCompletion(requiredByStage: BEFORE_PAYMENT) with hasCustomFields, allComplete, allRequiredComplete, and the incomplete/total counts for RequestItem, Attendance, and Contact (for example, countIncompleteAttendance and countTotalAttendance). Use these counts to show remaining checkout questions. If allRequiredComplete is false, collect the required answers before submitting checkout. Omitting requiredByStage or passing null uses BEFORE_PAYMENT; optional unanswered fields can leave allComplete false even when required answers are complete.

This summary does not expose answers, collection tokens, or the private firstIncomplete navigation structure. Use your supported question-collection flow and refresh the summary afterwards. Refresh checkoutPolicy whenever the cart changes: its compatible, targetState, and conflicts are authoritative for the combined cart, even when individual offers advertise a checkout target state.

Apply or remove a code before checkout

Send one mutation per HTTP request with the cart access token as input.id and the customer's voucher number, coupon, or promotion code as input.code:

graphql
mutation ApplyCheckoutCode($input: RequestApplyCodeInput!) {
  requestApplyCode(input: $input) {
    request {
      vouchers {
        number
        name
        value
      }
      totalVouchers
      discounts {
        label
        value
        coupon {
          number
        }
      }
      totalDiscount
    }
    requestItem {
      id
    }
    followUpProducts {
      id
      name
    }
    followUpPriceValue
    errors {
      key
      message
      messageTranslated
    }
  }
}

Check top-level GraphQL errors and payload errors before treating the code as applied. On success, refresh your displayed vouchers, discounts, and totals from request. Voucher amounts appear in vouchers and totalVouchers; coupon and promotion discounts appear in discounts and totalDiscount. Applying a voucher to a cart does not reserve its balance.

A goods voucher can require a product choice. When the payload reports requestItem: blank and returns followUpProducts, show those products and submit requestApplyCode again with the same cart token and code plus input.requestItem. Build that RequestItemInput from the chosen product, with offerableId, offerableType, and pricings containing each selected priceOriginId, priceOriginType, and quantity. Include any required personalization. The first response is a request for this choice, not a successful application, and its request can be null.

When supplied, followUpPriceValue describes the price transferred from the voucher. The server applies that transfer when it creates the item; do not send a client price override to reproduce it. Booking v1 does not expose targetPricingId or eligibleVoucherTargets for selecting an existing cart price row. The product follow-up creates a new cart item.

To remove a code, pass the same cart token and the applied vouchers.number or discounts.coupon.number as input.code:

graphql
mutation RemoveCheckoutCode($input: RequestRemoveCodeInput!) {
  requestRemoveCode(input: $input) {
    request {
      vouchers {
        number
        name
        value
      }
      totalVouchers
      discounts {
        label
        value
        coupon {
          number
        }
      }
      totalDiscount
    }
    errors {
      key
      message
      messageTranslated
    }
  }
}

Handle errors before updating the cart display, then use the returned totals when continuing checkout.

graphql
query CheckoutCart($id: IdOrNumberOrToken!) {
  request(id: $id) {
    checkoutHold {
      expiresAt
      secondsRemaining
    }
    checkoutPolicy {
      compatible
      targetState
      conflicts {
        reasonCode
        requestItemId
        targetState
      }
    }
    requestItems {
      id
      requiresWithdrawalEarlyStartConsent
    }
  }
}

Send this cart query in its own HTTP request, with the cart token as id. Anonymous access through a token supports only one top-level field per operation. Fetch the legal documents in a separate HTTP request, using the same X-Tenant-Domain header:

graphql
query CheckoutLegalDocuments($after: String) {
  legalDocuments(first: 50, after: $after, required: [true]) {
    edges {
      node {
        id
        kind
        nameTranslated
        contentTranslated
        checkboxLabelTranslated
        isUrl
        required
        publishedAt
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

For the first document request, omit after or set it to null. When hasNextPage is true, send another document request with after set to endCursor so every required document is shown. Do not combine the cart and document queries into one operation. Only published, non-discarded documents are exposed. Use isUrl to distinguish a document URL from inline content.

Present each required document and its acceptance label to the customer. Keep its id and exact publishedAt alongside the acceptance. Do not substitute the current time for the version the customer read. If a version changes before submission, reload it and collect acceptance of the new version.

If checkoutPolicy.compatible is false, use the returned conflicts to let the customer change the cart before continuing. Show the checkout-hold deadline when supplied. A countdown is advisory; the server checks whether the hold is still active when checkout is submitted.

For items with requiresWithdrawalEarlyStartConsent: true, collect explicit early-start consent. Submit those item IDs and the exact text shown to the customer; do not infer consent from accepting other legal documents.

Submit checkout

graphql
mutation CompleteCheckout($input: RequestPaymentInitiateInput!) {
  requestPaymentInitiate(input: $input) {
    request {
      id
      state
      checkoutHold {
        expiresAt
        secondsRemaining
      }
    }
    invoice {
      accessToken
      number
      paymentLink
      paymentState
      paymentIntentCreateError
      payments {
        state
        providerInitializationFailed
      }
    }
    errors {
      key
      message
      messageTranslated
    }
  }
}

Use variables with the accepted document versions:

json
{
  "input": {
    "id": "<cart access token>",
    "legalDocumentAcceptances": [
      {
        "legalDocumentId": "<document ID>",
        "legalDocumentVersion": "<publishedAt shown to the customer>"
      }
    ]
  }
}

When required, add withdrawalEarlyStartConsent inside input, with accepted: true, requestItemIds containing the affected cart item IDs, and text containing the consent wording shown. Only send consent after the customer gives it.

Handle both top-level GraphQL errors and mutation-payload errors. A payload error is not a completed checkout; use its field key and translated message to request corrected input or refreshed legal acceptance.

Open payment only when it is required

A successful checkout can return invoice: null, for example for a free cart. In that case, inspect the returned request.state; do not dereference invoice fields, start payment, or poll an invoice without a token.

When an invoice is present, use its paymentState. An invoice can be present for an already settled checkout. Its presence, its number, a BOOKED order state, or the existence of paymentLink does not prove that money remains due or that payment succeeded.

For an invoice requiring payment, open the returned paymentLink unchanged. It leads to the hosted payment page and contains an invoice access token. Treat the link and token as credentials and avoid analytics or log capture. This is the existing token-bearing payment link; it does not provide a single-use launch-code exchange or a configurable return URL through this mutation.

Inspect paymentIntentCreateError and payments.providerInitializationFailed when payment initialization fails. Do not show a payment confirmation for failed, pending, or processing payments.

Read the payment result and handle expiry

graphql
query CheckoutResult($id: IdOrNumberOrToken!) {
  invoice(id: $id) {
    paymentState
    paymentIntentCreateError
    payments {
      state
      providerInitializationFailed
    }
    request {
      state
      checkoutHold {
        expiresAt
        secondsRemaining
      }
    }
  }
}

Pass the invoice's accessToken as id, with the same shop header. Use the refreshed invoice paymentState as the authoritative payment result. A browser redirect or return from the payment provider is not payment confirmation.

If the hold expires, refresh the authoritative state before offering another payment attempt. To request cancellation of an expired cart, call requestCancelExpiredCheckoutHold with RequestCancelExpiredCheckoutHoldInput containing requestId: "<cart access token>". Select request { state } and errors { key message } in its payload. The server checks eligibility; an active hold returns checkoutHold: not_expired. Do not assume a local countdown has canceled the order or released its capacity.

Ready to Get Started?

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