Skip to main content
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.
Deprecated endpoints respond with Deprecation, Sunset, and Link headers pointing here. Monitor those headers in your client to track the sunset date.

Path mapping

Legacy endpointv2 endpointNotes
GET /rest/offersGET /v2/offersSee Offers below
POST /rest/ordersPOST /v2/bookingsReturns a booking, not a ticket array
GET /rest/ordersGET /v2/bookingsPaginates bookings, not bare tickets
GET /rest/orders/:idGET /v2/bookings/:id:id is now a booking id
GET /rest/orders/:id/refund-rulesGET /v2/bookings/:id/refund-rulesBooking-level ownership check
PATCH /rest/orders (paymentStatus: Paid)POST /v2/bookings/:id/payBooking-scoped; no status field
PATCH /rest/orders (paymentStatus: Booked)POST /v2/bookings/:id/bookBooking-scoped
PATCH /rest/orders (status: Canceled)POST /v2/bookings/:id/cancelBooking-scoped
PATCH /rest/orders (Confirmed/NotConfirmed/Prepaid)not migratedCarrier lifecycle — use the backoffice

Authentication

The auth mechanism is the same: send your API key in the x-api-key header on every request.
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:
{ "data": { "booking": { ... } } }
List endpoints include total for the unpaginated count:
{ "data": { "bookings": [...], "total": 42 } }
Errors use the standard shape across all v2 endpoints:
{
  "errors": [
    {
      "message": "Offer expired",
      "errorCode": "OFFER_EXPIRED"
    }
  ]
}

Status codes

StatusWhen v2 returns it
201POST /v2/bookings — booking created
400Validation error (bad date, expired offer token, etc.)
401Missing or invalid API key
404Booking not found, or belongs to a different partner
409Conflict — see Actions and conflicts
500Unexpected 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

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.

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:
{
  "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:
Endpoint409 conditions
POST /v2/bookings/:id/payAny targeted ticket is already Paid, Canceled, or Refund
POST /v2/bookings/:id/bookAny targeted ticket is already Booked, Paid, Canceled, or Refund
POST /v2/bookings/:id/cancelAny 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:
{
  "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.