EDistribution API

Errors#

Every failure returns the same JSON envelope, whatever went wrong and whichever endpoint produced it.

Some failures add fields on top — a credit refusal carries the shortfall, a price conflict carries both figures — but the five below are always present.

The envelope
{
  "status": 400,
  "error": "Bad Request",
  "message": "The window between `from` and `to` accepts at most 365 days.",
  "path": "/v1/availability",
  "timestamp": "2026-07-29T13:05:56.198280408Z"
}
statusinteger

The HTTP status, repeated in the body so a logged payload is self-describing.

errorstring

The HTTP reason phrase.

messagestring

Written for a human. Do not pattern-match on it — the wording is not part of the contract and will change.

pathstring

The request path that produced the failure.

timestampstring<date-time>

ISO-8601 instant. Quote this and path when you contact support; together they identify the request.

Status codes#

StatusMeansRetry?
400Malformed request, or a cap exceededNo — fix the request
401Authentication failedNo
402Credit denied, or account on holdNo — free credit first
404Not found, or not yoursNo
409Conflict: price moved, or idempotency key reusedOnly after re-quoting
429Rate limited. Carries Retry-AfterYes, after the delay
5xxOur faultYes, with backoff

404 is also "not yours"#

A resource that exists but belongs to another account returns 404, with the same message as an id that never existed.

This is a deliberate security property, not an oversight. A 403 would confirm the resource exists, which turns any id field into an oracle for enumerating other accounts' bookings. Because the two are indistinguishable, you cannot use this API to discover whether an id exists outside your own account and neither can anyone who steals your key.

The practical consequence for your code: a 404 on an id you believe you own usually means you are pointing at the wrong environment or the wrong key, not that the record vanished.

400 — the caps reject, they never clamp#

Bulk endpoints have limits, and exceeding one is an error rather than a silent truncation.

That choice matters: a clamped request returns a partial answer that looks complete, and a nightly sync built on it drops rows forever without ever failing. An explicit 400 is the version you can debug.

CapApplies to
100 ids per parameterlistingIds, propertyIds, bookingIds, externalBookingIds
365 daysany from/to window
100 rows per pagesize

The id cap is per parameter, not per request — 100 listingIds and 100 propertyIds in one call is fine.

402 — credit denied#

Booking is the only operation that returns 402. The body carries three extra fields telling you exactly how far short you are, so you can show a real number rather than "try again later".

reasonCode is the field to branch on:

INSUFFICIENT_CREDIT — the booking exceeds what is left. shortfall is the gap in rupees.

ACCOUNT_ON_HOLD — an overdue balance past your grace period has blocked booking entirely. Paying the invoice clears it. Reads keep working throughout.

402
{
  "status": 402,
  "error": "Payment Required",
  "message": "This booking would exceed the agent group's available credit.",
  "path": "/v1/bookings",
  "timestamp": "2026-07-29T13:05:56.198280408Z",
  "reasonCode": "INSUFFICIENT_CREDIT",
  "availableCredit": 18400.00,
  "shortfall": 3650.00
}

409 — conflicts#

Two distinct situations share this status, and the extra fields tell them apart.

The price changed#

Your acceptedTotal no longer matches what the stay costs. Nothing was booked and nothing was charged.

Re-quote with POST /v1/orders/{orderId}/reprice, show the new figure to whoever is deciding, and book again with the new total. Never retry with the old one — it will fail identically, by design.

This is the mechanism that stops a price moving underneath a customer between quote and confirmation.

409
{
  "status": 409,
  "error": "Conflict",
  "message": "The price for this order has changed.",
  "path": "/v1/bookings",
  "timestamp": "2026-07-29T13:05:56.198280408Z",
  "acceptedTotal": 22050.00,
  "currentTotal": 23400.00
}

The idempotency key was reused with different contents#

An externalBookingId already exists in your account against a different payload.

The same id with the same payload is not an error — that is a retry, and it returns the original booking. This 409 means two genuinely different bookings were attempted under one id, which is almost always a bug in id generation. Use a new id, or send the original payload.

Retrying, properly#

Only three classes are worth retrying: 429, 5xx, and network timeouts.

Never blind-retry a 4xx other than 429. It will fail identically and burn your rate limit.

Honour Retry-After when present rather than using a fixed delay — it reflects your account's real refill rate, so a hardcoded value will just be rejected again.

A timeout on POST /v1/bookings is not a failure. The booking may exist. Retry with the same externalBookingId — that is exactly what it is for.

Python
RETRYABLE = {429, 500, 502, 503, 504}

def call(method, url, **kw):
    for attempt in range(5):
        try:
            res = requests.request(method, url,
                                   auth=(API_KEY, ""), timeout=30, **kw)
        except requests.Timeout:
            continue                      # same body, same idempotency key
        if res.status_code not in RETRYABLE:
            return res
        wait = int(res.headers.get("Retry-After", 2 ** attempt))
        time.sleep(wait)
    raise RuntimeError("gave up after 5 attempts")

Failures that are not errors#

Two responses look like failures and are not:

  • An empty data array is a successful search with no matches, not a 404.
  • 202 on a booking means accepted and being confirmed with the property. It is not a refusal — wait for the callback or poll GET /v1/bookings/{id}.

Next#

Rate limits covers 429 and the headers on every response.