Cancellations and refunds
How to read cancellation policies, quote the fee before you cancel, cancel without double calls, and return money to the guest as a separate step.
How cancellation policies read
cancellationPolicies[] is a list of penalty steps. Each step says "from date on, cancelling costs amount". A step with amount 0 means free until the next step's date, and the last step is usually the full price. The steps are cumulative: each amount is the whole penalty from its date, not an extra charge on top of the previous step.
{
"price": { "amount": 808.04, "currency": "EUR" },
"cancellationPolicies": [
{ "date": "2026-10-01", "amount": { "amount": 0.0, "currency": "EUR" } },
{ "date": "2026-11-07", "amount": { "amount": 444.46, "currency": "EUR" } },
{ "date": "2026-11-10", "amount": { "amount": 808.04, "currency": "EUR" } }
],
"currentCancellationType": { "deadline": "2026-11-07", "type": "REFUNDABLE" }
}For a stay from 2026-11-10 to 2026-11-14, this reads:
| Cancelled on | Penalty |
|---|---|
| 2026-10-01 to 2026-11-06 | 0.00 EUR, free |
| 2026-11-07 to 2026-11-09 | 444.46 EUR |
| From 2026-11-10 | 808.04 EUR, the full price |
Each step (CancellationPolicyVO) also carries netPrice, nonCommissionableAmount, commissionPriceAmount and policyAmount, which has a microsite and an operator leg.
type Money = { amount: number; currency: string };
type PolicyStep = { date: string; amount: Money };
/** The penalty on `day` (yyyy-MM-dd): the last step whose date is on or before it. */
export function penaltyOn(steps: PolicyStep[], day: string): Money | null {
let fee: Money | null = null;
for (const s of [...steps].sort((a, b) => a.date.localeCompare(b.date))) {
if (s.date <= day) fee = s.amount;
}
return fee; // null: the day is before the first step
}Use this for display only. For a booked service, the cancellation fee endpoint gives the real number.
Which policy to trust
| Where | What you get | Trust it for |
|---|---|---|
| Quote, Quote single | cancellationPolicies[] plus currentCancellationType | Search results and filters only. Informational. |
| Confirm, Prebook | cancellationPolicies[] | The terms the guest agrees to. Confirm is the first authoritative step. |
Booked service in getBookings | hotelservice[].cancelPolicy[] | After booking. There is no currentCancellationType here. |
Booked trip in getBookings | cancellationPolicies[] at booking level | Dates to show people (see below). |
currentCancellationTypehastypeanddeadline.typeisREFUNDABLE(free untildeadline),PARTIALLY_REFUNDABLE,NON_REFUNDABLEorUNKNOWN. It only appears in quote responses.- If Confirm or Prebook returns a
CANCELLATION_POLICIES_CHANGEwarning, show the new terms and get the guest's consent again. A refundable rate may have become non-refundable.
Show the booking-level date
The service-level cancelPolicy[].date is serialised in UTC. The booking-level cancellationPolicies[].date is in the operator's local time, which is what agents see in the back office. Near midnight the two differ by a day: for example, the service shows 2026-11-06 while the booking level shows 2026-11-07.
- Show the booking-level date to people.
- Suppliers apply deadlines in the hotel's local time. Tell guests they can cancel free "before
<date>", not "until<date>".
Quote the fee, then cancel
Two calls on the same path look alike and do very different things.
| Call | Does | Safe to repeat |
|---|---|---|
GET …/cancellation-fee | Returns what cancelling today would cost, as cancellationFee (amount, currency). Changes nothing. | Yes |
DELETE /booking/{bookingReference}/accommodations/{accommodationBookingReference} | Cancels at the supplier and returns the booking with status: CANCELED at both levels. | No |
const BASE = process.env.NAVA_BASE_URL!;
export async function cancelHotel(
token: string,
ref: string, // bookingReference, for example TST-1464
accRef: string, // accommodation.bookingReference, for example TST-1464-0
guestAccepts: (fee: Money) => Promise<boolean>,
) {
const path = `/booking/${encodeURIComponent(ref)}/accommodations/${encodeURIComponent(accRef)}`;
// 1. Read-only: what does cancelling today cost?
const { cancellationFee } = await call(token, 'GET', `${path}/cancellation-fee`);
if (!(await guestAccepts(cancellationFee))) return { outcome: 'kept' as const };
// 2. Cancel once. Never retried automatically.
let booking;
try {
booking = await call(token, 'DELETE', path, 120_000);
} catch {
// Timeout or network drop: the outcome is unknown. Read stored state instead of cancelling again.
booking = await call(token, 'GET', path);
}
const cancelled = booking.status === 'CANCELED' && booking.accommodation?.status === 'CANCELED';
if (!cancelled) return { outcome: 'needs_person' as const, booking }; // a person checks, then decides
return { outcome: 'cancelled' as const, booking, fee: cancellationFee }; // next: refund minus the fee
}
async function call(token: string, method: string, path: string, timeoutMs = 60_000) {
const res = await fetch(BASE + path, {
method,
headers: { 'auth-token': token, 'Accept-Encoding': 'gzip' },
signal: AbortSignal.timeout(timeoutMs),
});
const json = await res.json();
if (json?.auditData?.authToken) json.auditData.authToken = '[REDACTED]';
log(method, path, res.status, json?.auditData?.traceId);
if (!res.ok) throw new ApiError(res.status, json);
return json;
}{
"auditData": {
"authToken": "[REDACTED]",
"traceId": "D4AE54C6-…"
},
"cancellationFee": { "amount": 150.0, "currency": "EUR" }
}{
"auditData": {
"authToken": "[REDACTED]",
"traceId": "…"
},
"bookingReference": "TST-1464",
"externalReference": "ORDER-8812",
"status": "CANCELED",
"accommodation": {
"code": "MASTER-1782232",
"bookingReference": "TST-1464-0",
"status": "CANCELED"
}
}Refresh
Refresh (PUT on the same path) re-reads the booking from the supplier, for example to resolve RQ or to spot a cancellation made on the supplier's side.
- It returns the status the supplier reports. If nothing changed at the supplier, the booking stays as it was.
- A supplier that doesn't support refreshing answers
406. Don't retry: read booking detail instead.
The generic cancel endpoint
The generic Booking API can also cancel a hotel service.
serviceTypeisHOTELfor a hotel.serviceIdis probablyhotelservice[].idfromgetBookings, which holds the same value as Book'saccommodation.bookingReference(TST-1464-0). Whether this endpoint takes it is unverified.- These generic endpoints belong to the Booking API. An API user enabled only for the hotel booking flow gets
401 ... not allowed to access hereon them; ask your Nava account manager if you need them. - Body:
cancellationType(required,PROVIDERorMANUAL),emailNotifyCancel,manualCancellationFee,relatedManualCancellationFee. - Response:
status,cancellationType,cancellationDate,providerCancellationDate.
cancellationType | Probably means | |
|---|---|---|
PROVIDER | Cancel at the supplier, like the DELETE above. | |
MANUAL | Record a cancellation handled outside the platform, with manualCancellationFee. |
The spec gives no descriptions for these values, so test both in your test account before you rely on them. The matching fee endpoint, GET /booking/{bookingReference}/{serviceType}/{serviceId}/cancellation-fee, also returns relatedService and supportedCancellationTypes.
See Cancel service and Service cancellation fee.
Refunds are a separate step
Cancelling does not return money to the guest. The refund is its own call, and a refund does not cancel any service.
| Field | Type | Meaning |
|---|---|---|
orderNumber | string, required | The order to refund. |
amount | number | Amount to refund. 0 or omitted refunds the full remaining amount. |
manualRefund | boolean, default false | false refunds through the original payment gateway. true registers a refund handled outside the platform. |
The response carries bookingReference, orderNumber, amount, manualRefund, paymentRefundStatus (NONE, PARTIAL or COMPLETE) and secondPaymentRefundStatus. A REFUND webhook fires when a refund succeeds.
If you took the payment through your own payment provider, refund it there instead.
Playbook: the guest cancels
Quote the fee with GET …/cancellation-fee. The fee is for today, so quote again if the guest decides on another day.
Show "Cancelling now costs X" and get the guest's consent.
Call DELETE once, with a timeout of at least 120 seconds. Never retry it automatically.
Check that status and accommodation.status are both CANCELED. After a timeout, read booking detail instead of cancelling again.
Refund the guest minus the fee: POST /booking/refund if the platform holds the payment, or through your own payment provider.
If the platform processed the refund, a REFUND webhook arrives. Record it.
Playbook: cancelled on the platform side
A supplier or an agent can cancel a booking without you calling the API.
A CANCELED (or MODIFIED) webhook arrives. Answer with a 2xx quickly. The body only carries the reference.
Fetch the trip with getBookings (GET /booking/getBookings/{micrositeId}/{bookingReference}).
Find the hotel in hotelservice[] with status: CANCELED. If a new BOOKED service sits beside it at the same hotel and dates, this is a cancel and rebook, not a lost booking.
Tell the guest and reverse the accounting. Keep the cancelled line at zero rather than deleting it, so the history survives.
Related
Booking statuses
What each status means at service and booking level, what the guest should see, how to handle on-request bookings, and which status changes after booking are normal.
Webhooks
The API calls your endpoint when a booking is created, modified, cancelled or refunded, or gets a client request. The call is only a signal, so fetch the booking to see what changed.