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
| Field | Value |
|---|
brand | weebora.com |
locale | it |
productId | t0054825 (provider code) — or use the numeric CMS id from GET /v1/products |
| Venue | TocaHub Lanzarote |
| Hotel (default selection) | THB Lanzarote Beach (4★) |
| Default config | adults=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:
| Status | When |
|---|
400 | Validation error in body / query (e.g. malformed startDate, missing roomIds). |
401 | Missing internal Bearer or missing X-End-User-Authorization on user-scoped reads. |
403 | Internal client lacks profile=internal or the required allowedEntities entry. Or the resolved brand is not in CHECKOUT_PROXY_ALLOWED_BRANDS (env hard cap). |
404 | Upstream returned 404 (itinerary / activity / accommodation / booking not found). |
429 | Quota exceeded for this client. |
502 | Brand 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.