The Zameera Partner API
One HTTP API for two jobs: read the published Zameera catalog (every experience and destination, with the same copy, imagery and pricing the website renders), and file booking requests on behalf of your guests. Built for travel advisors, concierge platforms and partners who want the collection inside their own product.
Base URL https://zameera.com/api/v1
Three Calls to a First Booking
- Get a key. Email contact@zameera.com with who you are and what you are building. Keys look like
zk_live_…, carry one or more scopes (catalog:read,bookings:write,bookings:read) and are usually issued within a day. Keep it server-side: it is a password. - Pull the catalog. Every endpoint lives under
https://zameera.com/api/v1; send the key as a bearer token.curl -s "https://zameera.com/api/v1/experiences?destination=rwanda&limit=2" \ -H "Authorization: Bearer zk_live_YOUR_KEY_HERE"
- File a booking request. Post the guest's details against an experience slug. You get back a Zameera reference; our team confirms dates and price with the guest from there.
curl -s -X POST "https://zameera.com/api/v1/bookings" \ -H "Authorization: Bearer zk_live_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: SANDHURST-48213" \ -d '{ "experience": "the-humble-silverback", "dates": { "start": "2026-11-12" }, "guests": { "adults": 2, "children": 0 }, "customer": { "name": "Amelia Hart", "email": "amelia.hart@example.com", "phone": "+44 7700 900123", "country": "GB", "language": "en" }, "special_requests": "Celebrating an anniversary; a quiet room please.", "partner_reference": "SANDHURST-48213" }'
Base URL, Authentication and Scopes
Base URL: https://zameera.com/api/v1. Authentication is a single bearer token: send your key as an Authorization: Bearer header, or as X-API-Key if that suits your client better. We store only a hash of your key, so a lost key is reissued rather than recovered; a leaked one is revoked in seconds and replaced.
Each key carries scopes. catalog:read unlocks the experience and destination endpoints, bookings:write lets you file booking requests, and bookings:read lets you read back the bookings that key filed, and only those. A call outside your scopes returns 403 forbidden. Ask for the scopes you need when you request the key; adding one later is a new key.
Responses are JSON. Errors use one shape, { "error": { "code", "message", "details"? } }, listed in full under Errors below. CORS is open, so browser clients work without a proxy, though a booking key belongs on a server.
Endpoints
| Endpoint | Scope | Returns |
|---|---|---|
GET /api/v1 | Public | Service index: version, docs and the OpenAPI URL. |
GET /api/v1/openapi.json | Public | The full OpenAPI 3.1 description of every endpoint. |
GET /api/v1/experiences | catalog:read | Published experiences. Filter with destination, page with cursor, sync with updated_since. |
GET /api/v1/experiences/{slug} | catalog:read | One experience in full: overview, itinerary, facts, features, gallery, before-you-go. |
GET /api/v1/destinations | catalog:read | Every destination with a public page, and how many experiences it holds. |
GET /api/v1/destinations/{slug} | catalog:read | One destination, its editorial content and its experiences. |
POST /api/v1/bookings | bookings:write | File a booking request for a guest. Returns the Zameera reference. |
GET /api/v1/bookings | bookings:read | The bookings this key filed, newest first. Filter by status, poll with updated_since. |
GET /api/v1/bookings/{reference} | bookings:read | One booking and its current status. |
Destination slugs are page slugs, the same ones the site uses: rwanda, maldives, south-africa. Sabi Sand and Kruger are one destination (south-africa) in the API, exactly as they are one page on the site. Import /api/v1/openapi.json into Postman, Insomnia or your generator of choice for typed clients.
What an Experience Looks Like
{
"data": [
{
"id": "0f0d2f0e-6a4f-4a4c-9d2c-2f3c9a1b7e10",
"slug": "the-humble-silverback",
"url": "https://zameera.com/experience/the-humble-silverback",
"title": "The Humble Silverback",
"subtitle": "Eight days beside the Virungas",
"card_summary": "Two treks, one lodge, eight days.",
"destination": {
"slug": "rwanda",
"name": "Rwanda",
"country": "Rwanda",
"url": "https://zameera.com/destination/rwanda"
},
"images": {
"hero": { "url": "https://zameera.com/images/hs-subhero.jpg", "alt": "The Humble Silverback" },
"gallery": []
},
"duration_days": 8,
"capacity": { "min": 2, "max": 8 },
"pricing": {
"mode": "per_person",
"from": { "amount": 67000, "currency": "USD" },
"note": null,
"display": "From USD 67,000 per person"
},
"availability": {
"mode": "on_request",
"note": "Dates are confirmed privately by the Zameera team; enquire to check availability.",
"duration_days": 8
},
"updated_at": "2026-08-01T09:12:44.128Z"
}
],
"meta": { "count": 1, "next_cursor": null, "generated_at": "2026-08-18T06:00:00.000Z" }
}The detail endpoint returns everything above plus the long-form content: description, the editorial overview (cards, key info, sample itinerary, before-you-go), tiles, facts, features, day_phases and story. Every image URL is absolute and safe to hotlink. Nothing operational is exposed: no suppliers, no other partners' bookings, no customer data, no unpublished rows.
- Pagination. Pass
limit(default 50, maximum 100). When more rows exist,meta.next_cursoris an opaque cursor; send it back as?cursor=…for the next page. When it isnull, you have the whole set. - Conditional requests. Every catalog response carries a weak
ETag. Send it back asIf-None-Matchand an unchanged catalog answers304 Not Modifiedwith no body. - Incremental sync.
?updated_since=2026-01-31T00:00:00Zreturns only rows changed after that instant. Store the largestupdated_atyou have seen and pass it next time. - Caching. Catalog responses are shared-cacheable for 60 seconds with a 5-minute stale-while-revalidate window. Mirroring the catalog locally and refreshing every few minutes is the intended pattern.
- Pricing.
pricing.from.amountis the ratefor the experience'spricing.mode, not a total:flatis the whole-party price,per_personmultiplies by guests,per_nightby nights, andper_person_per_nightby both. Usepricing.displayif you just want a line to print. - Availability is on request. Zameera does not sell instant inventory. Every experience reports
availability.mode: "on_request": there are no dates to query and no seats to hold. Present the experience, then either send the guest to the returnedurlor file a booking request through the API. If live availability ever exists for part of the collection, it will arrive as a newmodevalue, never as a change to this one.
Filing a Booking Request
POST /api/v1/bookings files a booking request. It is not an instant reservation and nothing is charged: the request lands with the Zameera team as status: enquiry, we check the dates with the lodge, operator or crew, agree the final price with the guest, and send a payment link. Exactly the same path a guest takes on the website, with your reference attached.
The body is JSON with four required parts (experience, dates, guests, customer) and a few optional ones. Unknown fields are ignored, so you can send your own metadata without breaking anything.
| Field | Type | Rules |
|---|---|---|
experience | string, required | The experience slug from GET /experiences, e.g. the-humble-silverback. |
dates.start | date, required | Arrival, YYYY-MM-DD. Today or later, within 5 years. |
dates.end | date, optional | Departure, after start. Leave it out for a fixed-length experience and we derive it from the itinerary. |
guests | integer or object, required | Party size 1 to 50, as a number or { "adults": n, "children": n }. |
customer.name | string, required | Full name, up to 200 characters. |
customer.email | string, required | A valid address. Zameera writes to the guest here unless notify_guest is false. |
customer.phone | string, optional | International format preferred, e.g. +44 7700 900123. |
customer.country | string, optional | Country of residence: ISO 3166-1 alpha-2 or a name. |
customer.language | string, optional | Preferred language for correspondence, e.g. en, fr, ar. |
customer.nationality | string, optional | Used for visa and permit guidance. |
special_requests | string, optional | Free text, up to 2,000 characters, passed verbatim to our team. |
partner_reference | string, optional | Your own booking or enquiry id, up to 100 characters. Echoed back on every read. |
notify_guest | boolean, default true | Send the guest Zameera's 'request received' email. false keeps all guest correspondence on your side until we confirm. |
marketing_opt_in | boolean, default false | Only true when the guest has actually agreed to hear from Zameera. |
The request
curl -s -X POST "https://zameera.com/api/v1/bookings" \
-H "Authorization: Bearer zk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: SANDHURST-48213" \
-d '{
"experience": "the-humble-silverback",
"dates": { "start": "2026-11-12" },
"guests": { "adults": 2, "children": 0 },
"customer": {
"name": "Amelia Hart",
"email": "amelia.hart@example.com",
"phone": "+44 7700 900123",
"country": "GB",
"language": "en"
},
"special_requests": "Celebrating an anniversary; a quiet room please.",
"partner_reference": "SANDHURST-48213"
}'The response
201 Created with the booking. Store reference: it is how the guest, your team and ours talk about this trip from now on. estimate is computed from the published rate and is never a quote. For a fixed-length experience dates.end is filled in for you.
HTTP/1.1 201 Created
Location: /api/v1/bookings/ZAM-7K2QMD
Cache-Control: no-store
{
"data": {
"reference": "ZAM-7K2QMD",
"status": "enquiry",
"payment_status": "unpaid",
"experience": {
"slug": "the-humble-silverback",
"title": "The Humble Silverback",
"url": "https://zameera.com/experience/the-humble-silverback"
},
"dates": { "start": "2026-11-12", "end": "2026-11-20", "nights": 8 },
"guests": 2,
"estimate": {
"amount": 134000,
"currency": "USD",
"display": "USD 134,000",
"note": "Approximate, computed from the published rate. Zameera confirms the final price with the guest before any payment is requested."
},
"customer": {
"name": "Amelia Hart",
"email": "amelia.hart@example.com",
"phone": "+44 7700 900123",
"country": "GB",
"language": "en"
},
"special_requests": "Celebrating an anniversary; a quiet room please.",
"partner_reference": "SANDHURST-48213",
"source": "api",
"created_at": "2026-08-18T09:14:02.511Z",
"updated_at": "2026-08-18T09:14:02.511Z",
"confirmed_at": null,
"cancelled_at": null,
"confirmation_url": "https://zameera.com/book/confirmation/ZAM-7K2QMD"
},
"meta": { "generated_at": "2026-08-18T09:14:02.530Z" }
}Idempotency: never file twice
Send an Idempotency-Key header on every POST, unique per booking; your own booking id is ideal. If the network drops and you retry, a request with a key we have already seen returns the original booking with 200 and Idempotent-Replayed: true instead of creating a second one. Without the header, every successful POST is a new request.
Validation
We validate the whole body and report every problem at once, so a bad payload is fixed in one round trip. An unknown or unpublished experience slug is a validation error, not a 404.
HTTP/1.1 400 Bad Request
{
"error": {
"code": "bad_request",
"message": "The booking request is invalid. See `details` for each field.",
"details": [
{ "field": "dates.start", "message": "Must be today or later." },
{ "field": "customer.email", "message": "Must be a valid email address." }
]
}
}Following the request
Read a booking back with GET /api/v1/bookings/{reference}, or poll GET /api/v1/bookings?updated_since=… to pick up everything that changed since your last check. A key only ever sees the bookings it filed. Statuses move forward only:
# One booking curl -s "https://zameera.com/api/v1/bookings/ZAM-7K2QMD" \ -H "Authorization: Bearer zk_live_YOUR_KEY_HERE" # Everything that changed since your last poll curl -s "https://zameera.com/api/v1/bookings?updated_since=2026-08-18T09:00:00Z" \ -H "Authorization: Bearer zk_live_YOUR_KEY_HERE"
| status | Meaning |
|---|---|
enquiry | Received. Every API booking starts here; our team is reviewing it. |
pending | In hand: dates are being checked with the lodge, operator or crew. |
confirmed | Dates and final price agreed with the guest. A payment link has been or will be sent. |
in_progress | The guest is travelling. |
completed | The experience has taken place. |
cancelled | Withdrawn or declined. cancelled_at is set. |
payment_status (unpaid, processing, paid, refunded, failed) is tracked separately: a booking can be paid while still pending the final operational confirmation. Payment itself happens on a Zameera link we send the guest; the API never takes card details.
Who hears from whom
- Our team is alerted the moment a request lands, tagged with your key's name and your
partner_reference. - The guest receives Zameera's "request received" email at
customer.email, and all follow-up from our team, unless you sendnotify_guest: false. Use that when you want to own the conversation until we confirm. - Booking responses are private (
Cache-Control: no-store) and carry personal data. Treat them accordingly on your side. - Need to change or withdraw a request? For now, email contact@zameera.com quoting the reference. Cancellation and amendment endpoints are on the roadmap and will arrive additively.
Errors
One shape everywhere: { "error": { "code", "message", "details"? } }. message is written for a human reading logs; code is what your code should branch on. Errors are never cached.
| code | HTTP | When |
|---|---|---|
unauthorized | 401 | No key, an unknown key, or a revoked one. Both look the same on purpose. |
forbidden | 403 | A live key that lacks the scope the endpoint needs. |
bad_request | 400 · 413 | Invalid query, body or Idempotency-Key. On validation failures details lists every field. |
not_found | 404 | Unknown slug, or a booking reference this key did not file. |
rate_limited | 429 | Over the per-minute quota. Wait the seconds in Retry-After. |
server_error | 500 · 503 | Our side. Safe to retry with the same Idempotency-Key. |
Rate Limits
Each key has a per-minute quota across all endpoints, 600 requests by default. Booking creation has its own, tighter ceiling of 30 successful requests per minute per key, because every one of them emails people and lands on a human's desk. Exceed either and you get 429 with a Retry-After header; nothing is blocked beyond that window. If your integration needs more, tell us what you are building and we will raise it.
Referral Codes Are Automatic
If your key is linked to a Zameera affiliate account, every catalog URL the API returns already carries your referral code as ?ref=your-code, and every booking you file through the API is attributed to you without any extra field. Link to the URLs as returned and enquiries attribute themselves; there is nothing to append and nothing to track on your side.
Catalog to Booking in Two Languages
JavaScript (Node 18+, fetch)
const BASE = "https://zameera.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.ZAMEERA_API_KEY}`,
"Content-Type": "application/json",
};
// 1. Pull the catalog (page through next_cursor until it is null)
const catalog = await fetch(`${BASE}/experiences?limit=100`, { headers })
.then((r) => r.json());
// 2. File a booking request — reuse your own id as the Idempotency-Key
const res = await fetch(`${BASE}/bookings`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": "SANDHURST-48213" },
body: JSON.stringify({
experience: catalog.data[0].slug,
dates: { start: "2026-11-12" },
guests: 2,
customer: { name: "Amelia Hart", email: "amelia.hart@example.com" },
partner_reference: "SANDHURST-48213",
}),
});
const { data: booking, error } = await res.json();
if (!res.ok) throw new Error(error.message);
console.log(booking.reference, booking.status); // "ZAM-7K2QMD" "enquiry"
// 3. Later: has it been confirmed?
const status = await fetch(`${BASE}/bookings/${booking.reference}`, { headers })
.then((r) => r.json());
console.log(status.data.status, status.data.payment_status);Python (requests)
import os, requests
BASE = "https://zameera.com/api/v1"
headers = {"Authorization": f"Bearer {os.environ['ZAMEERA_API_KEY']}"}
# 1. Pull the catalog
catalog = requests.get(f"{BASE}/experiences", headers=headers,
params={"limit": 100}).json()
# 2. File a booking request
payload = {
"experience": catalog["data"][0]["slug"],
"dates": {"start": "2026-11-12"},
"guests": {"adults": 2, "children": 0},
"customer": {"name": "Amelia Hart", "email": "amelia.hart@example.com"},
"partner_reference": "SANDHURST-48213",
}
r = requests.post(f"{BASE}/bookings", headers={**headers,
"Idempotency-Key": "SANDHURST-48213"}, json=payload)
r.raise_for_status()
booking = r.json()["data"]
print(booking["reference"], booking["status"]) # ZAM-7K2QMD enquiry
# 3. Later: poll for status changes
changed = requests.get(f"{BASE}/bookings", headers=headers,
params={"updated_since": "2026-08-18T09:00:00Z"}).json()Anything that speaks HTTPS and JSON works the same way; the OpenAPI document generates typed clients for most languages.
Versioning and Changelog
- This is v1, served at
/api/v1and stamped on every response asX-API-Version: 1. - Additive changes (new fields, new endpoints, new enum values in a documented list) ship without notice. Write clients that ignore fields they do not recognise.
- A breaking change ships as
/api/v2. v1 keeps running for at least six months after v2 is announced, and we email every key holder.
| Date | Change |
|---|---|
| 2026-08-18 | Bookings: POST /bookings, GET /bookings, GET /bookings/{reference}; scopes bookings:write and bookings:read; Idempotency-Key; error.details on validation failures. |
| 2026-08-18 | v1 launch: experiences and destinations, cursor pagination, ETags, updated_since, automatic affiliate attribution. |
Request a Key
Tell us who you are, what you are building, which scopes you need and roughly what volume you expect. Keys are issued by hand, usually within a day.
contact@zameera.comWorking with clients rather than code? Zameera Enterprise is the trade programme.


