Zameera
Developers

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

Quick Start

Three Calls to a First Booking

  1. 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.
  2. 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"
  3. 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"
      }'
Getting Started

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.

Reference

Endpoints

EndpointScopeReturns
GET /api/v1PublicService index: version, docs and the OpenAPI URL.
GET /api/v1/openapi.jsonPublicThe full OpenAPI 3.1 description of every endpoint.
GET /api/v1/experiencescatalog:readPublished experiences. Filter with destination, page with cursor, sync with updated_since.
GET /api/v1/experiences/{slug}catalog:readOne experience in full: overview, itinerary, facts, features, gallery, before-you-go.
GET /api/v1/destinationscatalog:readEvery destination with a public page, and how many experiences it holds.
GET /api/v1/destinations/{slug}catalog:readOne destination, its editorial content and its experiences.
POST /api/v1/bookingsbookings:writeFile a booking request for a guest. Returns the Zameera reference.
GET /api/v1/bookingsbookings:readThe bookings this key filed, newest first. Filter by status, poll with updated_since.
GET /api/v1/bookings/{reference}bookings:readOne 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.

Catalog

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_cursor is an opaque cursor; send it back as ?cursor=… for the next page. When it is null, you have the whole set.
  • Conditional requests. Every catalog response carries a weak ETag. Send it back as If-None-Match and an unchanged catalog answers 304 Not Modified with no body.
  • Incremental sync. ?updated_since=2026-01-31T00:00:00Z returns only rows changed after that instant. Store the largest updated_at you 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.amount is the ratefor the experience's pricing.mode, not a total: flat is the whole-party price, per_person multiplies by guests, per_night by nights, and per_person_per_night by both. Use pricing.display if 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 returned url or file a booking request through the API. If live availability ever exists for part of the collection, it will arrive as a new mode value, never as a change to this one.
Bookings

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.

FieldTypeRules
experiencestring, requiredThe experience slug from GET /experiences, e.g. the-humble-silverback.
dates.startdate, requiredArrival, YYYY-MM-DD. Today or later, within 5 years.
dates.enddate, optionalDeparture, after start. Leave it out for a fixed-length experience and we derive it from the itinerary.
guestsinteger or object, requiredParty size 1 to 50, as a number or { "adults": n, "children": n }.
customer.namestring, requiredFull name, up to 200 characters.
customer.emailstring, requiredA valid address. Zameera writes to the guest here unless notify_guest is false.
customer.phonestring, optionalInternational format preferred, e.g. +44 7700 900123.
customer.countrystring, optionalCountry of residence: ISO 3166-1 alpha-2 or a name.
customer.languagestring, optionalPreferred language for correspondence, e.g. en, fr, ar.
customer.nationalitystring, optionalUsed for visa and permit guidance.
special_requestsstring, optionalFree text, up to 2,000 characters, passed verbatim to our team.
partner_referencestring, optionalYour own booking or enquiry id, up to 100 characters. Echoed back on every read.
notify_guestboolean, default trueSend the guest Zameera's 'request received' email. false keeps all guest correspondence on your side until we confirm.
marketing_opt_inboolean, default falseOnly 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"
statusMeaning
enquiryReceived. Every API booking starts here; our team is reviewing it.
pendingIn hand: dates are being checked with the lodge, operator or crew.
confirmedDates and final price agreed with the guest. A payment link has been or will be sent.
in_progressThe guest is travelling.
completedThe experience has taken place.
cancelledWithdrawn 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 send notify_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.
Reference

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.

codeHTTPWhen
unauthorized401No key, an unknown key, or a revoked one. Both look the same on purpose.
forbidden403A live key that lacks the scope the endpoint needs.
bad_request400 · 413Invalid query, body or Idempotency-Key. On validation failures details lists every field.
not_found404Unknown slug, or a booking reference this key did not file.
rate_limited429Over the per-minute quota. Wait the seconds in Retry-After.
server_error500 · 503Our side. Safe to retry with the same Idempotency-Key.
Fair Use

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.

Attribution

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.

Samples

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.

Stability

Versioning and Changelog

  • This is v1, served at /api/v1 and stamped on every response as X-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.
DateChange
2026-08-18Bookings: POST /bookings, GET /bookings, GET /bookings/{reference}; scopes bookings:write and bookings:read; Idempotency-Key; error.details on validation failures.
2026-08-18v1 launch: experiences and destinations, cursor pagination, ETags, updated_since, automatic affiliate attribution.
Access

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.com

Working with clients rather than code? Zameera Enterprise is the trade programme.