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

# Migrating from /rest to /v2

> Step-by-step guide for external partners moving from the legacy /rest endpoints to the v2 Distribution API.

The legacy `/rest/offers` and `/rest/orders` endpoints are deprecated and will be removed after a usage-gated window of at least 90 days. This guide covers everything you need to migrate to their v2 replacements.

<Note>
  Deprecated endpoints respond with `Deprecation`, `Sunset`, and `Link` headers
  pointing here. Monitor those headers in your client to track the sunset date.
</Note>

## Path mapping

| Legacy endpoint                                             | v2 endpoint                         | Notes                                  |
| ----------------------------------------------------------- | ----------------------------------- | -------------------------------------- |
| `GET /rest/offers`                                          | `GET /v2/offers`                    | See [Offers](#offers) below            |
| `POST /rest/orders`                                         | `POST /v2/bookings`                 | Returns a booking, not a ticket array  |
| `GET /rest/orders`                                          | `GET /v2/bookings`                  | Paginates bookings, not bare tickets   |
| `GET /rest/orders/:id`                                      | `GET /v2/bookings/:id`              | `:id` is now a **booking id**          |
| `GET /rest/orders/:id/refund-rules`                         | `GET /v2/bookings/:id/refund-rules` | Booking-level ownership check          |
| `PATCH /rest/orders` (`paymentStatus: Paid`)                | `POST /v2/bookings/:id/pay`         | Booking-scoped; no `status` field      |
| `PATCH /rest/orders` (`paymentStatus: Booked`)              | `POST /v2/bookings/:id/book`        | Booking-scoped                         |
| `PATCH /rest/orders` (`status: Canceled`)                   | `POST /v2/bookings/:id/cancel`      | Booking-scoped                         |
| `PATCH /rest/orders` (`Confirmed`/`NotConfirmed`/`Prepaid`) | **not migrated**                    | Carrier lifecycle — use the backoffice |

## Authentication

The auth mechanism is the same: send your API key in the `x-api-key` header on every request.

```bash theme={null}
curl https://api.prexsell.com/v2/bookings \
  -H "x-api-key: <your-api-key>"
```

One change: a missing or unrecognized key now returns **401** (was 403 with a 401-semantics body in the legacy tree). Update any code that branches on the exact status code.

## Response envelope

All v2 responses wrap the resource in a `{ data: {...} }` envelope:

```json theme={null}
{ "data": { "booking": { ... } } }
```

List endpoints include `total` for the unpaginated count:

```json theme={null}
{ "data": { "bookings": [...], "total": 42 } }
```

Errors use the standard shape across all v2 endpoints:

```json theme={null}
{
  "errors": [
    {
      "message": "Offer expired",
      "errorCode": "OFFER_EXPIRED"
    }
  ]
}
```

## Status codes

| Status | When v2 returns it                                             |
| ------ | -------------------------------------------------------------- |
| `201`  | `POST /v2/bookings` — booking created                          |
| `400`  | Validation error (bad date, expired offer token, etc.)         |
| `401`  | Missing or invalid API key                                     |
| `404`  | Booking not found, or belongs to a different partner           |
| `409`  | Conflict — see [Actions and conflicts](#actions-and-conflicts) |
| `500`  | Unexpected server error                                        |

404 is returned for both unknown ids and ids that belong to another partner — the API does not reveal whether a foreign resource exists.

## Offers

`GET /v2/offers` replaces `GET /rest/offers`. The search parameters are mostly the same with these changes:

* `passengersQty` is renamed to `passengers` (integer ≥ 1).
* `departureDate` is validated as an ISO 8601 date string; a garbage value returns 400 instead of 500.
* The response shape changes from `{ data: { source, results } }` to `{ data: { offers, source } }`.
* Each offer now carries an `expiresAt` timestamp (ISO 8601 UTC). This is the exact expiry of the 20-minute booking token — a per-offer value decoded from the token itself. Book before this time; an expired token returns **400 + `OFFER_EXPIRED`**.

## The booking pivot

The most significant change: **the addressing unit is now the booking, not the individual ticket (order)**.

When you call `POST /v2/bookings`, one booking is created for all the passengers in that single request. Every ticket carries both its own `id` and a `bookingId`. Use the `bookingId` as the handle for reads and actions going forward.

### Booking id stability

Booking ids reflect **current state**, not the creation snapshot. If a passenger's identity fields are edited internally (date change, reassignment, route transfer), the affected ticket may be moved to a new booking id — one you have never seen.

Consequences:

* Always use the `bookingId` field on each returned ticket as the authoritative crosswalk. If a previously valid booking id returns 404, re-resolve via the ticket's current `bookingId`.
* `GET /v2/bookings` and `GET /v2/bookings/:id` always reflect the current grouping.
* An action's `orderIds` subset (see below) may return 404 if an order moved to a different booking since you last fetched it. Re-fetch the booking first.

## Creating a booking

```bash theme={null}
POST /v2/bookings
Content-Type: application/json
x-api-key: <your-api-key>

{
  "offerId": "<token>",
  "currency": "UAH",
  "passengers": [
    {
      "firstName": "Anna",
      "lastName": "Kovalenko",
      "phones": [{ "phone": "+380991234567" }],
      "email": "anna@example.com"
    }
  ]
}
```

Changes from `POST /rest/orders`:

* Returns `201` with `{ data: { booking } }` — a booking object containing all tickets — instead of `200` with a bare ticket array.
* The body is **strictly validated** — unknown fields are rejected with 400. Remove any undeclared legacy fields (`price`, `seats`, `paymentRecipient`, `invoice`, `sessionId`, `octobusRaceId`, `blaBlaCarRaceId`) from your request body.
* `offerId` is now a top-level body field (not per-passenger). All passengers in a booking share the same offer token. v2 no longer accepts a `source` field — every v2 booking is created against the single top-level PrexSell `offerId`; external-source routing is not available on v2.
* The booking response includes `paymentRequiredBy` — see [Time-limited fields](#time-limited-fields).

## Booking status

The booking's `status` field is derived from the statuses of its member tickets:

* `Canceled` — all tickets are Refund or Canceled.
* `Upcoming` / `Boarding` / `InTransit` / `Completed` — derived from `departureDate` and 8h/24h windows when any ticket is still active.

`Booking.departureDate` is the **UTC onboarding instant** for the trip (timezone-aware, sourced from ADR-0008). It is not the same value as the per-ticket local `departureDate`. Use the booking-level field for booking-aggregate logic; use the ticket-level field when you need the local departure time.

## Reading bookings

`GET /v2/bookings` paginates bookings, newest first (default `take: 50`, maximum `take: 50`). The optional `departureDate` query param (ISO 8601 date, `YYYY-MM-DD`) filters to bookings whose **UTC onboarding instant** (`booking.departureDate`) falls on that calendar day; omit it to list all your bookings. The legacy `createdAt` filter is **not** carried over. Each booking carries only your own tickets. `total` counts bookings, not tickets.

`GET /v2/bookings/:id` returns a single booking by its id with all partner-owned tickets nested under `booking.tickets`.

## Refund rules

`GET /v2/bookings/:id/refund-rules` performs a booking-level ownership check first (404 if not yours), then returns per-order refund rules:

```json theme={null}
{
  "data": {
    "refundRules": [
      {
        "orderId": "<order-id>",
        "rules": [{ "id": "...", "hours": 24, "amount": 50 }]
      }
    ]
  }
}
```

## Actions and conflicts

The three PATCH branches are replaced by per-action POST endpoints that are scoped to a booking id:

* `POST /v2/bookings/:id/pay`
* `POST /v2/bookings/:id/book`
* `POST /v2/bookings/:id/cancel`

All three accept an optional `orderIds` array to target a subset of the booking's tickets. When omitted, the action applies to **all current members** of the booking. If you supply `orderIds`, every id must belong to the booking — unknown or foreign ids return 404.

Cross-booking batches (a single legacy `PATCH /rest/orders` call with ids from multiple creation pools) must become N separate booking-scoped calls, one per booking id.

Each action returns **409** with a list of offending ids when the targeted tickets are in an incompatible state:

| Endpoint                       | 409 conditions                                                           |
| ------------------------------ | ------------------------------------------------------------------------ |
| `POST /v2/bookings/:id/pay`    | Any targeted ticket is already `Paid`, `Canceled`, or `Refund`           |
| `POST /v2/bookings/:id/book`   | Any targeted ticket is already `Booked`, `Paid`, `Canceled`, or `Refund` |
| `POST /v2/bookings/:id/cancel` | Any targeted ticket is already `Canceled` or `Refund`                    |

The conflict check is best-effort (not transactional). Under concurrent duplicate requests, both may pass the check and execute. Use the subset-retry path (supply explicit `orderIds` for the non-conflicting tickets) to recover from a partial 409.

### Partial failure semantics

Pay and book actions process tickets concurrently. If one ticket fails mid-batch, already-processed siblings remain committed and their side effects (provider confirmations, emails, Viber notifications) have fired. The error response reflects the failure; successfully processed tickets are not rolled back.

On partial failure, retry with the explicit `orderIds` subset that still needs processing. Do not retry the full set — the already-`Paid` or already-`Booked` tickets will 409.

### Book action: one history row

The legacy `PATCH /rest/orders` Booked branch wrote two history rows per ticket. `POST /v2/bookings/:id/book` writes one. If your tooling parses order history counts, update it accordingly.

## Time-limited fields

### `expiresAt` on offers

Each offer carries an `expiresAt` ISO 8601 timestamp. Attempting to book an expired token returns:

```json theme={null}
{
  "errors": [{ "message": "Offer expired", "errorCode": "OFFER_EXPIRED" }]
}
```

A malformed (non-JWT) token returns 400 with `errorCode: "VALIDATION_ERROR"`.

### `paymentRequiredBy` on bookings

A booking with unpaid tickets includes `paymentRequiredBy` — the deadline to pay derived from the earliest unpaid ticket's creation time plus the 20-minute payment window. After this deadline, unpaid tickets flip to `Uncompleted`. Paying after the flip still revives the tickets; `paymentRequiredBy` is informational, not a hard cutoff.

Once all tickets are paid, booked, canceled, or uncompleted, `paymentRequiredBy` is `null`.

### `refundAmount` on canceled tickets

`POST /v2/bookings/:id/cancel` returns the refreshed booking. Each canceled ticket carries its `refundAmount` directly in the response — you do not need to re-query. Cancellation is always allowed — there is no timing-based block. A **paid PrexSell ticket** canceled within 20 minutes of its creation is refunded in full regardless of the carrier's refund rules; after that grace window, the refund follows the refund-rule tiers. Unpaid or merely booked tickets, and paid tickets from external operators, cancel at any time (they become `Canceled`, with no refund).

## Response shape changes

The following fields changed between `/rest` and `/v2`:

* **Offer response:** `{ data: { source, results } }` → `{ data: { offers, source } }`. The array key is `offers`, not `results`.
* **Create response:** bare ticket array → `{ data: { booking } }` with tickets nested under `booking.tickets`.
* **Slice-level `seat`:** now declared and returned in each slice. The legacy phantom top-level `seat` field (declared but never sent) is removed.
* **Prices:** `amount` is always a number (explicit `Decimal → number` conversion). The legacy behavior was implicit; no value change expected.

The following fields from the offer or ticket response may be removed during your partner cutover. Confirm with PREXSELL before migration:

* `company.ceo`
* `company.socials`
* `company.partnerCompany`

## Deprecation timeline

The `/rest/offers` and `/rest/orders` endpoints serve `Deprecation`, `Sunset`, and `Link` headers from the moment v2 goes live. The sunset window is **at least 90 days AND** requires per-partner Moesif traffic to reach zero — it is usage-gated, not calendar-gated. Once the window closes, both endpoints return **410 Gone** with a pointer to this guide. Plan your migration accordingly and coordinate with PREXSELL support.
