> ## Documentation Index
> Fetch the complete documentation index at: https://boxo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Flights Integration

> Host app integration guide for the flights miniapp — events, payments, and booking info.

For host app owners integrating the flights miniapp.

This document defines every event sent by the flights miniapp to a host app, and every host-app request concerning a flight order. It covers payloads, triggers, authentication, retries, and integration flow.

All instructions use the host-app perspective: endpoints exposed by the host app, Boxo Platform endpoints called by the host app, and the data exchanged through each.

## How the integration fits together

The host app and flights miniapp do not call each other directly. The Boxo Platform mediates both directions.

```mermaid theme={"system"}
flowchart LR
  HostApp["Host app"] <--> Platform["Boxo Platform"] <--> Miniapp["Flights miniapp"]
```

Consequently, all host-app inbound requests arrive from the Boxo Platform and use the host app's credentials. All host-app outbound requests go to a Boxo Platform endpoint, which relays the request to the flights miniapp and returns its response.

### Endpoints exposed by the host app

The host app must expose the following endpoints and register each URL in the Boxo Dashboard. The platform authenticates with Basic `base64(hostapp_client_id:hostapp_secret_key)`, using the host-app credentials configured in the dashboard.

| Endpoint                 | Method            | Purpose                                                                     | Required for                           |
| ------------------------ | ----------------- | --------------------------------------------------------------------------- | -------------------------------------- |
| Event receiver           | `POST`            | Receives every flight order event                                           | All order events                       |
| Create order payment     | `POST`            | Creates a host-app payment and returns `order_payment_id`                   | Checkout                               |
| Get order payment status | `POST` (or `GET`) | Returns the current status of a payment                                     | Recovery when a payment result is lost |
| Boxo Connect endpoints   | —                 | Identify the traveller. OAuth (get access token + get user data) or Direct. | Login                                  |

<Warning>
  The **Get order payment status** endpoint is not optional in practice. Payment results do get lost in transit, and this is the only way an order stuck mid-payment ever resolves — otherwise a traveller can be charged and never ticketed.
</Warning>

### Boxo Platform endpoints called by the host app

| Operation                         | Platform endpoint                                                |
| --------------------------------- | ---------------------------------------------------------------- |
| Report a payment result           | `POST /api/v1/orders/complete-order/`                            |
| Request booking information       | `POST /api/v1/events/hostapp/` with `event_type: "booking.info"` |
| Connect a user (Direct flow only) | `POST /api/v1/connect/`                                          |

### Dashboard settings to confirm

| Setting                                                    | Why                                                         |
| ---------------------------------------------------------- | ----------------------------------------------------------- |
| **Event receiver enabled**, with the host-app receiver URL | Off by default. No order events are sent until enabled.     |
| `hostapp_client_id` / `hostapp_secret_key`                 | Used to authenticate calls in both directions               |
| Payment endpoint URLs                                      | Create order payment, and get order payment status          |
| IP whitelisting or Request Signaturing                     | Required for Boxo Platform endpoints called by the host app |

<Note>
  Boxo must enable Event Bridge for both the host app and the flights miniapp. Enabling only one side prevents event delivery.
</Note>

## Authentication

**Calls received by the host app** (event receiver, create order payment, get payment status) carry:

```
Authorization: Basic base64({hostapp_client_id}:{hostapp_secret_key})
Content-Type: application/json
```

The host app must verify these credentials on every request. There is no signature over the body. Host-app endpoints should answer **`200` in all cases**, using a response field to report problems; a non-200 response is treated as a delivery failure and retried.

**Calls made by the host app** to the Event Bridge API use the same Basic credentials. Payment platform endpoints such as `complete-order` are authorized by **IP whitelisting** or **request signaturing** configured in the dashboard.

<Note>
  IP whitelisting and request signaturing are also available for securing server-to-server calls. See [Security measures](/host-apps/SecurityMeasures).
</Note>

## What an event delivery looks like

Every flight order event arrives at the host app's **event receiver** in the platform's standard envelope:

```json theme={"system"}
{
  "app_id": "app_XXXXXX",
  "client_id": "host_XXXXXXXXXXXX",
  "event_type": "order.succeeded",
  "payload": { }
}
```

| Field        | Notes                                                                                          |
| ------------ | ---------------------------------------------------------------------------------------------- |
| `app_id`     | The flights miniapp identifier. Host apps serving several miniapps should route on this field. |
| `client_id`  | Host app identifier                                                                            |
| `event_type` | One of the five types in [The five event types](#the-five-event-types)                         |
| `payload`    | The order body — [Payload reference](#payload-reference). Identical shape for every type.      |

Return HTTP `200`. The response body is forwarded to the flights miniapp, but the flights event flow does not depend on its contents; `{}` is sufficient.

<Tip>
  The receiver may also receive event types from other miniapps and the platform. Unsupported event types should be acknowledged with `200` and ignored.
</Tip>

## The five event types

The event type identifies the outcome and can be used for routing without inspecting the payload.

All five lifecycle events require a settled host-app payment. At the time an event is first emitted, `payment_status` is `PAID`. A pending, cancelled, or failed payment never produces a lifecycle event.

| Type                | Fires when                                                                                    |
| ------------------- | --------------------------------------------------------------------------------------------- |
| `order.succeeded`   | The ticket exists. Booking is confirmed and issued.                                           |
| `order.failed`      | The booking did not complete — the airline refused it, or a paid change could not be applied. |
| `order.refunded`    | Money is owed back to the traveller. Carries the exact net amount to return.                  |
| `booking.changed`   | An existing trip's itinerary was rewritten — new dates, times, or flights.                    |
| `booking.cancelled` | The trip is cancelled, by the traveller, the airline, or support.                             |

Nothing is sent while an order is still moving — awaiting payment or mid-ticketing. Events communicate final outcomes rather than intermediate steps.

### `order.succeeded`

Sent once the airline has issued the ticket. The host app should use this event to add or confirm the booking. `booking_reference` holds the PNR and `booking` contains the information required to render a booking card.

Some airlines issue asynchronously, so this can arrive minutes after the payment settled. It fires when the ticket genuinely exists, not when it was requested.

### `order.failed`

The trip was not delivered — typically the airline refused the booking, or the traveller paid for a change the airline then would not make.

<Warning>
  **Not sent when the host app declines the charge.** A payment rejected by the host app is already known to it, and an unpaid order must not appear as a failed booking.
</Warning>

If money was taken and no ticket came back, `order.refunded` follows.

### `order.refunded`

Sent when a refund obligation is opened, not when it later completes. The host app must return `refund.amount` in `refund.currency` against `refund.source_order_payment_id`, which is the `order_payment_id` originally issued by the host app.

`refund.amount` is **already net** of any airline penalty. `penalty_amount` is informational and must not be subtracted again.

| `refund.reason`      | Meaning                                                     |
| -------------------- | ----------------------------------------------------------- |
| `fulfillment_failed` | The traveller was charged and no ticket was produced        |
| `booking_cancelled`  | A cancellation was accepted; `amount` is the net refund     |
| `booking_changed`    | A change produced a credit; the original booking stays paid |

### `booking.changed`

The trip moved. The payload carries the **new** flights. `status` is still `BOOKED` and `miniapp_order_id` is unchanged — this updates an existing booking, it is not a second ticket.

Fires on a confirmed change and when the flights miniapp detects an airline-initiated itinerary retime.

### `booking.cancelled`

The trip has ended. If a refund is due, a separate `order.refunded` follows.

## Payload reference

The `payload` object of every order event. **All keys are always present.** Missing data is `""` or `null`, never an omitted key. Amounts are decimal **strings**, never numbers.

```json theme={"system"}
{
  "miniapp_order_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "BOOKED",
  "payment_status": "PAID",
  "fulfillment_status": "COMPLETED",
  "booking_reference": "X7G9K2",
  "total_amount": "412.50",
  "total_currency": "USD",
  "route": "LHR-BKK",
  "departing_at": "2026-09-10T07:15:00+00:00",
  "payment_error_code": "",
  "booking": { },
  "price": { },
  "refund": null
}
```

### Top level

| Field                | Type             | Notes                                                                                                                                                                   |
| -------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `miniapp_order_id`   | string (UUID)    | The order id. Stable across every event for that order, and identical to `miniapp_order_id` in the create order payment request. **Use it as the host-app record key.** |
| `status`             | string           | One-word summary. See [Reading status](#reading-status).                                                                                                                |
| `payment_status`     | string           | `PENDING` \| `PAID` \| `FAILED` \| `REFUNDED`                                                                                                                           |
| `fulfillment_status` | string           | `PENDING` \| `PROCESSING` \| `COMPLETED` \| `FAILED` \| `CANCELLED`                                                                                                     |
| `booking_reference`  | string           | Airline PNR. `""` until confirmed. Same as `booking.reference`.                                                                                                         |
| `total_amount`       | decimal string   | What the traveller was charged                                                                                                                                          |
| `total_currency`     | string           | ISO 4217 code for `total_amount`                                                                                                                                        |
| `route`              | string           | `{origin}-{destination}` IATA of the first journey, e.g. `LHR-BKK`. `""` before an itinerary exists.                                                                    |
| `departing_at`       | string           | First departure as an ISO-8601 UTC instant, or `""`                                                                                                                     |
| `payment_error_code` | string           | Set when something went wrong with payment or booking; otherwise `""`                                                                                                   |
| `booking`            | object \| `null` | The trip. See [`booking`](#booking). `null` only for an order with no trip attached.                                                                                    |
| `price`              | object           | Always present. See [`price`](#price).                                                                                                                                  |
| `refund`             | object \| `null` | Present on `order.refunded`; `null` otherwise. See [`refund`](#refund--on-orderrefunded-only).                                                                          |

### `booking`

No airline is named at this level on purpose: one trip can be flown by several carriers. Airlines live on segments.

| Field         | Type           | Notes                                                          |
| ------------- | -------------- | -------------------------------------------------------------- |
| `reference`   | string         | PNR                                                            |
| `origin`      | place          | Trip origin                                                    |
| `destination` | place          | Trip destination                                               |
| `cabin_class` | string         | Cabin for the trip, or `""`                                    |
| `stops`       | int            | Total stops                                                    |
| `passengers`  | list           | See [Passenger](#passenger)                                    |
| `seats`       | list of string | Seat designators bought with the flight, e.g. `["23A", "23B"]` |
| `journeys`    | list           | Outbound, return, or multi-city hops. See [Journey](#journey). |

### Place

| Field       | Type   | Notes        |
| ----------- | ------ | ------------ |
| `iata_code` | string | e.g. `DXB`   |
| `name`      | string | Airport name |
| `city_name` | string | e.g. `Dubai` |

Unknown place → all three `""`.

Names are included so the host app does not require its own IATA lookup table. They use one stable catalogue language rather than the traveller's locale; host apps may localize them independently.

### Passenger

| Field            | Type                                                |
| ---------------- | --------------------------------------------------- |
| `passenger_type` | string (`adult`, `child`, `infant_without_seat`, …) |
| `given_name`     | string                                              |
| `family_name`    | string                                              |

### Journey

| Field                         | Type   | Notes                                                                   |
| ----------------------------- | ------ | ----------------------------------------------------------------------- |
| `position`                    | int    | 0-based order of the hop                                                |
| `origin`, `destination`       | place  |                                                                         |
| `departing_at`, `arriving_at` | string | **Airport wall-clock**, as printed on a boarding pass. `""` if unknown. |
| `departs_at`, `arrives_at`    | string | The same moments as UTC ISO-8601 instants                               |
| `duration`                    | string | ISO-8601 duration (`PT11H20M`) or `""`                                  |
| `stops`                       | int    |                                                                         |
| `segments`                    | list   | See [Segment](#segment)                                                 |

Display `departing_at` / `arriving_at`. Sort and compare on `departs_at` / `arrives_at`.

### Segment

| Field                                     | Type    | Notes                                                           |
| ----------------------------------------- | ------- | --------------------------------------------------------------- |
| `airline`                                 | carrier | Marketing carrier — whose flight number the ticket shows        |
| `operating_airline`                       | carrier | Who actually flies it. Blank fields when the same as `airline`. |
| `flight_number`                           | string  | e.g. `1463`                                                     |
| `origin`, `destination`                   | place   |                                                                 |
| `departing_at`, `arriving_at`             | string  | Wall-clock                                                      |
| `duration`                                | string  | ISO-8601 or `""`                                                |
| `origin_terminal`, `destination_terminal` | string  | e.g. `T3`, or `""`                                              |
| `cabin_class`                             | string  |                                                                 |

### Carrier

| Field             | Type   | Notes                               |
| ----------------- | ------ | ----------------------------------- |
| `iata_code`       | string |                                     |
| `name`            | string | Airline name                        |
| `logo_symbol_url` | string | Absolute image URL, ready to render |

All fields are `""` when unknown. A logo is supplied when available, so a separate host-app airline image library is not required.

### `price`

| Field             | Type           | Notes                                                          |
| ----------------- | -------------- | -------------------------------------------------------------- |
| `currency`        | string         | Same as `total_currency`                                       |
| `base_amount`     | decimal string | Fare before tax and extras; `""` if not broken out             |
| `tax_amount`      | decimal string |                                                                |
| `services_amount` | decimal string | Seats, bags, and other extras                                  |
| `total_amount`    | decimal string | Same as the top-level `total_amount`                           |
| `local_currency`  | string         | The traveller's currency when local pricing applies; else `""` |
| `local_total`     | decimal string | Rounded local total; else `""`                                 |

### `refund` — on `order.refunded` only

| Field                     | Type           | Notes                                                                                  |
| ------------------------- | -------------- | -------------------------------------------------------------------------------------- |
| `id`                      | string (UUID)  | Refund id, stable across retries of this event                                         |
| `source_order_payment_id` | string         | The `order_payment_id` issued by the host app; process the refund against this payment |
| `amount`                  | decimal string | Net amount to return to the traveller                                                  |
| `currency`                | string         |                                                                                        |
| `penalty_amount`          | decimal string | Airline penalty already deducted, for explanation only                                 |
| `penalty_currency`        | string         |                                                                                        |
| `reason`                  | string         | See [The five event types](#the-five-event-types)                                      |

## Delivery, retries, and idempotency

* Events are sent **after** the corresponding state change is committed, so delivered payloads describe committed state.
* A failed delivery is retried periodically, up to **5 attempts** in total. After that it is flagged for operational review and can be re-sent once the host-app endpoint is healthy.
* Retried events are **re-derived from current order state**. If the order changes between attempts, the retry describes the current state rather than replaying stale data.
* Delivery failures never affect the booking. An unavailable host-app endpoint cannot prevent ticket issuance.
* Any response other than HTTP `200` counts as a failure and is retried. Unsupported event types must also receive `200`.

<Warning>
  **The receiver must be idempotent.** The same event can arrive more than once, and events for one order can arrive out of order after an outage. Key records on `miniapp_order_id` and treat each payload as current order state rather than as a delta. Compare `payment_status` and `fulfillment_status` with stored values to discard stale repeats.
</Warning>

## Reading `status`

`status` is a one-word summary of two independent facts: whether payment settled and whether a ticket exists. All lifecycle events are emitted only after `payment_status=PAID`; the three status fields still remain in the payload for explicit state tracking.

| `payment_status` | `fulfillment_status` | `status`      | Lifecycle event                                                                                                 |
| ---------------- | -------------------- | ------------- | --------------------------------------------------------------------------------------------------------------- |
| `PAID`           | `COMPLETED`          | `BOOKED`      | `order.succeeded`; `booking.changed` for a confirmed itinerary update                                           |
| `PAID`           | `FAILED`             | `FAILED`      | `order.failed`, followed by `order.refunded` when a refund obligation is opened                                 |
| `PAID`           | `CANCELLED`          | `CANCELLED`   | `booking.cancelled`; may be followed by `order.refunded`                                                        |
| `PAID`           | `PROCESSING`         | `BOOKING`     | none — ticketing is still in progress                                                                           |
| `PAID`           | `PENDING`            | `BOOKING`     | none — payment settled but ticketing has not started                                                            |
| `PENDING`        | any                  | derived state | none — payment has not settled                                                                                  |
| `FAILED`         | any                  | derived state | none — the host app declined or failed the payment                                                              |
| `REFUNDED`       | any                  | derived state | none — no new lifecycle event; `order.refunded` was emitted when the refund was opened while payment was `PAID` |

<Note>
  The critical combination is **`payment_status=PAID` with `fulfillment_status=FAILED`**: the traveller was charged but no ticket was issued. It produces `order.failed`, followed by `order.refunded`.
</Note>

## Full example — `order.succeeded`

As delivered to the host-app event receiver:

```json theme={"system"}
{
  "app_id": "app_XXXXXX",
  "client_id": "host_XXXXXXXXXXXX",
  "event_type": "order.succeeded",
  "payload": {
    "miniapp_order_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "status": "BOOKED",
    "payment_status": "PAID",
    "fulfillment_status": "COMPLETED",
    "booking_reference": "X7G9K2",
    "total_amount": "412.50",
    "total_currency": "USD",
    "route": "LHR-BKK",
    "departing_at": "2026-09-10T07:15:00+00:00",
    "payment_error_code": "",
    "booking": {
      "reference": "X7G9K2",
      "origin": {
        "iata_code": "LHR",
        "name": "London Heathrow",
        "city_name": "London"
      },
      "destination": {
        "iata_code": "BKK",
        "name": "Suvarnabhumi",
        "city_name": "Bangkok"
      },
      "cabin_class": "economy",
      "stops": 0,
      "passengers": [
        {
          "passenger_type": "adult",
          "given_name": "Aisha",
          "family_name": "Khan"
        }
      ],
      "seats": ["23A", "23B"],
      "journeys": [
        {
          "position": 0,
          "origin": {
            "iata_code": "LHR",
            "name": "London Heathrow",
            "city_name": "London"
          },
          "destination": {
            "iata_code": "BKK",
            "name": "Suvarnabhumi",
            "city_name": "Bangkok"
          },
          "departing_at": "2026-09-10T08:15:00",
          "arriving_at": "2026-09-10T23:35:00",
          "departs_at": "2026-09-10T07:15:00+00:00",
          "arrives_at": "2026-09-10T16:35:00+00:00",
          "duration": "PT11H20M",
          "stops": 0,
          "segments": [
            {
              "airline": {
                "iata_code": "LH",
                "name": "Lufthansa",
                "logo_symbol_url": "https://img.example/lh.svg"
              },
              "operating_airline": {
                "iata_code": "LH",
                "name": "Lufthansa",
                "logo_symbol_url": "https://img.example/lh.svg"
              },
              "flight_number": "1463",
              "origin": {
                "iata_code": "LHR",
                "name": "London Heathrow",
                "city_name": "London"
              },
              "destination": {
                "iata_code": "BKK",
                "name": "Suvarnabhumi",
                "city_name": "Bangkok"
              },
              "departing_at": "2026-09-10T08:15:00",
              "arriving_at": "2026-09-10T23:35:00",
              "duration": "PT11H20M",
              "origin_terminal": "T3",
              "destination_terminal": "",
              "cabin_class": "economy"
            }
          ]
        }
      ]
    },
    "price": {
      "currency": "USD",
      "base_amount": "350.00",
      "tax_amount": "42.50",
      "services_amount": "20.00",
      "total_amount": "412.50",
      "local_currency": "KZT",
      "local_total": "210000"
    },
    "refund": null
  }
}
```

On `order.refunded`, `payload.refund` carries:

```json theme={"system"}
{
  "id": "11111111-2222-3333-4444-555555555555",
  "source_order_payment_id": "8X566iHGnpUxHXPX3FH0iDsXUVn9ku16xJGQ",
  "amount": "380.00",
  "currency": "USD",
  "penalty_amount": "32.50",
  "penalty_currency": "USD",
  "reason": "booking_cancelled"
}
```

## Request booking information — `booking.info`

The host app sends `booking.info` through the Boxo Event Bridge API. The platform relays the event to the flights miniapp and returns the flights miniapp response as the response body. This operation can backfill state after an outage or retrieve a booking list without storing every lifecycle event.

**`POST /api/v1/events/hostapp/`** on the Boxo Platform.

```bash theme={"system"}
curl --location --request POST 'https://api.boxo.io/api/v1/events/hostapp/' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Basic {{BASE64_ENCODED_CLIENT_ID_AND_CLIENT_SECRET}}' \
  --data-raw '{
    "app_id": "{{FLIGHTS_MINIAPP_ID}}",
    "client_id": "{{YOUR_CLIENT_ID}}",
    "event_type": "booking.info",
    "payload": {
      "user_id": "host-user-1"
    }
  }'
```

| Field                      | Required | Notes                                                                      |
| -------------------------- | -------- | -------------------------------------------------------------------------- |
| `event_type`               | **yes**  | Must be exactly `booking.info`                                             |
| `payload.user_id`          | **yes**  | Host-app traveller id — the same `reference` supplied through Boxo Connect |
| `payload.miniapp_order_id` | no       | Ask about one order. Omit for every trip that traveller booked.            |

### Response

```json theme={"system"}
{
  "success": true,
  "custom_attributes": {
    "bookings": [ { "…same payload as the payload reference…" } ]
  }
}
```

Each entry is exactly the payload from [Payload reference](#payload-reference), so code that already handles order events handles this with no second parser. `refund` is `null` here.

| Situation                                   | Answer                                    |
| ------------------------------------------- | ----------------------------------------- |
| Unknown `user_id`                           | Error — `{"user_id": ["User not found"]}` |
| Known traveller, unknown `miniapp_order_id` | `success: true`, empty `bookings`         |
| Known traveller, no trips                   | `success: true`, empty `bookings`         |

An unknown `user_id` returns an error rather than an empty list, preventing a mismatched reference from appearing to be a traveller with no trips.

## Payments

Flight checkout uses the standard [Boxo Payments](/host-apps/BoxoPayments) flow with three host-app integration points.

### Create order payment request

The Boxo Platform calls the host app's **create order payment** endpoint. The body carries `order.miniapp_order_id`, which is **the same id included in every lifecycle event for that order** and should be stored with the payment.

```json theme={"system"}
{
  "app_id": "app_XXXXXX",
  "order": {
    "currency": "USD",
    "amount": "412.50",
    "subtotal_amount": "412.50",
    "shipping_amount": "0",
    "discount_amount": "0",
    "tax_amount": "0",
    "taxes_included": true,
    "note": "",
    "miniapp_order_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "hostapp_user_id": "host-user-1",
    "custom_attributes": {
      "offer_expires_at": "2027-04-01T00:00:00Z",
      "offer": {
        "origin": {
          "iata_code": "LHR",
          "name": "London Heathrow",
          "city_name": "London"
        },
        "destination": {
          "iata_code": "BKK",
          "name": "Suvarnabhumi",
          "city_name": "Bangkok"
        },
        "cabin_class": "economy",
        "stops": 0,
        "passengers": [
          {
            "passenger_type": "adult",
            "given_name": "Aisha",
            "family_name": "Khan"
          }
        ],
        "seats": ["23A", "23B"],
        "journeys": [
          {
            "position": 0,
            "origin": {
              "iata_code": "LHR",
              "name": "London Heathrow",
              "city_name": "London"
            },
            "destination": {
              "iata_code": "BKK",
              "name": "Suvarnabhumi",
              "city_name": "Bangkok"
            },
            "departing_at": "2026-09-10T08:15:00",
            "arriving_at": "2026-09-10T23:35:00",
            "departs_at": "2026-09-10T07:15:00+00:00",
            "arrives_at": "2026-09-10T16:35:00+00:00",
            "duration": "PT11H20M",
            "stops": 0,
            "segments": [
              {
                "airline": {
                  "iata_code": "LH",
                  "name": "Lufthansa",
                  "logo_symbol_url": "https://img.example/lh.svg"
                },
                "operating_airline": {
                  "iata_code": "LH",
                  "name": "Lufthansa",
                  "logo_symbol_url": "https://img.example/lh.svg"
                },
                "flight_number": "1463",
                "origin": {
                  "iata_code": "LHR",
                  "name": "London Heathrow",
                  "city_name": "London"
                },
                "destination": {
                  "iata_code": "BKK",
                  "name": "Suvarnabhumi",
                  "city_name": "Bangkok"
                },
                "departing_at": "2026-09-10T08:15:00",
                "arriving_at": "2026-09-10T23:35:00",
                "duration": "PT11H20M",
                "origin_terminal": "T3",
                "destination_terminal": "",
                "cabin_class": "economy"
              }
            ]
          }
        ]
      },
      "price": {
        "currency": "USD",
        "base_amount": "350.00",
        "tax_amount": "42.50",
        "services_amount": "20.00",
        "total_amount": "412.50",
        "local_currency": "KZT",
        "local_total": "210000"
      }
    }
  }
}
```

Return `{"order_payment_id": "..."}` with HTTP `200`. The traveller settles that payment through the SDK. If a refund is later required, the same id appears as `refund.source_order_payment_id`.

| Field                                | Meaning                                                                                                          |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `amount`                             | What the traveller is charged. Tax is included in it.                                                            |
| `subtotal_amount`                    | Same as `amount` — flight fares are quoted tax-inclusive                                                         |
| `tax_amount`                         | Always `"0"` — the tax split is not broken out separately                                                        |
| `taxes_included`                     | Always `true` — `amount` is the whole of what the traveller pays                                                 |
| `miniapp_order_id`                   | The order key. Store it with the payment; every event carries it.                                                |
| `hostapp_user_id`                    | The traveller's `reference` from Boxo Connect                                                                    |
| `custom_attributes.offer_expires_at` | When the quoted fare stops being bookable. Always present as ISO-8601 UTC (`…Z`).                                |
| `custom_attributes.offer`            | The quoted trip — same itinerary keys as [`booking`](#booking) on later events, without `reference` (no PNR yet) |
| `custom_attributes.price`            | Fare breakdown — [same `price` object as every order event](#price)                                              |

Flight fares are quoted tax-inclusive: `amount` is the whole of what the traveller pays, `subtotal_amount` equals it, and the tax split is not broken out separately. Do not expect a subtotal below the total.

### Report the payment result

After the traveller confirms, call the platform:

```bash theme={"system"}
curl --location --request POST '[BOXO_PLATFORM_SERVER_URL]/api/v1/orders/complete-order/' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "order_payment_id": "{{ORDER_PAYMENT_ID}}",
    "app_id": "{{FLIGHTS_MINIAPP_ID}}",
    "client_id": "{{YOUR_CLIENT_ID}}",
    "payment_status": "paid"
  }'
```

How each status affects the order:

| `payment_status`      | Effect                                                                                                                                                                                                                               |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `paid`                | The order is marked `PAID` and booking proceeds. A lifecycle event is emitted only after this point: `order.succeeded`, `order.failed`, `booking.changed`, `booking.cancelled`, or `order.refunded`, according to the later outcome. |
| `cancelled`, `failed` | The order closes as unpaid. **No lifecycle event is emitted.** Include `payment_fail_reason` for operational diagnosis.                                                                                                              |
| `in_process`          | No state change; the order remains pending                                                                                                                                                                                           |

Booking starts **after** the payment result is acknowledged, not during the request. `order.succeeded`, rather than the payment-completion response, is the signal that a ticket exists.

A `failed` notice for a payment already recorded as settled is ignored as an out-of-order redelivery.

### Get order payment status

If a payment result is not delivered, the flights miniapp queries the Boxo Platform, which calls the host app's **get order payment status** endpoint. The endpoint must return the current status (`in_process`, `paid`, `cancelled`, or `failed`).

This endpoint is the recovery path for lost payment notifications. A missing or incorrect implementation can leave a charged traveller without a ticket or refund.

## Payment custom attributes

`order.custom_attributes` carries only what the host needs to render the first charge: the fare's expiry, the quoted trip as `offer`, and `price`. A **booking does not exist yet** — it is created after the host marks the payment `paid`, and arrives on `order.succeeded` as [`booking`](#booking).

`offer` uses the same itinerary keys as that later `booking` object (`origin`, `destination`, `journeys`, `passengers`, …) so the payment sheet and the booking card share a parser. It does not include `reference`: there is no PNR until the airline confirms.

Lifecycle fields (`miniapp_order_id`, `status`, `payment_status`, `fulfillment_status`, `booking_reference`, `refund`, …) are omitted.

When an order is also priced in the traveller's own currency, the same object carries a local-pricing audit trail:

```json theme={"system"}
{
  "offer_expires_at": "2027-04-01T00:00:00Z",
  "offer": { "…quoted itinerary, same keys as booking minus reference…" },
  "price": { "…same price object as the payload reference…" },
  "original_price": { "amount": "412.5", "currency": "USD" },
  "initial_converted_price": { "amount": "209987.12", "currency": "KZT" },
  "rounding_rule": "KZT_100"
}
```

| Key                       | Meaning                                                                                                                 |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `offer_expires_at`        | When the quoted fare stops being bookable. Always present as ISO-8601 UTC (`…Z`).                                       |
| `offer`                   | The quoted trip. Same itinerary keys as [`booking`](#booking), without `reference`. Omitted if no itinerary exists yet. |
| `price`                   | Fare breakdown. Same object as [`price` on every order event](#price). Always present.                                  |
| `original_price`          | The price before currency conversion. Omitted when local pricing does not apply.                                        |
| `initial_converted_price` | The converted amount **before** rounding. Omitted when local pricing does not apply.                                    |
| `rounding_rule`           | Which rounding rule produced the charged figure. Omitted when no rule applied.                                          |

The charge amount is always `order.amount`, which is the rounded local total when local pricing applies. The additional pricing keys provide an audit trail after exchange rates change.

## Traveller identity

Login uses [Boxo Connect](/host-apps/BoxoConnect) through either the OAuth or Direct flow. In both flows, **`reference`** in the supplied user data is the relevant identity field.

`reference` becomes the traveller's identity in the flights miniapp. It is sent as `user_id` in `booking.info` and echoed as `hostapp_user_id` in the create order payment request.

<Warning>
  It must be **stable for a user across sessions and devices**. If it changes, that traveller's booking history goes with it.
</Warning>

Supplying `first_name`, `last_name`, `email`, and `phone` allows checkout to prefill traveller details.

## Integration checklist

<Steps>
  <Step title="Enable Event Bridge">
    Request that Boxo enable **Event Bridge** for the host app and flights miniapp, and register the host-app **event receiver URL**.
  </Step>

  <Step title="Register payment endpoints">
    Register the host-app **create order payment** and **get order payment status** URLs. Both are required for safe checkout.
  </Step>

  <Step title="Verify inbound credentials">
    Verify Basic `hostapp_client_id:hostapp_secret_key` on every inbound call.
  </Step>

  <Step title="Acknowledge every event">
    Return `200` from the event receiver for **every** event, including unsupported types.
  </Step>

  <Step title="Route on event_type">
    Route on `event_type`. Do not reduce the five types to one generic status update.
  </Step>

  <Step title="Key records on miniapp_order_id">
    Key records on `payload.miniapp_order_id` and make handlers idempotent.
  </Step>

  <Step title="Render booking cards from the payload">
    Render booking cards from `payload.booking`: names, logos, wall-clock times, terminals, seats, and PNR. No host-app lookup tables are required.
  </Step>

  <Step title="Treat booking.changed as an update">
    Treat `booking.changed` as an update to an existing booking, never as a new one.
  </Step>

  <Step title="Refund the net amount">
    On `order.refunded`, return `refund.amount` — already net — against `refund.source_order_payment_id`.
  </Step>

  <Step title="Read payment and fulfillment together">
    Read `payment_status` and `fulfillment_status` together, not just `status`.
  </Step>

  <Step title="Keep Connect reference stable">
    Keep `reference` stable per user at connect.
  </Step>
</Steps>

## Quick reference

| Direction                  | What                     | Body                                                    | Where                                          |
| -------------------------- | ------------------------ | ------------------------------------------------------- | ---------------------------------------------- |
| Flights miniapp → host app | `order.succeeded`        | envelope + [payload](#payload-reference)                | Host-app event receiver                        |
| Flights miniapp → host app | `order.failed`           | envelope + [payload](#payload-reference)                | Host-app event receiver                        |
| Flights miniapp → host app | `order.refunded`         | envelope + [payload](#payload-reference) with `refund`  | Host-app event receiver                        |
| Flights miniapp → host app | `booking.changed`        | envelope + [payload](#payload-reference)                | Host-app event receiver                        |
| Flights miniapp → host app | `booking.cancelled`      | envelope + [payload](#payload-reference)                | Host-app event receiver                        |
| Flights miniapp → host app | Create order payment     | `{app_id, order}`                                       | Host-app payment endpoint                      |
| Flights miniapp → host app | Get order payment status | `{app_id, client_id, order_payment_id}`                 | Host-app status endpoint                       |
| Host app → flights miniapp | `booking.info`           | `{app_id, client_id, event_type, payload}`              | Platform `POST /api/v1/events/hostapp/`        |
| Host app → flights miniapp | Payment result           | `{order_payment_id, app_id, client_id, payment_status}` | Platform `POST /api/v1/orders/complete-order/` |

Platform reference: [Boxo Event Bridge](/host-apps/CES), [Boxo Payments](/host-apps/BoxoPayments), [Boxo Connect](/host-apps/BoxoConnect).
