This guide is for partners who call the Cloud Experiences public API: Resellers (OTAs and channels that sell every LIVE listing) and subscribers (suppliers and reservation systems that push availability and receive signed webhooks).
The API reference is the OpenAPI spec published alongside this guide. This document is the recipe book: auth, catalog, reserve → book → cancel, availability push, and webhook verification.
Environments
| Production | |
|---|---|
| Token | POST https://experiences.nuitee.cloud/api/oauth/token |
| API | https://experiences.nuitee.cloud/api/public/v1 |
Local development uses the same paths on http://localhost:3000.
Credentials themselves are sandbox or live. Sandbox tokens cannot confirm live inventory. Keep one credential per environment.
SLA targets: 99.8% monthly uptime; availability GET P95 under 1.5s; reserve/book P95 under 2.5s. Rate limit is 120 requests/minute per credential (OAuth token endpoint is 30/minute). 429 responses include Retry-After.
Two partner types
| Reseller | Supplier (channel manager) | |
|---|---|---|
| Who | Nuitee-issued marketplace credential (Connect, OTAs) | Owner-created credential in Manage → API |
| Catalog | Every LIVE product | That supplier’s LIVE products only |
| Scopes | availability.read + bookings.write | Any of availability.read, availability.write, bookings.write |
| Availability push | Not granted | inventorySource=API options only |
| Bookings | Persist on the product’s supplier account | Persist on your account |
| Webhooks | Fan-out of booking events in the same environment | Your account’s events |
The owner dashboard cannot mint reseller credentials. Ask Nuitee if you need marketplace access.
Authentication
Exchange client_id and client_secret for a Bearer token. Tokens expire in 3600 seconds. Cache them and refresh before expiry. Send JSON or application/x-www-form-urlencoded.
curl -s -X POST https://experiences.nuitee.cloud/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=client_credentials \
-d client_id="$NCE_CLIENT_ID" \
-d client_secret="$NCE_CLIENT_SECRET"{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "availability.read bookings.write"
}Call the API with Authorization: Bearer <access_token>. Successful list responses are { "data": …, "meta": { "total", "limit", "offset" } }. Single-resource responses are { "data": … }. Errors are { "error", "code", "details" } with a closed set of codes:
AUTHORIZATION_FAILURE · INVALID_PRODUCT · VALIDATION_FAILURE · INTERNAL_SYSTEM_FAILURE · NO_AVAILABILITY · INVALID_RESERVATION · INVALID_BOOKING
Reseller: sell the marketplace catalog
Use this path if you distribute Cloud Experiences on an OTA, metasearch, or Connect.
Scopes you need: availability.read, bookings.write.
1. Discover products
List LIVE products. Optional q searches title, description, and meeting point. Optional lat, lng, and radiusKm (default 50) apply a haversine filter. Products without coordinates are omitted from geo-filtered results only.
curl -s "https://experiences.nuitee.cloud/api/public/v1/products?q=Lisbon&lat=38.7223&lng=-9.1393&radiusKm=25&limit=20" \
-H "Authorization: Bearer $TOKEN"Each item includes id (reference code, for example NCE-P-ALFAMA), description, photos, inclusions/exclusions, meetingPoint, coordinates, placeId, cancellation policy, category, language, supplier { id, name }, options (id, name, duration, group size, adult/child price, currency), ratingAverage, and reviewCount.
curl -s https://experiences.nuitee.cloud/api/public/v1/products/NCE-P-ALFAMA \
-H "Authorization: Bearer $TOKEN"
curl -s https://experiences.nuitee.cloud/api/public/v1/products/NCE-P-ALFAMA/reviews \
-H "Authorization: Bearer $TOKEN"Product id in path or query may be the internal id or the reference code.
2. Pull availability
curl -s "https://experiences.nuitee.cloud/api/public/v1/availabilities?productId=NCE-P-ALFAMA&from=2026-10-01&to=2026-10-31" \
-H "Authorization: Bearer $TOKEN"The payload is a compact season plus exceptions, then per-option vacancies (date, startTime, capacity, remaining, cutoffMinutes, timezone) and current adult/child prices.
3. Hold, confirm, read, cancel
Holds last 15–60 minutes (default 30). Idempotency-Key is required on reserve, book, and cancel. Replaying the same key returns the original result.
Pass optionId plus either slotId or date + startTime. Map traveler mix to adults / children (no third-party category ids).
curl -s -X POST https://experiences.nuitee.cloud/api/public/v1/reservations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: hold-9f3a" \
-d '{
"optionId": "opt_alfama_morning",
"date": "2026-10-02",
"startTime": "10:00",
"adults": 2,
"children": 1,
"leadTravelerName": "Ana Silva",
"email": "[email protected]"
}'{ "data": { "id": "rsv_01", "expiresAt": "2026-10-01T12:30:00.000Z" } }There is no shopping cart. Treat the reservation id as your hold and cart id. Confirm it:
curl -s -X POST https://experiences.nuitee.cloud/api/public/v1/bookings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: book-9f3a" \
-d '{
"reservationId": "rsv_01",
"leadTravelerName": "Ana Silva",
"email": "[email protected]"
}'{
"data": {
"reference": "NCE-B-1001",
"status": "CONFIRMED",
"ticketCodes": ["NCE-B-1001-1", "NCE-B-1001-2"]
}
}After confirm, the booking id is the reference (NCE-B-*). Fetch or cancel with id or reference:
curl -s https://experiences.nuitee.cloud/api/public/v1/bookings/NCE-B-1001 \
-H "Authorization: Bearer $TOKEN"
curl -s -X DELETE "https://experiences.nuitee.cloud/api/public/v1/bookings/NCE-B-1001?initiatedBy=traveler" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: cancel-9f3a"initiatedBy=supplier refunds in full. initiatedBy=traveler applies the listing cancellation policy. Release an unused hold with DELETE /reservations/{id} (204).
4. Subscribe once
Resellers should create one webhook subscription per environment and persist the signing secret. Do not subscribe again on every process boot.
curl -s -X POST https://experiences.nuitee.cloud/api/public/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://partner.example/webhooks/nuiteecloud",
"eventTypes": ["booking.created", "booking.confirmed", "booking.cancelled"]
}'The create response returns { "data": { "ok": true, "secret": "nce_whsec_…" } }. The secret is shown once.
Booking events for marketplace sales are delivered both to the product supplier and to active reseller subscriptions in the same credential environment.
Supplier: connect a reservation system
Use this path if you operate listings in the Cloud Experiences extranet and want a PMS, channel manager, or custom backend to keep availability in sync and receive bookings.
- Create a workspace at
https://experiences.nuitee.cloud/signupand publish a LIVE product. - In Manage → API, create a supplier credential (
sandboxfirst). Choose scopes:availability.read— pull season and vacanciesavailability.write— push vacancy, price, cutoff (only options withinventorySource=API)bookings.write— reserve, book, cancel, subscribe to webhooks
- Store
client_id/client_secret. Rotating a secret invalidates the old one after you save the new value.
MANUAL options stay on the availability board. Do not POST availability for them.
Push vacancies
curl -s -X POST https://experiences.nuitee.cloud/api/public/v1/availabilities \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"productId": "NCE-P-ALFAMA",
"optionId": "opt_api_alfama",
"updates": [
{
"date": "2026-10-02",
"startTime": "14:00",
"vacancy": 10,
"cutoffMinutes": 120,
"timezone": "Europe/Lisbon"
}
]
}'Set closed: true to publish zero remaining. Optional adultPrice / childPrice update the option’s current prices. A successful push also emits availability.updated to subscribers.
Pull the same window with GET /availabilities to confirm remaining seats before you sell elsewhere.
Supplier-scoped GET /products returns only your LIVE rows. The reserve → book → cancel flow is the same as the reseller recipe; bookings land on your account and show up in the extranet.
Webhook subscribers
Both partner types subscribe with POST /webhooks and bookings.write. Supported eventTypes:
| Event | When |
|---|---|
booking.created | Reservation confirmed into a booking (same moment as confirmed for the API channel) |
booking.confirmed | Booking is confirmed |
booking.cancelled | Traveler or supplier cancel |
availability.updated | Supplier availability push |
payout.completed | Payout run finished (suppliers) |
Envelope and headers
POST /your-endpoint
Content-Type: application/json
X-Nuitee-Signature: t=1774454400,v1=<hex>
X-Nuitee-Timestamp: 1774454400
X-Nuitee-Event: booking.confirmed{
"event": "booking.confirmed",
"createdAt": "2026-09-21T16:00:00.000Z",
"data": {
"reference": "NCE-B-1001",
"status": "CONFIRMED",
"ticketCodes": ["NCE-B-1001-1"],
"optionId": "opt_alfama_morning",
"productCode": "NCE-P-ALFAMA",
"totalPrice": 75,
"currency": "EUR",
"activityDate": "2026-10-02T00:00:00.000Z",
"leadTravelerName": "Ana Silva"
}
}data is the serialized booking (or a compact availability payload for availability.updated). Verify the signature against the raw request body, not a re-serialized object.
Verify HMAC
X-Nuitee-Signature is t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, timestamp + "." + rawBody).
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyNuiteeSignature(secret, signatureHeader, rawBody) {
const parsed = Object.fromEntries(
signatureHeader.split(",").map((part) => {
const [key, ...rest] = part.trim().split("=");
return [key, rest.join("=")];
}),
);
const expected = createHmac("sha256", secret)
.update(`${parsed.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(parsed.v1, "utf8");
const b = Buffer.from(expected, "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}Reject stale timestamps (five minutes is a reasonable window). Return 2xx quickly; we retry with backoff (0s, 1m, 5m, 15m, 1h) up to five attempts. Treat unknown event values as ignored success (200) so new event types do not disable delivery.
Idempotency: the same booking can be delivered more than once. Key on data.reference (and event) and ignore duplicates.
Idempotency, capacity, and tickets
- Reserve and book must send
Idempotency-Key. Timeouts should retry with the same key. - A hold increments
bookedCount. Confirming it does not increment again. Cancel and expired holds restore capacity. - Confirmed API bookings include
ticketCodes(one per guest). Surface those as the voucher / supplier booking codes. - Reseller holds still decrement the product owner’s inventory.
Sandbox
After npm run db:seed on a local or sandbox database:
| Credential | Kind | Secret | Catalog |
|---|---|---|---|
nce_sb_lisbon_demo | Supplier | sandbox-secret-demo | Lisbon Explorers only |
nce_sb_connect | Reseller | sandbox-secret-connect | Every LIVE product |
These secrets are demo-only. Production reseller credentials are issued by Nuitee. Production supplier credentials are created in the extranet and shown once.
Go-live checklist
- Store credentials and the webhook signing secret outside source control
- Cache OAuth tokens; do not fetch a token per request
- Send
Idempotency-Keyon reserve, book, and cancel - Verify HMAC on the raw webhook body and return 2xx
- Subscribe webhooks once per environment
- Map adult/child counts only; do not send GetYourGuide category ids
- For geo search, require
coordinateson the listing (suppliers set this in the Locations wizard) - Honor
Retry-Afteron 429 - Use traveler cancel for guest-initiated cancellations; supplier cancel only when you intend a full refund
Related
- OpenAPI:
openapi.yaml - Extranet product and availability UI: supplier dashboard at
/manage
