Booking API quickstart
- Last verified
- Last verified Sep 29, 2026
This tutorial sells two tickets for an event with a fixed date, such as a concert, and hands payment to the shop's hosted payment page. It uses seven requests: find the event, read its price categories, add the tickets to a cart, add the customer, read the required legal documents, complete checkout and check the payment. Other offers shows what changes for general admission, time slots and vouchers.
Before you start
- A shop in KORONA Event with at least one published event that has places left and is configured for booking with online payment.
- The shop's domain, for example
tickets.example.com, and your KORONA Event API host. - A tool that sends HTTP POST requests with a JSON body, such as
curlor your application's HTTP client.
For reservations, booking inquiries or payment at the venue, follow Complete checkout and take payment with the Booking API.
Send every request as POST https://<api-host>/api/graphql/booking/v1 with these headers:
Content-Type: application/json
X-Tenant-Domain: tickets.example.com
Accept-Language: enThe body is a JSON object with query and variables. How the Booking API works explains the records and tokens used below.
1. Find the event
List the shop's events. posId: "auto" selects the shop's own sales channel.
query DiscoverEvents {
offers(posId: "auto", offerableTypes: [EVENT], first: 20) {
edges {
node {
... on Event {
id
nameTranslated
span
timeZone
locationName
bookingAvailability
}
}
}
}
}{
"data": {
"offers": {
"edges": [
{
"node": {
"id": "4711",
"nameTranslated": "Jazz evening",
"span": "[2026-11-12T18:00:00Z,2026-11-12T21:00:00Z)",
"timeZone": "Europe/Berlin",
"locationName": "Main hall",
"bookingAvailability": "AVAILABLE"
}
}
]
}
}
}span is the event's start and end in UTC; show them in the event's timeZone, here 19:00 to 22:00 in Berlin. Offer only events whose bookingAvailability is AVAILABLE; SOLD_OUT and EXCLUSIVELY_BOOKED events cannot be booked.
2. Read the price categories
An event is sold in price categories, such as adult and reduced tickets. Read the event's price rules: each one is a price category with its price.
query EventPrices($id: ID!) {
offer(id: $id, type: EVENT, posId: "auto") {
... on Event {
id
remainingQuota
priceRules(posId: "auto") {
id
priceCategory {
nameTranslated
}
product {
currentPriceValue
}
}
}
}
}{ "id": "4711" }{
"data": {
"offer": {
"id": "4711",
"remainingQuota": "120",
"priceRules": [
{
"id": "31",
"priceCategory": { "nameTranslated": "Adult" },
"product": { "currentPriceValue": { "amount": 2500, "currency": "EUR" } }
},
{
"id": "32",
"priceCategory": { "nameTranslated": "Reduced" },
"product": { "currentPriceValue": { "amount": 1500, "currency": "EUR" } }
}
]
}
}
}Money values contain the amount in the currency's smallest unit, so 2500 is 25.00 EUR. remainingQuota is the number of places left, sent as a string.
3. Add the tickets to a cart
The first cart mutation without a requestId creates the cart. Keep the returned accessToken: it is the cart token for every later request. Add one pricing per price category the customer chose, with the price rule's id and the number of tickets.
mutation AddToCart($input: RequestItemCreateInput!) {
requestItemCreate(input: $input) {
request {
accessToken
totalGrossValue
checkoutHold {
expiresAt
secondsRemaining
}
}
requestItem {
id
}
errors {
key
message
messageTranslated
}
}
}{
"input": {
"offerableType": "EVENT",
"offerableId": "4711",
"pricings": [
{ "quantity": 1, "priceOriginType": "PRICE_RULE", "priceOriginId": "31" },
{ "quantity": 1, "priceOriginType": "PRICE_RULE", "priceOriginId": "32" }
]
}
}The response contains the cart token, the cart total and the checkout hold. The total can be higher than the ticket prices when the venue charges fees. To add another event to the same cart, send the cart token as requestId in the next requestItemCreate.
4. Add the customer
Send the customer's details with the cart token as id. Add a billing address; mark it with billing: true.
mutation AddCustomer($input: RequestUpdateInput!) {
requestUpdate(input: $input) {
request {
contact {
email
}
billingAddress {
city
}
checkoutPolicy {
compatible
targetState
}
requiresShipping
customFieldsCompletion {
allRequiredComplete
}
}
errors {
key
message
messageTranslated
}
}
}{
"input": {
"id": "<cart token>",
"contact": {
"firstName": "Alex",
"lastName": "Example",
"email": "[email protected]",
"customer": {
"addresses": [
{
"billing": true,
"street": "Example Street 1",
"postalCode": "12345",
"city": "Berlin",
"country": "DE"
}
]
}
}
}
}Check the returned cart before checkout:
| Field | What to do |
|---|---|
checkoutPolicy.targetState is BOOKED | Continue with this tutorial. |
checkoutPolicy.compatible is false | The cart mixes offers that cannot be checked out together. Remove the lines listed in checkoutPolicy.conflicts. |
requiresShipping is true | Add an address with shipping: true in a country from shop.allowedShippingCountries. |
customFieldsCompletion.allRequiredComplete is false | Collect the required answers before checkout; see the note below the table. |
When required questions are open, Collect booking and attendee details with the Booking API shows how to answer them.
5. Read the required legal documents
Customers must accept the shop's required legal documents. Show a checkbox for each document labelled with checkboxLabelTranslated, or with nameTranslated when no label is set, and link to the document text from legalDocument(id:).
query RequiredDocuments {
legalDocuments(required: [true], first: 20) {
edges {
node {
id
nameTranslated
checkboxLabelTranslated
publishedAt
}
}
}
}Keep each document's id and publishedAt for the next step. publishedAt identifies the version the customer accepted. If the shop has more than 20 required documents, page through them as the checkout guide shows.
6. Complete checkout
After the customer accepts the documents, complete checkout. The response contains the invoice and the hosted payment link.
mutation CompleteCheckout($input: RequestPaymentInitiateInput!) {
requestPaymentInitiate(input: $input) {
request {
state
}
invoice {
accessToken
paymentState
paymentLink
}
errors {
key
message
messageTranslated
}
}
}{
"input": {
"id": "<cart token>",
"legalDocumentAcceptances": [
{ "legalDocumentId": "1", "legalDocumentVersion": "2026-09-23T19:21:22+00:00" },
{ "legalDocumentId": "2", "legalDocumentVersion": "2026-09-23T19:21:22+00:00" }
]
}
}{
"data": {
"requestPaymentInitiate": {
"request": { "state": "BOOKED" },
"invoice": {
"accessToken": "<invoice token>",
"paymentState": "REQUIRES_PAYMENT",
"paymentLink": "https://tickets.example.com/en/payments/<token>"
},
"errors": null
}
}
}Redirect the customer's browser to paymentLink. The customer chooses a payment method and pays on the hosted payment page, which uses the language of your Accept-Language header.
7. Check the payment
Read the invoice with the invoice token to follow the payment. paymentState becomes PAID when the payment succeeds.
query PaymentStatus($invoiceToken: IdOrNumberOrToken!) {
invoice(id: $invoiceToken) {
paymentState
paymentIntentCreateError
payments {
state
providerInitializationFailed
}
request {
state
checkoutHold {
secondsRemaining
}
ticketsUrl: shareablePublicTicketsUrl(format: PDF)
}
}
}The tickets URL can be present while ticket activation is pending. It may open the hosted ticket page, which makes the tickets available when activation finishes. Do not use the URL's presence as a signal that a PDF is ready.
Other offers
The same steps sell every kind of offer. Only finding the offer and the AddToCart variables change.
General admission. An admission without time slots, such as a day ticket, works like an event: use offerableTypes: [ADMISSION], read the price rules with ... on Admission and add the tickets with offerableType: "ADMISSION". When adding fails with an error for requestItems.0.offerableSpan, the admission has time slots.
Time slots. Event templates and admissions with time slots need a date and time. Show dates and time slots with the Booking API shows the calendar, the slots of a day and how to add a slot.
Vouchers. Find the shop's vouchers and add one with the voucher's id as offer and price origin:
query DiscoverVouchers {
offers(posId: "auto", offerableTypes: [VOUCHER_CONFIGURATION], first: 20) {
edges {
node {
... on VoucherConfiguration {
id
nameTranslated
currentPriceValue
}
}
}
}
}{
"input": {
"offerableType": "VOUCHER_CONFIGURATION",
"offerableId": "<voucher id>",
"pricings": [{ "quantity": 1, "priceOriginType": "VOUCHER_CONFIGURATION", "priceOriginId": "<voucher id>" }]
}
}Check your work
- You stored the cart token from step 3 and used it in steps 4 and 6.
- The customer accepted every required legal document before step 6.
- The customer landed on the hosted payment page from
paymentLink.