# Development quickstart

Learn how to search and book a flight through the CitizenPlane (opens new window) v4 API.


The CitizenPlane v4 API provides a standardized interface for distributors to search, price, order and book flights. The API follows an NDC-inspired booking flow. CitizenPlane (opens new window) only supports EUR as its currency.


# Testing

You'll be given a sandbox API key for the implementation phase and testing process. You can search and book test flights using this sandbox key. No real charge will be induced.


# Complete flow

The search-to-booking flow with the v4 API is as follows:

  1. Search Offers
  2. Get Offer
  3. Get Baggage Options
  4. Get Fares
  5. Create Order
  6. Confirm Order
  7. Get Order
  8. Get Reservation

# Step 1: Search Offers

Search for available flight offers matching the traveler's criteria. You can search for one-way or round-trip itineraries using either airport codes or city codes. Include the customerCode provided by CitizenPlane; it is required for Search Offers and Get Offer.

Flight content

Please note that all our flights are non-refundable, non-exchangeable and have no flex fares. Only ECONOMY class is currently supported.

# Implementation

curl 'https://booking-api.citizenplane.com/v4/search' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}' \
-d '{
  "adults": 1,
  "children": 0,
  "infants": 0,
  "tripType": "ONE_WAY",
  "travelClass": "ECONOMY",
  "outboundSegment": {
    "origin": { "code": "CDG", "type": "AIRPORT" },
    "destination": { "code": "AYT", "type": "AIRPORT" },
    "departureDate": "2027-08-06"
  },
  "inboundSegment": null,
  "customerCode": "ACME"
}'

# Example Response

{
  "offers": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "outboundSegment": {
        "sections": [
          {
            "origin": "CDG",
            "destination": "AYT",
            "departureDate": "2027-08-06T17:45:00",
            "arrivalDate": "2027-08-06T22:40:00",
            "travelClass": "ECONOMY",
            "availableSeats": 10,
            "flightNumber": "511",
            "operatingCarrierCode": "XQ",
            "marketingCarrierCode": "XQ",
            "technicalStops": []
          }
        ]
      },
      "fare": {
        "passengerPricingByPassengerType": {
          "adult": {
            "fareAmount": { "amount": 627.27, "currency": "EUR" },
            "taxAmount": { "amount": 0, "currency": "EUR" },
            "numberOfPassengers": 1
          }
        },
        "includedBaggage": {
          "checkedBaggageQuantity": 1,
          "cabinBaggageQuantity": 1
        },
        "price": {
          "totalAmount": 627.27,
          "sourceAmount": 627.27,
          "currency": "EUR"
        }
      },
      "paymentOptions": [
        { "type": "card", "fee": { "amount": 12.55, "currency": "EUR" } },
        { "type": "balance", "fee": { "amount": 0, "currency": "EUR" } }
      ]
    }
  ]
}

The response contains an array of offers, each with flight segments, pricing, included baggage information and available payment options. balance appears only when CitizenPlane has enabled balance payment for your organization.

Go to the API reference


# Step 2: Get Offer

Once you've identified an interesting offer from the search results, retrieve the detailed offer by providing the exact flight sections and the same required customerCode. This step verifies availability and pricing with the supplier.

The returned offer includes:

  • Detailed fare information with per-passenger pricing
  • Included baggage details
  • Additional passenger information requirements, such as passport needed
  • Available payment options, including any card-processing fee
  • An offer ID valid for 30 minutes

# Implementation

curl 'https://booking-api.citizenplane.com/v4/offer' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}' \
-d '{
  "adults": 1,
  "children": 0,
  "infants": 0,
  "tripType": "ONE_WAY",
  "travelClass": "ECONOMY",
  "outboundSegment": {
    "sections": [
      {
        "origin": "CDG",
        "destination": "AYT",
        "departureDate": "2027-08-06T17:45:00",
        "arrivalDate": "2027-08-06T22:40:00",
        "flightNumber": "511",
        "operatingCarrierCode": "XQ",
        "marketingCarrierCode": "XQ"
      }
    ]
  },
  "inboundSegment": null,
  "customerCode": "ACME"
}'

# Example Response

{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "outboundSegment": {
    "sections": [
      {
        "origin": "CDG",
        "destination": "AYT",
        "departureDate": "2027-08-06T17:45:00",
        "arrivalDate": "2027-08-06T22:40:00",
        "travelClass": "ECONOMY",
        "flightNumber": "511",
        "departureTerminal": "1",
        "arrivalTerminal": "1",
        "operatingCarrierCode": "XQ",
        "marketingCarrierCode": "XQ",
        "technicalStops": []
      }
    ]
  },
  "cheapestFare": {
    "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
    "passengerPricingByPassengerType": {
      "adult": {
        "fareAmount": { "amount": 627.27, "currency": "EUR" },
        "taxAmount": { "amount": 0, "currency": "EUR" },
        "numberOfPassengers": 1
      }
    },
    "includedBaggage": [
      { "quantity": 1, "descriptor": { "weight": 20 }, "type": "CHECK_IN" },
      { "quantity": 1, "descriptor": { "weight": 10, "dimensions": { "width": 56, "height": 45, "depth": 25 } }, "type": "CABIN" }
    ],
    "totalPrice": { "amount": 627.27, "currency": "EUR" }
  },
  "providerAdditionalPassengerInformationByPassengerType": {
    "adult": {
      "providerIdentificationTypeOptions": ["PASSPORT"],
      "providerRequiredAdditionalFields": ["BIRTH_DATE"]
    }
  },
  "paymentOptions": [
    { "type": "card", "fee": { "amount": 12.55, "currency": "EUR" } },
    { "type": "balance", "fee": { "amount": 0, "currency": "EUR" } }
  ]
}

Offer TTL

The offer is cached for 30 minutes. After that, you must retrieve a new offer.

Go to the API reference


# Step 3: Get Baggage Options

Retrieve the available paid baggage options for the offer. This returns only the paid options, because free baggage is already included in the offer's includedBaggage field.

# Implementation

curl 'https://booking-api.citizenplane.com/v4/offer/{offerId}/baggage' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}'

# Example Response

{
  "fareBaggageOptions": [
    {
      "fareId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "outboundSegmentOptions": [
        {
          "type": "CABIN",
          "maxPiecesPerPassenger": 1,
          "maxKilosPerPiece": 10,
          "pricePerPiece": { "amount": 15.00, "currency": "EUR" }
        },
        {
          "type": "CHECK_IN",
          "maxPiecesPerPassenger": 1,
          "maxKilosPerPiece": 20,
          "pricePerPiece": { "amount": 36.05, "currency": "EUR" }
        }
      ],
      "inboundSegmentOptions": null
    }
  ]
}

Go to the API reference


# Step 4: Get Fares

Retrieve all fare options for the offer when you want to present alternatives beyond the cheapest fare returned by Get Offer.

# Implementation

curl 'https://booking-api.citizenplane.com/v4/offer/{offerId}/fares' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}'

Go to the API reference


# Step 5: Create Order

Create an order with passenger details, selected baggage, customer contact information and the chosen payment method. Use { "card": true } for CitizenPlane card processing, or { "balance": true } only when CitizenPlane has enabled balance payment for your organization. If card has a processing fee in paymentOptions, the order total includes that fee.

# Implementation

curl 'https://booking-api.citizenplane.com/v4/order?offerId={offerId}&fareId={fareId}' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}' \
-d '{
  "passengers": [
    {
      "number": 1,
      "type": "adult",
      "title": "MR",
      "name": "John",
      "middleName": "Paul",
      "firstLastName": "Doe",
      "gender": "MALE",
      "birthDate": "1990-01-15",
      "nationalityCountryCode": "FR",
      "countryCodeOfResidence": "FR",
      "localityCodeOfResidency": "FR-PAR",
      "identification": {
        "type": "PASSPORT",
        "identificationNumber": "ABCDE123",
        "expirationDate": "2030-01-01",
        "issueCountryCode": "FR"
      },
      "phone": { "number": "+33600000000", "countryCode": "FR" }
    }
  ],
  "selectedBaggage": [
    {
      "passengerNumber": 1,
      "outboundSegmentBaggageSelection": [
        { "type": "CHECK_IN", "numberOfPieces": 1, "kilosPerPiece": 20 }
      ],
      "inboundSegmentBaggageSelection": null
    }
  ],
  "customerContact": {
    "name": "John",
    "lastNames": "Doe",
    "gender": "MALE",
    "email": "john.doe@example.com",
    "countryCode": "FR",
    "phone": { "number": "+33600000000", "countryCode": "FR" },
    "address": "123 Rue de Paris",
    "cityName": "Paris",
    "zipCode": "75001",
    "carrierPortalCredentials": {
      "username": "carrier-user@example.com",
      "password": "carrier-password"
    }
  },
  "paymentMethod": { "card": true }
}'

# Example Response

{
  "id": "d4e5f6a7-b8c9-4123-abcd-234567890123",
  "offer": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "outboundSegment": { "sections": [...] },
    "fare": {
      "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "passengerPricingByPassengerType": {
        "adult": {
          "fareAmount": { "amount": 627.27, "currency": "EUR" },
          "taxAmount": { "amount": 0, "currency": "EUR" },
          "numberOfPassengers": 1
        }
      },
      "totalPrice": { "amount": 627.27, "currency": "EUR" }
    }
  },
  "passengers": [
    {
      "number": 1,
      "type": "adult",
      "title": "MR",
      "name": "John",
      "firstLastName": "Doe",
      "birthDate": "1990-01-15",
      "nationalityCountryCode": "FR"
    }
  ],
  "selectedBaggage": [
    {
      "passengerNumber": 1,
      "outboundSegmentBaggageSelection": [
        {
          "type": "CHECK_IN",
          "numberOfPieces": 1,
          "kilosPerPiece": 20,
          "pricePerPiece": { "amount": 36.05, "currency": "EUR" }
        }
      ],
      "inboundSegmentBaggageSelection": []
    }
  ],
  "paymentMethod": { "card": true },
  "status": "OPEN",
  "totalPrice": { "amount": 676.59, "currency": "EUR" }
}

The response returns an order with status OPEN. The example total includes the base fare, selected baggage and a card fee computed on the final order amount.

Go to the API reference


# Step 6: Confirm Order

Confirm the order by sending a request body with the same paymentMethod chosen at Create Order.

For card, send exactly one of payment_intent_id or payment_method. You may send three_d_secure only with payment_method; its version must be 1.0.2, 2.1.0, or 2.2.0, and its electronic_commerce_indicator must be 05, 06, 01, or 02. If Stripe requires additional verification or the payment is not ready to capture, Confirm Order returns 406 Not Acceptable. For balance, don't send any Stripe payment fields.

WARNING

An order can only be confirmed once. Duplicate confirmation requests will be rejected.

# Implementation

curl -X POST 'https://booking-api.citizenplane.com/v4/order/{orderId}/confirm' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}' \
-d '{
  "paymentMethod": { "card": true },
  "payment_method": "pm_1PxYzABC123456789",
  "three_d_secure": {
    "version": "2.2.0",
    "electronic_commerce_indicator": "05",
    "cryptogram": "4BQwsg4yuKt0S1LI1nDZTcO9vUM=",
    "transaction_id": "f879ea1c-aa2c-4441-806d-e30406466d79"
  }
}'

# Example Response

{
  "id": "d4e5f6a7-b8c9-4123-abcd-234567890123",
  "offer": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "outboundSegment": { "sections": [...] },
    "fare": { ... }
  },
  "passengers": [...],
  "selectedBaggage": [...],
  "customerContact": { ... },
  "paymentMethod": { "card": true },
  "status": "BOOKED",
  "reservationId": "e5f6a7b8-c9d0-4234-abcd-345678901234",
  "totalPrice": { "amount": 676.59, "currency": "EUR" }
}

The response returns the order with status BOOKED and a reservationId.

Go to the API reference


# Step 7: Get Order

Retrieve the latest persisted order state when you need to inspect an order after creation or confirmation.

# Implementation

curl 'https://booking-api.citizenplane.com/v4/order/{orderId}' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}'

Order status can be OPEN, BOOKED, or FAILED. A successful confirmation returns BOOKED; duplicate confirmation attempts are rejected rather than exposed as a separate order status.

Go to the API reference


# Step 8: Get Reservation

Retrieve the final reservation details including the PNR. You can retrieve the reservation either by reservation ID or by order ID.

# By reservation ID

curl 'https://booking-api.citizenplane.com/v4/reservation/{reservationId}' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}'

# By order ID

curl 'https://booking-api.citizenplane.com/v4/reservation?orderId={orderId}' \
-H 'Accept: application/json' \
-H 'Authorization: {your_api_key}'

# Example Response

{
  "id": "e5f6a7b8-c9d0-4234-abcd-345678901234",
  "orderId": "d4e5f6a7-b8c9-4123-abcd-234567890123",
  "pnr": "CPABCD",
  "email": "john.doe@example.com",
  "outboundSegment": {
    "sections": [
      {
        "origin": "CDG",
        "destination": "AYT",
        "departureDate": "2027-08-06T17:45:00",
        "arrivalDate": "2027-08-06T22:40:00",
        "travelClass": "ECONOMY",
        "flightNumber": "511",
        "departureTerminal": "1",
        "arrivalTerminal": "1",
        "operatingCarrierCode": "XQ",
        "marketingCarrierCode": "XQ",
        "technicalStops": []
      }
    ]
  },
  "passengers": [
    {
      "number": 1,
      "type": "adult",
      "title": "MR",
      "name": "John",
      "firstLastName": "Doe",
      "gender": "MALE",
      "birthDate": "1990-01-15",
      "nationalityCountryCode": "FR"
    }
  ],
  "selectedBaggage": [...],
  "customerContact": { ... },
  "fare": { ... }
}

The response includes the PNR, passenger details, flight segments, baggage selections and fare information.

Go to the API reference


# Error Responses

V4 returns standard Hapi/Boom error payloads. For most client errors, the useful detail is in the message field. V4 does not add a custom type field. Here are common error scenarios you may encounter:

Expired offer (404):

{
  "statusCode": 404,
  "error": "Not Found",
  "message": "this offer b2c3d4e5-f6a7-8901-bcde-f12345678901 does not exist or is no longer valid (TTL: 30min)"
}

Invalid request parameters (400):

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "unsupported travel class"
}

Balance payment unavailable (400):

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "balance payment method is not available for this organization"
}

Order not found (404):

{
  "statusCode": 404,
  "error": "Not Found",
  "message": "order not found"
}

Authentication error (401):

{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Unauthorized"
}

Balance payment forbidden (403):

{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "balance payment method is not available for organization {id}"
}

Additional card verification required (406):

{
  "statusCode": 406,
  "error": "Not Acceptable",
  "message": "Additional verification is required (likely 3D Secure). Please pass all required parameters on your next attempt or contact your card issuer."
}

Payment validation error (422):

{
  "statusCode": 422,
  "error": "Unprocessable Entity",
  "message": "amount mismatch: expected {expected} / sent {sent}"
}

Go to the API reference Errors for the full list of error responses.