The agnostic cart handoff lets a partner product page create a cart on any supported
House of Journeys brand channel and then send the traveler to the matching checkout.
The partner chooses the brand with ?brand=, while the HoJ API handles authentication,
brand allowlisting, tenant routing, and the upstream cart call.
Use this flow when you only need to create the cart from your product page and then let
the brand checkout own customer details, payment, and booking confirmation. If your
application needs to control every checkout step through API calls, use the full
Checkout flow instead.
These routes are internal only. Call them from your backend or serverless layer with
an internal HoJ API key. Do not expose the HoJ API key in browser JavaScript.
Handoff vs Checkout API
| Flow | Best For | Your App Handles | Brand Checkout Handles |
|---|
| Cart handoff | Fast integration from a product page to hosted checkout | Product selection, date/pax input, cart creation, redirect or iframe | Customer details, pax details, payment, booking confirmation |
| Full checkout API | Native checkout UI owned by your app | Itinerary, activities, accommodation, customer, pax, payment intent, booking confirmation | Upstream availability, pricing, payment backend, booking backend |
Flow
Your product page needs a few stable values:
brand: the HoJ distribution channel name or domain, for example brand.example.
locale: the checkout locale, for example it or en.
productId: either the numeric product id from GET /v1/products or the provider /
trip code returned to internal clients.
startDate: the trip start date in YYYY-MM-DD format.
adults and rooms: traveler configuration for the first cart creation.
affiliateId: optional tracking code agreed with HoJ / the brand channel.
currency: optional currency, currently EUR, USD, or GBP.
You can fetch the product detail first to get pricing, availability, media, and date
metadata:
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
"${BASE}/v1/products/${PRODUCT_ID}?brand=${BRAND}&locale=${LOCALE}"
Create The Cart
Create the cart from your backend:
curl -sS -X POST \
-H "Authorization: Bearer ${HOJ_KEY}" \
-H "Content-Type: application/json" \
"${BASE}/v1/itineraries?brand=${BRAND}&locale=${LOCALE}" \
-d '{
"productId": "t0000000",
"startDate": "2026-05-30",
"adults": 2,
"rooms": 1,
"affiliateId": "partner-affiliate",
"currency": "EUR"
}'
Successful response:
{
"data": { "itineraryId": "abc123xyz" },
"meta": { "now": 1776788413063 }
}
Persist or log the itineraryId; it is the cart identifier for later checkout, support,
or full internal checkout flows.
Build The Checkout URL
For a browser handoff, build a checkout URL using the resolved brand domain, locale, and
itinerary id:
function buildCheckoutUrl(input: {
brandDomain: string;
locale: string;
itineraryId: string;
affiliateId?: string | null;
}) {
const url = new URL(
`https://${input.brandDomain}/${input.locale}/checkout/${encodeURIComponent(input.itineraryId)}`,
);
if (input.affiliateId) {
url.searchParams.set("affiliate_id", input.affiliateId);
}
return url.toString();
}
Example output:
https://brand.example/it/checkout/abc123xyz?affiliate_id=partner-affiliate
If you do not know the canonical brand domain, resolve it first with
GET /v1/distribution-channels and match by channel name or domainUrl.
Redirect Or Embed
The simplest handoff is a normal browser redirect:
window.location.href = checkoutUrl;
If your product site needs to preserve its own header / footer, route the traveler to a
local checkout wrapper and pass the checkout URL as an encoded query parameter:
const wrapperUrl = new URL(`/${locale}/checkout/`, window.location.origin);
wrapperUrl.searchParams.set("u", checkoutUrl);
window.location.href = wrapperUrl.toString();
The wrapper page can render an iframe:
export function CheckoutFrame({ checkoutUrl }: { checkoutUrl: string }) {
return (
<iframe
src={checkoutUrl}
title="Checkout"
style={{ width: "100%", minHeight: 900, border: 0 }}
allow="payment *"
/>
);
}
For embedded checkout, coordinate iframe behavior with HoJ / the brand channel. Some
brand checkouts can be configured to hide their native header/footer, apply partner
theming, and post height/navigation events to the parent page.
Frontend Helper Example
Your browser should call your backend, not the HoJ API directly:
async function startCartHandoff(input: {
productId: string;
startDate: string;
adults: number;
locale: string;
}) {
const res = await fetch("/api/cart-handoff", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!res.ok) {
throw new Error("Could not create checkout cart");
}
const { checkoutUrl } = await res.json();
window.location.href = checkoutUrl;
}
Backend sketch:
export async function createCartHandoff(input: {
productId: string;
startDate: string;
adults: number;
locale: string;
}) {
const brand = process.env.HOJ_BRAND_DOMAIN ?? "brand.example";
const affiliateId = process.env.HOJ_AFFILIATE_ID ?? "partner-affiliate";
const res = await fetch(
`${process.env.HOJ_API_BASE}/v1/itineraries?brand=${encodeURIComponent(brand)}&locale=${encodeURIComponent(input.locale)}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HOJ_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
productId: input.productId,
startDate: input.startDate,
adults: input.adults,
rooms: 1,
affiliateId,
currency: "EUR",
}),
},
);
if (!res.ok) {
throw new Error(`HoJ cart creation failed: ${res.status}`);
}
const body = await res.json() as { data?: { itineraryId?: string } };
const itineraryId = body.data?.itineraryId;
if (!itineraryId) {
throw new Error("HoJ cart creation did not return an itineraryId");
}
return {
itineraryId,
checkoutUrl: buildCheckoutUrl({
brandDomain: brand,
locale: input.locale,
itineraryId,
affiliateId,
}),
};
}
Fallback Product URL
If cart creation fails or the response does not contain an itineraryId, show a clear
error and offer a fallback link to the product page:
https://brand.example/{locale}/product/{productCode}?affiliate_id=partner-affiliate
Use the fallback only as a recovery path. A successful cart handoff should always send
travelers to /checkout/{itineraryId}.
Operational Checklist
Before enabling a new product or brand:
- Confirm the internal client has
profile=internal and allowedEntities includes
itineraries.
- Confirm the brand is available in
GET /v1/distribution-channels and allowed by the
checkout proxy configuration.
- Verify the product id or provider code with
GET /v1/products.
- Test
POST /v1/itineraries with the chosen brand, locale, startDate, adults,
and rooms.
- Open the generated checkout URL in a browser and complete a test flow in the target
environment.
- For iframe embeds, test Safari/iOS and other browsers with third-party storage
restrictions.
Error Handling
The HoJ API returns problem-details errors. Treat any non-2xx response from
POST /v1/itineraries as a cart creation failure and avoid sending the traveler to a
broken checkout URL.
Common cases:
400: invalid request body or date format.
401: missing or invalid HoJ API key.
403: client is not internal, lacks itineraries, or brand is not allowed.
404: product or upstream cart resource was not found.
429: quota exceeded.
502: brand checkout upstream failed, timed out, or returned invalid JSON.