Purchase orders
A purchase order is one commercial transaction on a trip, placed on one channel at one purchase moment. Each holds order lines with four status dimensions.
| Operation | Endpoint |
|---|---|
| Find purchase orders by quote | GET /order-orchestration/purchase-orders |
| Read a purchase order | GET /order-orchestration/purchase-orders/{poId} |
| Apply an order change | POST /order-orchestration/purchase-orders/{poId}/changes |
| Change a purchase order's commercial status | POST /order-orchestration/purchase-orders/{poId}/transition |
| Hand frozen conditions over as a purchase order | POST /order-orchestration/trips/{tripId}/handover |
| Create a purchase order on a trip | POST /order-orchestration/trips/{tripId}/purchase-orders |
Find purchase orders by quote
/order-orchestration/purchase-ordersParameters
| Name | In | Required | Notes |
|---|---|---|---|
quoteRef | query | yes | The quote reference the orders were converted from. |
Response 200
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request failed validation. The message names the first failing field; oms-ops and group surfaces also return every issue in details. |
| 401 | unauthenticated | The bearer token is missing, invalid, expired or minted for another purpose. |
| 403 | scope_forbidden | The token is scoped to an agency or an access scope; this surface requires an unscoped tenant token. |
| 404 | not_enabled | Order orchestration is not enabled for the tenant. |
| 500 | internal | Unexpected failure. Retry with the same Idempotency-Key. |
Read a purchase order
/order-orchestration/purchase-orders/{poId}Parameters
| Name | In | Required | Notes |
|---|---|---|---|
poId | path | yes |
Response 200
/order-orchestration/purchase-orders/PO-75EB654AFFACRead a purchase ordercurl -X GET "$BASE/order-orchestration/purchase-orders/PO-75EB654AFFAC" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Request-Id: req-0020"
HTTP/1.1 200
X-Request-Id: req-0020
{
"poId": "PO-75EB654AFFAC",
"type": "standard",
"lines": [
{
"lineId": "L-490ECF37928C",
"status": {
"billing": "not_billed",
"payment": "not_due",
"commercial": "booked",
"fulfilment": "confirmed"
},
"offerId": "ALL-INCLUSIVE-2027",
"currency": "EUR",
"quantity": 1,
"ownership": {
"ruleId": "anchor-reservation",
"process": "create"
},
"amountMinor": 238000,
"description": "7-night Western Mediterranean, balcony cabin, 2 guests",
"explanation": [
{
"flag": "public",
"mode": "select",
"basis": "passenger",
"input": 0,
"layer": "base",
"reason": {
"key": "layer.base",
"params": {}
},
"ruleId": "base-balcony",
"source": {
"kind": "store",
"version": 14,
"timestamp": "2026-10-30T00:00:00Z"
},
"contribution": 264400,
"runningTotal": 264400
},
{
"flag": "public",
"mode": "adjust",
"basis": "booking",
"input": 264400,
"layer": "promotion",
"reason": {
"key": "layer.promotion",
"params": {}
},
"ruleId": "EARLY-BOOKING-10",
"source": {
"kind": "store",
"version": 14,
"timestamp": "2026-10-30T00:00:00Z"
},
"contribution": -26400,
"runningTotal": 238000
}
],
"externalRef": "RSV-1306C259-000001",
"productCode": "MED7-2027-04-12",
"productType": "cruise",
"passengerIds": [
"PAX-01",
"PAX-02"
],
"promotionIds": [
"EARLY-BOOKING-10"
],
"quoteLineRef": "q-line-1",
"sourceSystem": "reservation",
"sellingConditions": {
"refundable": true,
"depositPercent": 20
}
},
{
"lineId": "L-9F197F515BFD",
"status": {
"billing": "not_billed",
"payment": "not_due",
"commercial": "booked",
"fulfilment": "requested"
},
"offerId": "ALL-INCLUSIVE-2027",
"currency": "EUR",
"quantity": 2,
"ownership": {
"ruleId": "beverage-phase1",
"process": "create"
},
"amountMinor": 30800,
"description": "Classic drinks package, 7 nights",
"explanation": [],
"productCode": "DRINKS-CLASSIC-7N",
"productType": "beverage",
"passengerIds": [
"PAX-01",
"PAX-02"
],
"promotionIds": [],
"quoteLineRef": "q-line-2",
"sourceSystem": "onboard"
}
],
"payer": {
"ref": "CUST-20419",
"kind": "customer",
"displayName": "Giulia Rossi"
},
"terms": {},
"booker": {
"ref": "CUST-20419",
"kind": "customer",
"displayName": "Giulia Rossi"
},
"market": "IT",
"moment": "booking",
"origin": {
"channel": "web",
"actorRef": "CUST-20419"
},
"status": {
"billing": "not_billed",
"payment": "not_due",
"commercial": "booked",
"fulfilment": "requested"
},
"tripId": "TRP-0D3CE7B16625",
"channel": "web",
"version": 4,
"currency": "EUR",
"sequence": 1,
"createdAt": "2026-11-02T09:06:51.000Z",
"revisions": [],
"updatedAt": "2026-11-02T09:07:28.000Z",
"totalMinor": 268800,
"attribution": {},
"fullPayment": false,
"finalisation": {
"at": "2026-11-02T09:06:51.000Z",
"channel": "web"
},
"quotationRef": "QT-ED24E0954262",
"partiesLocked": true,
"frozenConditions": {
"document": {
"lines": [
{
"lineRef": "q-line-1",
"offerId": "ALL-INCLUSIVE-2027",
"quantity": 1,
"amountMinor": 238000,
"description": "7-night Western Mediterranean, balcony cabin, 2 guests",
"explanation": [
{
"flag": "public",
"mode": "select",
"basis": "passenger",
"input": 0,
"layer": "base",
"reason": {
"key": "layer.base",
"params": {}
},
"ruleId": "base-balcony",
"source": {
"kind": "store",
"version": 14,
"timestamp": "2026-10-30T00:00:00Z"
},
"contribution": 264400,
"runningTotal": 264400
},
{
"flag": "public",
"mode": "adjust",
"basis": "booking",
"input": 264400,
"layer": "promotion",
"reason": {
"key": "layer.promotion",
"params": {}
},
"ruleId": "EARLY-BOOKING-10",
"source": {
"kind": "store",
"version": 14,
"timestamp": "2026-10-30T00:00:00Z"
},
"contribution": -26400,
"runningTotal": 238000
}
],
"productCode": "MED7-2027-04-12",
"productType": "cruise",
"passengerIds": [
"PAX-01",
"PAX-02"
],
"promotionIds": [
"EARLY-BOOKING-10"
],
"sellingConditions": {
"refundable": true,
"depositPercent": 20
}
},
{
"lineRef": "q-line-2",
"offerId": "ALL-INCLUSIVE-2027",
"quantity": 2,
"amountMinor": 30800,
"description": "Classic drinks package, 7 nights",
"explanation": [],
"productCode": "DRINKS-CLASSIC-7N",
"productType": "beverage",
"passengerIds": [
"PAX-01",
"PAX-02"
],
"promotionIds": []
}
],
"market": "IT",
"channel": "web",
"currency": "EUR",
"frozenAt": "2026-11-02T09:04:00Z",
"quoteRef": "Q-2027-58213",
"promotions": [
{
"version": 3,
"promotionId": "EARLY-BOOKING-10",
"postBookingPolicy": "keep"
}
],
"totalMinor": 268800,
"customerRef": "CUST-20419",
"quoteVersion": 2,
"storeVersion": 14,
"configVersion": 9
},
"frozenAt": "2026-11-02T09:04:00Z",
"quoteRef": "Q-2027-58213",
"quoteVersion": 2,
"storeVersion": 14,
"configVersion": 9
}
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request failed validation. The message names the first failing field; oms-ops and group surfaces also return every issue in details. |
| 401 | unauthenticated | The bearer token is missing, invalid, expired or minted for another purpose. |
| 403 | scope_forbidden | The token is scoped to an agency or an access scope; this surface requires an unscoped tenant token. |
| 404 | not_found | The trip, purchase order, quotation or configuration item does not exist. |
| 404 | not_enabled | Order orchestration is not enabled for the tenant. |
| 500 | internal | Unexpected failure. Retry with the same Idempotency-Key. |
Apply an order change
/order-orchestration/purchase-orders/{poId}/changesApplies a repricing proposal to the order as a revision, either passed inline or by the id of a confirmed proposal from the repricing service. Modify requests go to the owning systems. Send If-Match to make the write conditional.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
poId | path | yes |
Headers: Idempotency-Key, If-Match, plus X-Request-Id.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| Shape 1 | |||
proposal | object | yes | |
proposal.baselineQuoteRef | string | yes | min 1 chars |
proposal.changeType | string | yes | min 1 chars |
proposal.lines | array of object | yes | at least 1 |
proposal.lines[].action | enum: keep, recalculate, lose, add, remove | yes | |
proposal.lines[].explanation | array of object | ||
proposal.lines[].lineRef | string | yes | min 1 chars |
proposal.lines[].newAmountMinor | one of several shapes | yes | |
proposal.lines[].newLine | object | ||
proposal.lines[].oldAmountMinor | one of several shapes | yes | |
proposal.lines[].promotionOutcomes | array of object | ||
proposal.newTotalMinor | integer | yes | |
proposal.oldTotalMinor | integer | yes | |
proposal.proposalId | string | yes | min 1 chars |
proposal.reason | string | max 500 chars | |
actorRef | string | yes | min 1 chars |
| Shape 2 | |||
repricingProposalId | string | yes | min 1 chars |
actorRef | string | yes | min 1 chars |
Response 200
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request failed validation. The message names the first failing field; oms-ops and group surfaces also return every issue in details. |
| 401 | unauthenticated | The bearer token is missing, invalid, expired or minted for another purpose. |
| 403 | scope_forbidden | The token is scoped to an agency or an access scope; this surface requires an unscoped tenant token. |
| 404 | not_found | The trip, purchase order, quotation or configuration item does not exist. |
| 404 | repricing_not_enabled | Repricing is not enabled for the tenant. |
| 404 | not_enabled | Order orchestration is not enabled for the tenant. |
| 409 | not_changeable | The purchase order is not booked or optioned. |
| 409 | baseline_mismatch | The proposal baseline does not match the purchase order's frozen conditions. |
| 409 | stale_proposal | The repricing proposal was computed against an older purchase order. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Retry after the Retry-After interval. |
| 412 | version_mismatch | The If-Match version is not the current version. |
| 422 | invalid_proposal | The order change proposal is invalid. |
| 422 | unknown_line | A line named in the request is not on the order. |
| 422 | total_mismatch | The proposal totals do not match its lines. |
| 422 | idempotency_key_reused | The Idempotency-Key was used for a different request. |
| 500 | internal | Unexpected failure. Retry with the same Idempotency-Key. |
Change a purchase order's commercial status
/order-orchestration/purchase-orders/{poId}/transitionAllowed transitions: quotation or option to booked or cancelled; booked to cancelled, frozen or closed; frozen to booked, cancelled or closed. Send If-Match with the version you read to make the write conditional.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
poId | path | yes |
Headers: Idempotency-Key, If-Match, plus X-Request-Id.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
to | enum: cancelled, frozen, closed, booked | yes | |
reason | string | yes | min 1 chars |
Response 200
/order-orchestration/purchase-orders/PO-8B18D9E89570/transitionA write conditional on a stale versioncurl -X POST "$BASE/order-orchestration/purchase-orders/PO-8B18D9E89570/transition" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Request-Id: req-0035" \
-H "If-Match: "3"" \
-H "Content-Type: application/json" \
-d '{
"to": "frozen",
"reason": "Hold for payment review"
}'
HTTP/1.1 412
X-Request-Id: req-0035
{
"error": "version_mismatch",
"message": "purchase order PO-8B18D9E89570 is at version 4, not 3",
"code": "version_mismatch",
"requestId": "req-0035"
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request failed validation. The message names the first failing field; oms-ops and group surfaces also return every issue in details. |
| 400 | invalid_if_match | The If-Match header is not a version number. |
| 401 | unauthenticated | The bearer token is missing, invalid, expired or minted for another purpose. |
| 403 | scope_forbidden | The token is scoped to an agency or an access scope; this surface requires an unscoped tenant token. |
| 404 | not_found | The trip, purchase order, quotation or configuration item does not exist. |
| 404 | not_enabled | Order orchestration is not enabled for the tenant. |
| 409 | invalid_transition | The status transition is not allowed from the current status. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Retry after the Retry-After interval. |
| 412 | version_mismatch | The If-Match version is not the current version. |
| 422 | idempotency_key_reused | The Idempotency-Key was used for a different request. |
| 500 | internal | Unexpected failure. Retry with the same Idempotency-Key. |
Hand frozen conditions over as a purchase order
/order-orchestration/trips/{tripId}/handoverCreates a booked purchase order on the trip directly from frozen conditions, without a tracked quotation. Idempotent per quote reference and version.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
tripId | path | yes |
Headers: Idempotency-Key, plus X-Request-Id.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
frozen | object | yes | |
frozen.channel | string | yes | min 1 chars |
frozen.configVersion | one of several shapes | ||
frozen.currency | string | yes | pattern ^[A-Z]{3}$ |
frozen.customerRef | string | max 200 chars | |
frozen.frozenAt | string (date-time) | yes | |
frozen.lines | array of object | yes | at least 1 |
frozen.lines[].amountMinor | integer | yes | |
frozen.lines[].beneficiaryRef | string | max 200 chars | |
frozen.lines[].description | string | max 500 chars | |
frozen.lines[].explanation | array of object | ||
frozen.lines[].lineRef | string | yes | min 1 chars |
frozen.lines[].offerId | string | max 200 chars | |
frozen.lines[].passengerIds | array of string | ||
frozen.lines[].productCode | string | yes | min 1 chars |
frozen.lines[].productType | string | yes | min 1 chars |
frozen.lines[].promotionIds | array of string | ||
frozen.lines[].quantity | integer | > 0; default 1 | |
frozen.lines[].sellingConditions | object | ||
frozen.lines[].serviceDate | string (date-time) | ||
frozen.market | string | max 60 chars | |
frozen.promotions | array of object | ||
frozen.promotions[].postBookingPolicy | enum: keep, recalculate, lose | yes | |
frozen.promotions[].promotionId | string | yes | min 1 chars |
frozen.promotions[].version | integer | ≥ 0 | |
frozen.quoteRef | string | yes | min 1 chars |
frozen.quoteVersion | integer | yes | ≥ 0 |
frozen.storeVersion | one of several shapes | ||
frozen.totalMinor | integer | yes | |
moment | string | yes | min 1 chars |
channel | string | ||
booker | object | ||
booker.displayName | string | max 200 chars | |
booker.kind | enum: customer, agency, agent, company | yes | |
booker.ref | string | yes | 1–200 chars |
payer | object | ||
payer.displayName | string | max 200 chars | |
payer.kind | enum: customer, agency, agent, company | yes | |
payer.ref | string | yes | 1–200 chars |
type | string | ||
fullPayment | boolean | ||
actorRef | string |
Response 201
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request failed validation. The message names the first failing field; oms-ops and group surfaces also return every issue in details. |
| 401 | unauthenticated | The bearer token is missing, invalid, expired or minted for another purpose. |
| 403 | scope_forbidden | The token is scoped to an agency or an access scope; this surface requires an unscoped tenant token. |
| 404 | not_found | The trip, purchase order, quotation or configuration item does not exist. |
| 404 | not_enabled | Order orchestration is not enabled for the tenant. |
| 409 | trip_closed | The trip is closed. |
| 409 | frozen_conditions_mismatch | The frozen money posted differs from the accepted quote (tenants with verify_frozen_conditions). |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Retry after the Retry-After interval. |
| 422 | unknown_moment | The purchase moment is not in the orchestration configuration. |
| 422 | idempotency_key_reused | The Idempotency-Key was used for a different request. |
| 500 | internal | Unexpected failure. Retry with the same Idempotency-Key. |
| 503 | handover_source_unavailable | The quote store could not be reached to verify frozen conditions. |
Create a purchase order on a trip
/order-orchestration/trips/{tripId}/purchase-ordersSells one or more lines into an existing trip on a given channel and purchase moment, for example an add-on bought in the mobile app after booking. Each line is routed to its owning system by the execution-ownership rules.
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
tripId | path | yes |
Headers: Idempotency-Key, plus X-Request-Id.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
actorRef | string | max 200 chars | |
attribution | object | ||
attribution.agencyRef | string | max 200 chars | |
attribution.campaignRef | string | max 200 chars | |
booker | object | ||
booker.displayName | string | max 200 chars | |
booker.kind | enum: customer, agency, agent, company | yes | |
booker.ref | string | yes | 1–200 chars |
channel | string | yes | min 1 chars |
commercialStatus | enum: quotation, option, booked | default "booked" | |
currency | string | yes | pattern ^[A-Z]{3}$ |
fullPayment | boolean | default false | |
idempotencyKey | string | 1–200 chars | |
journeysOrderRef | string | ||
lines | array of object | yes | at least 1 |
lines[].amountMinor | integer | yes | |
lines[].beneficiaryRef | string | max 200 chars | |
lines[].description | string | max 500 chars | |
lines[].explanation | array of object | ||
lines[].offerId | string | max 200 chars | |
lines[].passengerIds | array of string | ||
lines[].productCode | string | yes | min 1 chars |
lines[].productType | string | yes | min 1 chars |
lines[].promotionIds | array of string | ||
lines[].quantity | integer | > 0; default 1 | |
lines[].quoteLineRef | string | ||
lines[].sellingConditions | object | ||
lines[].sellingConditions.balanceDueAt | string (date-time) | ||
lines[].sellingConditions.cancellationPolicyId | string | ||
lines[].sellingConditions.depositPercent | number | ≥ 0; ≤ 100 | |
lines[].sellingConditions.refundable | boolean | ||
lines[].serviceDate | string (date-time) | ||
market | string | max 60 chars | |
moment | string | yes | min 1 chars |
payer | object | ||
payer.displayName | string | max 200 chars | |
payer.kind | enum: customer, agency, agent, company | yes | |
payer.ref | string | yes | 1–200 chars |
terms | object | ||
terms.balanceDueAt | string (date-time) | ||
terms.cancellationPolicyId | string | ||
terms.depositPercent | number | ≥ 0; ≤ 100 | |
terms.refundable | boolean | ||
touchpoint | string | max 100 chars | |
type | string | 1–60 chars; default "standard" |
Schema: apps/journeys/src/order-orchestration/contracts.ts (CreatePurchaseOrderInputSchema)
Response 201
/order-orchestration/trips/TRP-B2BD5A9771E4/purchase-ordersAdd shore excursions from the mobile app as a second purchase ordercurl -X POST "$BASE/order-orchestration/trips/TRP-B2BD5A9771E4/purchase-orders" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Request-Id: req-0006" \
-H "Idempotency-Key: 43563ae7-1d12-4b6d-a226-3a66f7d48c7f" \
-H "Content-Type: application/json" \
-d '{
"channel": "app",
"moment": "pre_trip",
"currency": "EUR",
"booker": {
"kind": "customer",
"ref": "CUST-20419",
"displayName": "Giulia Rossi"
},
"payer": {
"kind": "customer",
"ref": "CUST-20419",
"displayName": "Giulia Rossi"
},
"lines": [
{
"productType": "excursion",
"productCode": "EXC-NAP-POMPEII",
"description": "Pompeii and Vesuvius, Naples call",
"quantity": 2,
"amountMinor": 17800,
"passengerIds": [
"PAX-01",
"PAX-02"
],
"serviceDate": "2027-04-14T08:30:00Z"
},
{
"productType": "excursion",
"productCode": "EXC-BCN-SAGRADA",
"description": "Sagrada Familia skip-the-line, Barcelona call",
"quantity": 1,
"amountMinor": 6900,
"passengerIds": [
"PAX-02"
],
"serviceDate": "2027-04-17T09:00:00Z"
}
]
}'
HTTP/1.1 201
X-Request-Id: req-0006
{
"poId": "PO-71B4D4926EDD",
"type": "standard",
"lines": [
{
"lineId": "L-FDAF4DA2CF1A",
"status": {
"billing": "not_billed",
"payment": "not_due",
"commercial": "booked",
"fulfilment": "requested"
},
"currency": "EUR",
"quantity": 2,
"ownership": {
"ruleId": "excursions",
"process": "create"
},
"amountMinor": 17800,
"description": "Pompeii and Vesuvius, Naples call",
"productCode": "EXC-NAP-POMPEII",
"productType": "excursion",
"serviceDate": "2027-04-14T08:30:00Z",
"passengerIds": [
"PAX-01",
"PAX-02"
],
"promotionIds": [],
"sourceSystem": "excursions"
},
{
"lineId": "L-93A9D925FCE1",
"status": {
"billing": "not_billed",
"payment": "not_due",
"commercial": "booked",
"fulfilment": "requested"
},
"currency": "EUR",
"quantity": 1,
"ownership": {
"ruleId": "excursions",
"process": "create"
},
"amountMinor": 6900,
"description": "Sagrada Familia skip-the-line, Barcelona call",
"productCode": "EXC-BCN-SAGRADA",
"productType": "excursion",
"serviceDate": "2027-04-17T09:00:00Z",
"passengerIds": [
"PAX-02"
],
"promotionIds": [],
"sourceSystem": "excursions"
}
],
"payer": {
"ref": "CUST-20419",
"kind": "customer",
"displayName": "Giulia Rossi"
},
"terms": {},
"booker": {
"ref": "CUST-20419",
"kind": "customer",
"displayName": "Giulia Rossi"
},
"moment": "pre_trip",
"origin": {
"channel": "app"
},
"status": {
"billing": "not_billed",
"payment": "not_due",
"commercial": "booked",
"fulfilment": "requested"
},
"tripId": "TRP-B2BD5A9771E4",
"channel": "app",
"version": 2,
"currency": "EUR",
"sequence": 2,
"createdAt": "2026-11-02T09:08:42.000Z",
"revisions": [],
"updatedAt": "2026-11-02T09:08:42.000Z",
"totalMinor": 24700,
"attribution": {},
"fullPayment": false,
"finalisation": {
"at": "2026-11-02T09:08:42.000Z",
"channel": "app"
},
"partiesLocked": true
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request failed validation. The message names the first failing field; oms-ops and group surfaces also return every issue in details. |
| 401 | unauthenticated | The bearer token is missing, invalid, expired or minted for another purpose. |
| 403 | scope_forbidden | The token is scoped to an agency or an access scope; this surface requires an unscoped tenant token. |
| 404 | not_found | The trip, purchase order, quotation or configuration item does not exist. |
| 404 | not_enabled | Order orchestration is not enabled for the tenant. |
| 409 | trip_closed | The trip is closed. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Retry after the Retry-After interval. |
| 422 | unknown_channel | The channel is not in the orchestration configuration. |
| 422 | unknown_moment | The purchase moment is not in the orchestration configuration. |
| 422 | unknown_market | The market is not in the orchestration configuration. |
| 422 | second_anchor | The trip already has an anchor product. |
| 422 | not_available_on_channel | The product type may not be sold on this channel. |
| 422 | idempotency_key_reused | The Idempotency-Key was used for a different request. |
| 500 | internal | Unexpected failure. Retry with the same Idempotency-Key. |