Errors
The shape
Section titled “The shape”An error is a JSON problem (RFC 9457, application/problem+json) with a
code that names it:
curl "https://api.stg.vacationpackagesoman.com/v1/public/products/no-such-trip" \ -H "Tp-Publishable-Key: $TP_KEY"const res = await fetch("https://api.stg.vacationpackagesoman.com/v1/public/products/no-such-trip", { headers: { "Tp-Publishable-Key": process.env.TP_KEY!, },})const answer = await res.json()Answer: 404 Not Found
{ "type": "about:blank", "title": "Not Found", "status": 404, "code": "NotFound", "detail": "No product no-such-trip.", "entity": "product"}| Field | What it is |
|---|---|
status |
The HTTP status, again. |
title |
The status’s name, e.g. Not Found. |
code |
What went wrong, e.g. NotFound. Switch on this. |
detail |
A sentence for developers. Don’t show it to travellers. |
type |
Always about:blank. |
Some codes carry more fields, listed below. Switch on code, never on
detail’s words, and treat a code you don’t know by its status: new codes
may be added.
Every code
Section titled “Every code”| Code | Status | When | What to do |
|---|---|---|---|
Unauthenticated | 401 | The key is missing, wrong or revoked, or the endpoint needs a session the request doesn't carry. | Send the website's key as Tp-Publishable-Key, or a secret key as Authorization: Bearer. Don't retry until it's fixed. |
NotPermitted | 403 | The secret key or the staff member is known, but their role lacks the permission this needs. Carries permission, and reason: missing_permission or no_operator. | Give the key's role the permission named in permission, in the portal. |
OriginNotAllowed | 403 | A browser on a page whose origin the website key's site doesn't list called with that key. A call from a server, with no Origin, never gets this. Carries origin: the page's origin that was refused. | Add the page's origin to the site's allowed origins in the portal (API keys, Website keys), or call from your server. |
NotFound | 404 | There is no such record on this site: a trip, page, option or booking it doesn't sell or can't see. A booking with the wrong token or email is not found too, so a guess learns nothing. Carries entity: the kind of record, such as product or booking. | Check the slug, id or reference. For a booking, check the token or the lead traveller's email. |
Conflict | 409 | The record's state doesn't allow it now: cancelling a booking that is already cancelled, or checking out a hold that was checked out for someone else. | Read the record again and act on its current state. |
InvalidInput | 422 | A field is missing, has the wrong type or a value the API doesn't take. | Fix the request; detail names the field. |
PriceTableNotMonotonic | 422 | Seller endpoints only: a price table where one more traveller would lower the total. Carries cells: each with unit_id, tier and min_net_usd. | Raise the cells listed in cells. |
RateLimited | 429 | Too many tries in a short time at a sign-in link, a passkey sign-in or a contact change. Carries retry_after_seconds. | Wait retry_after_seconds, then try again. |
SearchUnavailable | 503 | Search is down, or not set up on this server. | Fall back to the trip list (GET /v1/public/products with q), and try search again later. |
A request that doesn’t fit the contract (a field missing, a wrong type, a
value the API doesn’t take) is InvalidInput, and detail names the
field:
curl "https://api.stg.vacationpackagesoman.com/v1/public/products?duration=weekend" \ -H "Tp-Publishable-Key: $TP_KEY"const res = await fetch("https://api.stg.vacationpackagesoman.com/v1/public/products?duration=weekend", { headers: { "Tp-Publishable-Key": process.env.TP_KEY!, },})const answer = await res.json()Answer: 422 Unprocessable Content
{ "type": "about:blank", "title": "Unprocessable Content", "status": 422, "code": "InvalidInput", "detail": "duration takes up_to_3h, 3h_to_6h, full_day, multi_day, not weekend"}Why a quote or booking is refused
Section titled “Why a quote or booking is refused”A quote, a hold or a checkout that can’t go ahead is a 422 with one of
these codes and the facts in details (named as the API sends them):
curl -X POST "https://api.stg.vacationpackagesoman.com/v1/public/quotes" \ -H "Tp-Publishable-Key: $TP_KEY" \ -H "Content-Type: application/json" \ -d '{ "option_id": "9e4c2a71-3b5d-4e8f-a1c6-7d20f5b93e48", "start": "2026-11-14T15:00", "units": [ { "unit_id": "1c7e9b42-6a3f-4d85-b0e2-58f4a1c9d736", "qty": 4 } ], "currency": "USD"}'const res = await fetch("https://api.stg.vacationpackagesoman.com/v1/public/quotes", { method: "POST", headers: { "Tp-Publishable-Key": process.env.TP_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ "option_id": "9e4c2a71-3b5d-4e8f-a1c6-7d20f5b93e48", "start": "2026-11-14T15:00", "units": [ { "unit_id": "1c7e9b42-6a3f-4d85-b0e2-58f4a1c9d736", "qty": 4 } ], "currency": "USD" }),})const answer = await res.json()Answer: 422 Unprocessable Content
{ "type": "about:blank", "title": "Unprocessable Content", "status": 422, "code": "NotEnoughCapacity", "detail": "Not enough places are left then.", "details": { "remaining": 2 }}| Code | Status | When | What to do |
|---|---|---|---|
SlotClosed | 422 | The option doesn't run at that start. | Show the calendar again and let the traveller pick another start. |
SoldOut | 422 | No places are left at that start. | Offer another start from the calendar. |
NotEnoughCapacity | 422 | Fewer places are left than travellers. Carries remaining. | Say how many places are left, from remaining. |
PastCutoff | 422 | It is too late to book that start. Carries cutoffAt. | Offer a later start. |
PriceNotAvailable | 422 | The seller has no price for this selection: out of season, a group size or a weekday they don't price. Carries unitId, and reason: outside_season, group_size or weekday. | Offer an enquiry instead. |
UnitQuantityInvalid | 422 | Too few or too many of one kind of traveller or item. Carries unitId, min, max. | Keep the number between min and max. |
AgeRestriction | 422 | A traveller's age is outside the unit's ages. Carries unitId, minAge, maxAge. | Book them under the right unit, or not on this option. |
AccompanimentRequired | 422 | A traveller, usually a child, needs someone with them. Carries unitId, by. | Add one of the units in by. |
UnknownUnit | 422 | A unit_id isn't one of this option's units. Carries unitId. | Take the units from the trip's option. |
VehicleCapacityExceeded | 422 | The vehicles chosen don't have enough seats. Carries pax, seats. | Choose more or bigger vehicles. |
VehicleLimitReached | 422 | Not enough vehicles are left that day. Carries remainingVehicles, limitName. | Offer another day. |
RoomingInvalid | 422 | The rooms don't fit the travellers. Carries reason. | Change the rooms. |
CurrencyNotEnabled | 422 | The site doesn't offer that currency. Carries currency. | Use one of the storefront's currencies. |
ExtraQuantityInvalid | 422 | More of an extra than it allows. Carries extraId, max. | Keep it at max or fewer. |
ExtraSoldOut | 422 | An extra is sold out that day. Carries extraId. | Remove the extra. |
PickupNotOffered | 422 | Pickup isn't offered from there, or the option has no pickup. Carries reason, sometimes. | Ask for another pickup place, or none. |
OnRequest | 422 | A hold was asked for an option the seller confirms by hand. | Skip the hold: check out directly, and the booking waits as pending. |
HoldRequired | 422 | Checkout without a hold, for an option that confirms at once. | Hold first, then check out with the hold's ref. |
HoldExpired | 422 | The hold ran out and its places went back on sale. | Hold again; quote first if time has passed. |
AdjustmentTooLarge | 422 | Seller endpoints only: a booking staff make for a traveller, whose adjustment lines take the total below zero. Carries total. | Make the discount smaller; total is what the lines would leave. |
PaymentMethodRequired | 422 | Checkout without payment_method, on a site that has ways to pay set up. Carries offered. | Send one of offered as payment_method. |
PaymentMethodNotOffered | 422 | That way to pay isn't offered for this start. Carries method. | Ask GET /v1/public/payment-methods again and use one of them. |
TravellerDetailsRequired | 422 | The trip needs every traveller's details before booking, and some are missing. Carries missing: each with traveller and, when only some are missing, fields. | Collect what missing lists and check out again. |
TravellerDetailsInvalid | 422 | Some traveller details aren't right, such as a passport that expires before the trip ends. Carries problems, or the counts that don't match. | Show each of problems next to its field. |
PickupDetailsRequired | 422 | The pickup asks for a flight, a ship or a room, and it is missing. Carries missing. | Collect what missing lists. |
PriceChanged | 422 | Seller endpoints only: the price moved between the quote staff saw and applying a booking's change, or making a booking for a traveller. Carries difference, refund, expected_difference and expected_refund for a change; total and expected_total for a booking. | Quote again and check the new price, then send it as expected_difference for a change or expected_total for a booking. |
SellerUnavailable | 422 | A hold or checkout on the own site of a seller the platform has suspended. Bookings already made keep working. | Say the seller can't take new bookings for now, and keep the trip pages up. |
Other answers worth handling
Section titled “Other answers worth handling”curl -X POST "https://api.stg.vacationpackagesoman.com/v1/public/bookings/TP-4HZQ-8MNE/cancel?token=TOKEN_FROM_THE_LINK" \ -H "Tp-Publishable-Key: $TP_KEY" \ -H "Content-Type: application/json" \ -d '{}'const res = await fetch("https://api.stg.vacationpackagesoman.com/v1/public/bookings/TP-4HZQ-8MNE/cancel?token=TOKEN_FROM_THE_LINK", { method: "POST", headers: { "Tp-Publishable-Key": process.env.TP_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({}),})const answer = await res.json()Answer: 409 Conflict
{ "type": "about:blank", "title": "Conflict", "status": 409, "code": "Conflict", "detail": "booking TP-4HZQ-8MNE is cancelled, not confirmed"}curl "https://api.stg.vacationpackagesoman.com/v1/public/search?q=Desert%20safari%20with%20BBQ%20dinner" \ -H "Tp-Publishable-Key: $TP_KEY"const res = await fetch("https://api.stg.vacationpackagesoman.com/v1/public/search?q=Desert%20safari%20with%20BBQ%20dinner", { headers: { "Tp-Publishable-Key": process.env.TP_KEY!, },})const answer = await res.json()Answer: 503 Service Unavailable
{ "type": "about:blank", "title": "Service Unavailable", "status": 503, "code": "SearchUnavailable", "detail": "Search is down for now: try again shortly."}