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

FlowBest ForYour App HandlesBrand Checkout Handles
Cart handoffFast integration from a product page to hosted checkoutProduct selection, date/pax input, cart creation, redirect or iframeCustomer details, pax details, payment, booking confirmation
Full checkout APINative checkout UI owned by your appItinerary, activities, accommodation, customer, pax, payment intent, booking confirmationUpstream availability, pricing, payment backend, booking backend

Flow

Inputs

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.