Travixor GlobalDevelopers

Errors and retries

Every error has the same shape and a code you can test for. Know which requests are safe to send again before you add retries.

The error format

Errors use an HTTP status and a JSON body. Test for error.code; it is stable. The message is for people and its wording may change. Keep the request_id when you contact us.

Example error.
{
  "error": {
    "code": "insufficient_funds",
    "message": "The wallet does not have enough available balance for this.",
    "request_id": "2f6a1c8e-5b93-4d07-a4e2-91c3d8b05f76"
  }
}

Codes

StatusCodeWhat happened
400validation_errorA field is missing, has the wrong type, or an unknown field was sent. The message says which.
400invalid_daysDays given for a fixed package, missing for a package sold per day, or outside 1 to max_days.
400invalid_quantityQuantity is not 1 to 10.
400invalid_cursorThe cursor is not one we gave you. Start the list again.
400idempotency_key_requiredPOST /v1/orders needs an Idempotency-Key header.
400invalid_idempotency_keyThe key must be 8 to 100 letters, digits or the characters _ . : -. A UUID is ideal.
400credentials_in_urlThe key was sent in the address. Use the header.
401invalid_api_keyThe key is missing, wrong, expired or revoked.
403insufficient_scopeThe key lacks the scope this route needs.
403organization_not_approvedThe organisation is not approved, or has been suspended.
403ip_not_allowedThe key is limited to other IP addresses.
404package_not_foundNo package with that id, or it cannot be bought now.
404quote_not_foundNo quote with that id for your organisation.
404not_foundNo order or eSIM with that id for your organisation, or no such address.
409insufficient_fundsThe available balance is lower than the order. Nothing was created.
409quote_expired, quote_usedThe quote is older than 5 minutes, or already has an order. Get a new quote.
409quote_price_changedThe price moved since the quote. Get a new quote.
409package_unavailableThe package cannot be bought now.
413payload_too_largeThe request body is larger than 64 KB.
415unsupported_media_typeSend the body as application/json.
422idempotency_key_reusedThat key was used for a different order.
429rate_limitedToo many requests. Wait for Retry-After seconds.
500internal_errorSomething went wrong on our side. Send the same request again; if it keeps happening, contact us with the request_id.
503ordering_pausedOrders are paused for a short time. Nothing was charged.

An order that was accepted and then could not be supplied is not an HTTP error. It comes back as an order with status failed and a failure.code, for example temporarily_unavailable. Nothing is charged.

What is safe to send again

RequestSafe to repeat?
Every GETAlways.
POST /v1/quotesYes. It only makes another quote; nothing is bought.
POST /v1/ordersYes, but only with the same Idempotency-Key and the same body. That returns the first order instead of buying again.

Retry on a network failure, a timeout, a 5xx and a 429. Wait longer after each attempt, and use Retry-After when it is there. Do not retry other 4xx errors: the request itself needs to change.

cURL
# curl sends the same headers on every retry, so the Idempotency-Key stays the same.
curl https://api.travixorglobal.com/v1/orders \
  --retry 5 --retry-all-errors --retry-delay 2 \
  -H "Authorization: Bearer $TRAVIXOR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6b0f2c1e-8d4a-4c7e-9b53-1f2a7d90e4c8" \
  -d '{"quote_id": "QUOTE_ID"}'
TypeScript
// Retries a request only when it is safe: a network failure, a timeout, a 5xx, or a 429.
// For POST /v1/orders it is safe because every attempt carries the same Idempotency-Key.
async function sendWithRetry(url: string, init: RequestInit, attempts = 5): Promise<Response> {
  for (let attempt = 1; ; attempt++) {
    try {
      const res = await fetch(url, { ...init, signal: AbortSignal.timeout(30_000) })
      const retryable = res.status === 429 || res.status >= 500
      if (!retryable || attempt === attempts) return res
      const wait = Number(res.headers.get('retry-after')) || 2 ** attempt
      await new Promise((resolve) => setTimeout(resolve, wait * 1000))
    } catch (error) {
      if (attempt === attempts) throw error
      await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000))
    }
  }
}

const res = await sendWithRetry('https://api.travixorglobal.com/v1/orders', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.TRAVIXOR_API_KEY}`,
    'content-type': 'application/json',
    'idempotency-key': '6b0f2c1e-8d4a-4c7e-9b53-1f2a7d90e4c8',
  },
  body: JSON.stringify({ quote_id: 'QUOTE_ID' }),
})
console.log(res.status, await res.json())
Python
import os
import time

import requests


def send_with_retry(method, url, attempts=5, **kwargs):
    """Retries only when it is safe: a network failure, a timeout, a 5xx, or a 429.
    For POST /v1/orders it is safe because every attempt carries the same Idempotency-Key."""
    for attempt in range(1, attempts + 1):
        try:
            res = requests.request(method, url, timeout=30, **kwargs)
        except (requests.ConnectionError, requests.Timeout):
            if attempt == attempts:
                raise
            time.sleep(2**attempt)
            continue
        if (res.status_code == 429 or res.status_code >= 500) and attempt < attempts:
            time.sleep(int(res.headers.get("Retry-After", 2**attempt)))
            continue
        return res


res = send_with_retry(
    "POST",
    "https://api.travixorglobal.com/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['TRAVIXOR_API_KEY']}",
        "Idempotency-Key": "6b0f2c1e-8d4a-4c7e-9b53-1f2a7d90e4c8",
    },
    json={"quote_id": "QUOTE_ID"},
)
print(res.status_code, res.json())

Rate limits

Limits count requests from one IP address in a minute. Over the limit you get a 429 with a Retry-After header.

RoutePer minute
POST /v1/quotes60
POST /v1/orders30
Everything else120

If you need more, tell us before you launch.