The brand-channel checkout proxy lets internal clients drive the full purchase flow on booking.hofj.com, weebora.com, and terrarossa.com through a single, normalized HoJ API surface. The brand is selected at every call with ?brand=; the resolved domain is both the upstream host and the tenant routing value.
These routes are internal only. The calling client must have profile=internal and allowedEntities includes itineraries (cart manipulation), bookings (confirmation + user-scoped lookup), and trips (user-scoped lists). Unknown brands are rejected against the CMS distribution-channels list (see GET /v1/distribution-channels).

How it works

Every response uses the standard HoJ envelope:
{ "data": { /* ... */ }, "meta": { "now": 1747505000000 } }
meta.now is forwarded from the brand site (epoch ms) for clock-skew accounting on the frontend. Errors are RFC 7807 problem-details (application/problem+json).

Authentication

  • Internal client credentials: standard HoJ Bearer (API key or OAuth2 client_credentials). Sent on every call as Authorization: Bearer <key>. Required for all routes.
  • End-user Bearer (X-End-User-Authorization): only required by user-scoped reads (GET /v1/bookings/:id, GET /v1/trips/:status, GET /v1/trips/active-summary). The proxy forwards it verbatim as the upstream Authorization header (the brand site expects the Cognito access token from the user’s session).
BASE="https://api.hofj.com"
HOJ_KEY="REPLACE-WITH-INTERNAL-API-KEY"
END_USER_TOKEN="REPLACE-WITH-COGNITO-ACCESS-TOKEN" # only for user-scoped reads
BRAND="weebora.com" # or booking.hofj.com, terrarossa.com
LOCALE="it"
For staging, use the staging API base URL and a staging brand such as staging.weebora.com (or simply Weebora, resolved through /v1/distribution-channels).

1. Pre-flight — pick a product

The brand-site productId must match a product visible on the chosen brand. For internal flows you can either look it up from the public listing or via trip code:
# numeric CMS id route
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/products?brand=${BRAND}&locale=${LOCALE}&limit=25"

# trip code → full internal DTO (returns providerID like `t0054825`)
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/products/tripcode/MKT_PAD_LZL?brand=${BRAND}&locale=${LOCALE}"
POST /v1/itineraries accepts either form for productId:
  • numeric CMS id (e.g. 181) — matches the id field of GET /v1/products.
  • alphanumeric trip / provider code (e.g. t0054825) — matches providerID / tripCode on the internal DTO.

2. Create itinerary

curl -sS -X POST -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries?brand=${BRAND}&locale=${LOCALE}" \
  -d '{
        "productId": "t0054825",
        "startDate": "2026-07-15",
        "adults": 2,
        "rooms": 1,
        "currency": "EUR"
      }'
Response:
{ "data": { "itineraryId": "abc-123-def-456" }, "meta": { "now": 1747505000000 } }
Persist itineraryId — it identifies the cart on every subsequent call.

3. Experience options (activities)

# recommended activities for the trip date
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/activities?brand=${BRAND}&locale=${LOCALE}&startDate=2026-07-15"

# add an activity (body matches the brand-site `AddActivity` schema)
curl -sS -X POST -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/activities/${ACTIVITY_ID}?brand=${BRAND}&locale=${LOCALE}" \
  -d '{ "selectedCategory": { "id": "cat-1" }, "selectedAmenities": [], "paxNumber": 2, "startTime": "09:00", "endTime": "11:00" }'

# update or remove
curl -sS -X PATCH  "${BASE}/v1/itineraries/${ITINERARY_ID}/activities/${ACTIVITY_ID}?brand=${BRAND}&locale=${LOCALE}" \
  -H "Authorization: Bearer ${HOJ_KEY}" -H "Content-Type: application/json" \
  -d '{ "paxNumber": 2, "startTime": "10:00" }'
curl -sS -X DELETE "${BASE}/v1/itineraries/${ITINERARY_ID}/activities/${ACTIVITY_ID}?brand=${BRAND}&locale=${LOCALE}" \
  -H "Authorization: Bearer ${HOJ_KEY}"

4. Accommodation

# paginated list with filters & sort
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/accommodations?brand=${BRAND}&locale=${LOCALE}&startDate=2026-07-15&sortByValue=recommended&page=1&stars=4,5"

# lock the desired rooms
curl -sS -X PATCH -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/accommodations/${ACCOMMODATION_ID}?brand=${BRAND}&locale=${LOCALE}" \
  -d '{ "roomIds": ["room-double-balcony"] }'

5. Customer info

curl -sS -X PUT -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/customer?brand=${BRAND}&locale=${LOCALE}" \
  -d '{
        "firstName": "Mario", "lastName": "Rossi",
        "email": "mario@example.com", "phone": "+390000000000",
        "taxNumber": "RSSMRA80A01H501U", "marketingOptIn": true,
        "address": { "line1": "Via Roma 1", "city": "Roma", "postalCode": "00100", "country": "IT" }
      }'

6. Pax details

GET .../pax returns one slot per traveler with a stable refId. PUT .../pax accepts the full array; each element MUST preserve its refId.
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/pax?brand=${BRAND}&locale=${LOCALE}"

curl -sS -X PUT -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/pax?brand=${BRAND}&locale=${LOCALE}" \
  -d '[
        { "refId": "pax-1", "firstName": "Mario", "lastName": "Rossi", "gender": "Male", "nationalityCountryCode": "IT", "birthDate": "1980-01-01T00:00:00.000Z" },
        { "refId": "pax-2", "firstName": "Luigi", "lastName": "Verdi", "gender": "Male", "nationalityCountryCode": "IT" }
      ]'

7. Payment

Optionally apply promo code / change currency, then refresh the Stripe payment intent:
# apply a promo code (optional)
curl -sS -X POST -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/promo-code?brand=${BRAND}&locale=${LOCALE}" \
  -d '{ "code": "WEB10" }'

# switch currency (optional; EUR | USD | GBP)
curl -sS -X PUT -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/currency/USD?brand=${BRAND}&locale=${LOCALE}"

# refresh the Stripe payment intent — returns the client_secret to confirm on the frontend
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/payment?brand=${BRAND}&locale=${LOCALE}"
Response:
{ "data": "pi_3O9...client_secret", "meta": { "now": 1747505000000 } }
On the frontend, hand the data to Stripe.js:
const { error } = await stripe.confirmCardPayment(clientSecret, {
  payment_method: { card: cardElement },
});

8. Thank-you — confirm booking

After Stripe confirms the payment:
curl -sS -X POST -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/bookings?brand=${BRAND}&locale=${LOCALE}" \
  -d '{ "itineraryId": "'"${ITINERARY_ID}"'" }'
Response:
{ "data": "R-789012", "meta": { "now": 1747505000000 } }
data is the human-readable reservation code — display it on the thank-you page.

Post-purchase lookups (user-scoped)

These routes are user-scoped on the brand site. Pass the end-user’s Cognito access token in X-End-User-Authorization:
# single booking detail
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "X-End-User-Authorization: Bearer ${END_USER_TOKEN}" \
  "${BASE}/v1/bookings/${BOOKING_ID}?brand=${BRAND}&locale=${LOCALE}"

# trips by status
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "X-End-User-Authorization: Bearer ${END_USER_TOKEN}" \
  "${BASE}/v1/trips/active?brand=${BRAND}&locale=${LOCALE}"

# active trips summary
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "X-End-User-Authorization: Bearer ${END_USER_TOKEN}" \
  "${BASE}/v1/trips/active-summary?brand=${BRAND}&locale=${LOCALE}"

Worked example — Magnifico Padel a Lanzarote

Product page: weebora.com/it/destinazioni/lanzarote/tocahub-lanzarote/magnifico-padel-a-lanzarote
FieldValue
brandweebora.com
localeit
productIdt0054825 (provider code) — or use the numeric CMS id from GET /v1/products
VenueTocaHub Lanzarote
Hotel (default selection)THB Lanzarote Beach (4★)
Default configadults=2, rooms=1
Starting price€578
BASE="https://api.hofj.com"
HOJ_KEY="REPLACE-ME"
BRAND="weebora.com"
LOCALE="it"

# 1. create itinerary
RESP=$(curl -sS -X POST -H "Authorization: Bearer ${HOJ_KEY}" \
  -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries?brand=${BRAND}&locale=${LOCALE}" \
  -d '{ "productId": "t0054825", "startDate": "2026-07-15", "adults": 2, "rooms": 1, "currency": "EUR" }')
ITINERARY_ID=$(echo "$RESP" | sed -n 's/.*"itineraryId":"\([^"]*\)".*/\1/p')

# 2. update customer + pax
curl -sS -X PUT -H "Authorization: Bearer ${HOJ_KEY}" -H "Content-Type: application/json" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/customer?brand=${BRAND}&locale=${LOCALE}" \
  -d '{ "firstName":"Mario","lastName":"Rossi","email":"mario@example.com","phone":"+390000000000","address":{"line1":"Via Roma 1","city":"Roma","country":"IT"} }'

# 3. refresh payment intent
curl -sS -H "Authorization: Bearer ${HOJ_KEY}" \
  "${BASE}/v1/itineraries/${ITINERARY_ID}/payment?brand=${BRAND}&locale=${LOCALE}"

# 4. (frontend confirms Stripe payment)

# 5. confirm booking
curl -sS -X POST -H "Authorization: Bearer ${HOJ_KEY}" -H "Content-Type: application/json" \
  "${BASE}/v1/bookings?brand=${BRAND}&locale=${LOCALE}" \
  -d "{ \"itineraryId\": \"${ITINERARY_ID}\" }"

Error model

All errors are application/problem+json per RFC 7807:
StatusWhen
400Validation error in body / query (e.g. malformed startDate, missing roomIds).
401Missing internal Bearer or missing X-End-User-Authorization on user-scoped reads.
403Internal client lacks profile=internal or the required allowedEntities entry. Or the resolved brand is not in CHECKOUT_PROXY_ALLOWED_BRANDS (env hard cap).
404Upstream returned 404 (itinerary / activity / accommodation / booking not found).
429Quota exceeded for this client.
502Brand site returned 5xx, timed out, or returned non-JSON.

Operational notes

  • Idempotency: brand-site POST /booking is an upsert keyed by itineraryId — calling it twice after a successful payment is safe and returns the same reservation code.
  • Timeouts: default 15s per upstream call (CHECKOUT_PROXY_TIMEOUT_MS). Activity / accommodation searches can be the slowest because they fan out to upstream availability.
  • Tenant routing: the same domain resolved from ?brand= is also sent as X-Nezasa-Channel to the CMS for content lookups. Use GET /v1/distribution-channels to see all registered brand names and domains.