# Book your first ticket

This tutorial walks you through a complete integration with the Korus Ticket API from scratch. By the end, you will
have made a real booking and retrieved the customer's ticket document.

The tutorial uses the **demo environment** (`api.demo.korusticket.com`) so you can safely experiment without creating
real bookings.

**What you will build:** A sequence of API calls that authenticates, discovers a product, checks its availability and
price, creates a reservation, converts it to an order, and downloads the ticket document.

**Estimated time:** 20–30 minutes.

## Prerequisites

Before you start, make sure you have:

- Your OIDC credentials: `client_id`, `client_secret`, `token_endpoint`, and `refresh_token_endpoint`. These are
provided when your organization is onboarded. Contact [contact@korusticket.com](mailto:contact@korusticket.com) if
you do not have them yet.
- `curl` installed (all examples in this tutorial use curl).
- Access to at least one catalog in the demo environment. You can verify this in [Step 3](#step-3-discover-the-catalog).


## Step 1: Authenticate

The Korus Ticket API uses OIDC for authentication. You need a short-lived access token to call any endpoint. Request
one using your credentials:


```bash
curl --request POST \
  --url https://auth.non-prod.korusticket.com/realms/korusticket-demo/protocol/openid-connect/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data 'grant_type=client_credentials' \
  --data 'client_id={{YOUR_CLIENT_ID}}' \
  --data 'client_secret={{YOUR_CLIENT_SECRET}}'
```

The response contains the `access_token` you will use in all subsequent requests:


```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6...",
  "expires_in": 1799,
  "refresh_expires_in": 1799,
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6...",
  "token_type": "Bearer"
}
```

Replace `{{YOUR_JWT_TOKEN}}` in all subsequent curl commands with this `access_token` value. Tokens expire after the
number of seconds indicated by `expires_in` (typically 1800 seconds). See the
[Authentication guide](/authentication/oidc) for the full token refresh flow.

## Step 2: List your catalogs

A **Catalog** is the entry point to all products and offers. Your organization has access to one or more catalogs,
each provided by a different ticket issuer.


```bash
curl --request GET \
  --url https://api.demo.korusticket.com/v1/ticketing/catalogs \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Accept-Language: en'
```


```json
{
  "@context": "/contexts/TicketingCatalog",
  "@id": "/v1/ticketing/catalogs",
  "@type": "Collection",
  "totalItems": 1,
  "member": [
    {
      "@id": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e",
      "@type": "TicketingCatalog",
      "id": "0194abdc-3a71-72b0-a9b9-89d4be0cd73e",
      "name": "Puy du Fou - CSE"
    }
  ]
}
```

Note the `@id` of your catalog — you will use it in every subsequent request. In this tutorial the catalog IRI is
`/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e`. Substitute it with your own.

If the response returns an empty `member` array, your organization has not been granted access to any catalog. Contact
[contact@korusticket.com](mailto:contact@korusticket.com).

## Step 3: Browse the catalog

### List product bases

**ProductBases** group related products together (for example, different seating categories for the same event). List
them to find what is available in your catalog:


```bash
curl --request GET \
  --url 'https://api.demo.korusticket.com/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/product_bases?itemsPerPage=5' \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Accept-Language: en'
```


```json
{
  "@type": "Collection",
  "totalItems": 2,
  "member": [
    {
      "@id": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/product_bases/0194abdc-3a71-72b0-a9b9-89d4be11111",
      "@type": "TicketingProductBase",
      "id": "0194abdc-3a71-72b0-a9b9-89d4be11111",
      "name": "Cinéscénie",
      "disabled": false
    },
    {
      "@id": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/product_bases/0194abdc-3a71-72b0-a9b9-89d4be22222",
      "@type": "TicketingProductBase",
      "id": "0194abdc-3a71-72b0-a9b9-89d4be22222",
      "name": "Grand Parc",
      "disabled": false
    }
  ]
}
```

### List products

Each ProductBase contains one or more **Products**. A Product is the purchasable unit — it represents a specific
combination of options (price category, zone, etc.):


```bash
curl --request GET \
  --url 'https://api.demo.korusticket.com/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/product_bases/0194abdc-3a71-72b0-a9b9-89d4be11111/products?itemsPerPage=5' \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Accept-Language: en'
```


```json
{
  "@type": "Collection",
  "totalItems": 3,
  "member": [
    {
      "@id": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/products/0194abfe-1c04-7ade-9bc3-dbcfefb12652",
      "@type": "TicketingProduct",
      "id": "0194abfe-1c04-7ade-9bc3-dbcfefb12652",
      "name": "Cinéscénie - Adult",
      "disabled": false
    }
  ]
}
```

Pick a product you want to book. Note its `@id` IRI.

## Step 4: Find an offer

**Offers** are date-specific price entries for a product. List offers for your chosen product:


```bash
curl --request GET \
  --url 'https://api.demo.korusticket.com/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers?product[]=/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/products/0194abfe-1c04-7ade-9bc3-dbcfefb12652&itemsPerPage=3' \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Accept-Language: en'
```


```json
{
  "@type": "Collection",
  "totalItems": 5,
  "member": [
    {
      "@id": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers/0194ac40-fda7-78c1-8fcb-26fc2df25894",
      "@type": "TicketingOffer",
      "id": "0194ac40-fda7-78c1-8fcb-26fc2df25894",
      "date": "2026-06-07",
      "price": {
        "amountInclTax": "80.00",
        "originalAmountInclTax": null,
        "currency": "EUR"
      },
      "availabilityCheckRequired": true,
      "realtimePriceRequired": false,
      "tickets": [
        {
          "@id": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers/0194ac40-fda7-78c1-8fcb-26fc2df25894/tickets/0194ac40-fda7-78c1-8fcb-26fc2e2612d4",
          "@type": "TicketingOfferTicket",
          "id": "0194ac40-fda7-78c1-8fcb-26fc2e2612d4",
          "name": "Adult entry",
          "sessions": []
        }
      ]
    }
  ]
}
```

Two flags tell you whether you need extra calls before booking:

- `availabilityCheckRequired: true` — you must verify availability before reserving (see Step 5).
- `realtimePriceRequired: true` — you must fetch the current price before reserving (see Step 5).


Note the offer `@id` and the `@id` of each ticket in the `tickets` array — you need both when creating a reservation.
If a ticket has `sessions`, note the session `@id` you want to book as well.

## Step 5: Check availability and price (conditional)

### Availability

Because `availabilityCheckRequired` is `true` for the offer above, check that seats are still available:


```bash
curl --request GET \
  --url https://api.demo.korusticket.com/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers/0194ac40-fda7-78c1-8fcb-26fc2df25894/availability \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Accept-Language: en'
```


```json
{
  "@context": "/contexts/TicketingOfferAvailability",
  "@id": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers/0194ac40-fda7-78c1-8fcb-26fc2df25894/availability",
  "@type": "TicketingOfferAvailability",
  "availableQuantity": 412,
  "sessionsAvailability": []
}
```

If `availableQuantity` is `0`, the offer is sold out and you cannot reserve it.

### Real-time price

If `realtimePriceRequired` were `true`, you would also call the price endpoint to get the latest amount:


```bash
curl --request GET \
  --url https://api.demo.korusticket.com/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers/0194ac40-fda7-78c1-8fcb-26fc2df25894/price \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}'
```

See the [Get realtime price](/how-to/real-time-price) guide for full details.

## Step 6: Create a reservation

A **Reservation** temporarily holds the offer, guaranteeing availability and price for a short window (see
`expiresAt` in the response). Make this call after the customer selects what they want to buy, but before they pay.


```bash
curl --request POST \
  --url https://api.demo.korusticket.com/v1/booking/reservations/_bulk \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Accept-Language: en' \
  --header 'Content-Type: application/ld+json' \
  --data '{
    "items": [
      {
        "quantity": 1,
        "offer": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers/0194ac40-fda7-78c1-8fcb-26fc2df25894",
        "ticketSessions": [
          {
            "ticket": "/v1/ticketing/catalogs/0194abdc-3a71-72b0-a9b9-89d4be0cd73e/offers/0194ac40-fda7-78c1-8fcb-26fc2df25894/tickets/0194ac40-fda7-78c1-8fcb-26fc2e2612d4"
          }
        ]
      }
    ]
  }'
```


```json
{
  "@type": "CreateReservationBulkOutput",
  "reservations": [
    {
      "@id": "/v1/booking/reservations/35e72d9b-0fdc-44e3-869c-e1781c14418e",
      "@type": "BookingReservation",
      "id": "35e72d9b-0fdc-44e3-869c-e1781c14418e",
      "expired": false,
      "totalAmountInclTax": "80.00",
      "totalAmountExclTax": "75.83",
      "expiresAt": "2026-06-07T08:10:53+00:00",
      "status": "reserved"
    }
  ],
  "failedReservations": []
}
```

The `expiresAt` timestamp tells you how long the hold lasts. If you do not convert the reservation to an order before
this time, the hold is released and you must start from Step 6 again.

Note the reservation `@id` — you need it in the next step.

## Step 7: Create an order

Once the customer's **payment is confirmed**, convert the reservation into an order. Do not call this endpoint before
payment is complete.


```bash
curl --request POST \
  --url https://api.demo.korusticket.com/v1/booking/orders/_bulk \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Content-Type: application/ld+json' \
  --data '{
    "reservations": [
      "/v1/booking/reservations/35e72d9b-0fdc-44e3-869c-e1781c14418e"
    ],
    "customer": {
      "firstname": "Jane",
      "lastname": "Doe",
      "email": "jane.doe@example.com",
      "phone": "0612345678",
      "country": "FR",
      "language": "en"
    },
    "externalReference": "MY_ORDER_REF_001"
  }'
```


```json
{
  "@type": "CreateOrderBulkOutput",
  "orders": [
    {
      "@id": "/v1/booking/orders/0194b132-448c-7d1b-bb13-f8e09ae5c18f",
      "@type": "BookingOrder",
      "id": "0194b132-448c-7d1b-bb13-f8e09ae5c18f",
      "reference": "KT-ORD-6799E80128885",
      "externalReference": "MY_ORDER_REF_001",
      "status": "confirmed",
      "totalAmountInclTax": "80.00",
      "documents": [
        "/v1/booking/orders/0194b132-448c-7d1b-bb13-f8e09ae5c18f/documents/0194b132-449c-79cd-9da1-50be439b6e9d"
      ]
    }
  ]
}
```

The `externalReference` field is **required**. Use it to store your own booking identifier (max 255 characters) —
this lets you correlate Korus Ticket orders with your own system.

The `documents` array lists the ticket documents attached to this order.

## Step 8: Download the ticket document

Order documents may not be available immediately. The `downloadableAt` property on each document tells you when it
will be ready. First, list the documents to check:


```bash
curl --request GET \
  --url https://api.demo.korusticket.com/v1/booking/orders/0194b132-448c-7d1b-bb13-f8e09ae5c18f/documents \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}'
```


```json
{
  "@type": "Collection",
  "totalItems": 1,
  "member": [
    {
      "@id": "/v1/booking/orders/0194b132-448c-7d1b-bb13-f8e09ae5c18f/documents/0194b132-449c-79cd-9da1-50be439b6e9d",
      "@type": "BookingOrderDocument",
      "id": "0194b132-449c-79cd-9da1-50be439b6e9d",
      "downloadableAt": "2026-06-07T08:00:00+00:00"
    }
  ]
}
```

Once `downloadableAt` has passed, generate a temporary download URL with a PATCH call:


```bash
curl --request PATCH \
  --url https://api.demo.korusticket.com/v1/booking/orders/0194b132-448c-7d1b-bb13-f8e09ae5c18f/documents/0194b132-449c-79cd-9da1-50be439b6e9d \
  --header 'accept: application/ld+json' \
  --header 'Authorization: Bearer {{YOUR_JWT_TOKEN}}' \
  --header 'Accept-Language: en' \
  --header 'Content-Type: application/merge-patch+json' \
  --data '{}'
```


```json
{
  "@type": "BookingOrderDocument",
  "id": "0194b132-449c-79cd-9da1-50be439b6e9d",
  "temporaryUrl": "https://storage.googleapis.com/...",
  "cachedLanguage": "en"
}
```

Use the `temporaryUrl` to download the PDF. The URL expires after a short time — do not share it directly with
customers. Either download and store the file in your own storage, or call this endpoint again to generate a fresh
URL when needed.

## What's next

You have completed a full booking flow. Here are the common follow-up topics:

- **Multiple items in one booking** — Learn how to reserve several offers at once and handle partial failures in the
[Handle reservations and orders errors](/how-to/create-reservations-orders) guide.
- **Beneficiary requirements** — Some ticket issuers require passenger details (name, date of birth, etc.) before
documents can be generated. See the [Beneficiary requirements](/how-to/beneficiary-requirements) guide.
- **Seating** — If the product base has `seatSelectionEnabled: true`, you can let customers choose specific seats
using the [Seat maps](/openapi/ticketingseatmap) endpoints.
- **Refreshing your token** — Access tokens expire. See the [Authentication guide](/authentication/oidc) for
the token refresh flow.