Error Handling & Rate Limits

Every error comes back as predictable JSON with a status, a machine-readable code and a human message. Here is the full map — and how to handle rate limits robustly.

Build integrations that fail gracefully.

Log in to the Console

The error model

Every error response uses the same JSON shape:

{
  "status": 400,
  "code": "invalid_date_format",
  "message": "start_date must be a valid date in YYYY-MM-DD format."
}

| Field | Description | |---|---| | status | The HTTP status code | | code | A stable, machine-readable identifier you can switch on in code | | message | A human-readable explanation |

Always switch on code, never on the message text — messages may change, codes won't.

Error codes

| HTTP | Code | Meaning | |---|---|---| | 400 | missing_parameter | A required parameter (e.g. service_number) wasn't provided | | 400 | invalid_phone_format | Phone number isn't a valid Australian number | | 400 | invalid_date_format | start_date/end_date isn't valid YYYY-MM-DD | | 400 | invalid_date_range | start_date is after end_date | | 400 | last_forwarding_number | Can't remove the last forwarding number on a service | | 400 | unsupported_routing_type | The service uses IVR/advanced routing, not list forwarding | | 401 | unauthorized | Missing, invalid or expired Bearer token | | 403 | unauthorized_service | The service belongs to a different account | | 404 | service_not_found | No service exists with that ID | | 405 | method_not_allowed | Wrong HTTP method for the endpoint | | 429 | too_many_requests | Rate limit exceeded | | 500 | internal_error | Unexpected server error |

Handling rate limits

Rate limiting is communicated through headers on every response:

  • X-RateLimit-Limit — max requests in the window.
  • X-RateLimit-Remaining — requests left in the window.
  • X-RateLimit-Reset — Unix timestamp when the window resets.

When you exceed the limit you get 429 too_many_requests. A robust client respects these rather than hammering the endpoint.

Respecting rate limits in JavaScript

const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });

const remaining = res.headers.get("X-RateLimit-Remaining");
const resetAt  = Number(res.headers.get("X-RateLimit-Reset"));

if (res.status === 429) {
  const waitMs = Math.max(0, resetAt * 1000 - Date.now()) + 1000;
  console.log(`Rate limited — retrying in ${Math.round(waitMs / 1000)}s`);
  await new Promise(r => setTimeout(r, waitMs));
  // ... retry the request
}

Backoff for transient errors

For 429 and 5xx errors, retry with exponential backoff — start short and grow:

const delay = (ms) => new Promise(r => setTimeout(r, ms));

async function apiGet(url, retries = 5) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
    if (res.status < 500 && res.status !== 429) return res;
    const waitMs = Math.min(1000 * 2 ** (attempt - 1), 15000);
    console.log(`Retry ${attempt} in ${waitMs}ms`);
    await delay(waitMs);
  }
  throw new Error("Max retries exceeded");
}

Conventions that reduce errors

  • Read before you write. Before a DELETE, fetch the current routing so you never hit last_forwarding_number.
  • Validate dates client-side. Send YYYY-MM-DD and ensure start_date <= end_date.
  • Use Australian formats. 0298765432 / +61298765432 or 0412345678 / +61412345678.
  • Send the right method. Use PUT for updates, POST for creates, DELETE for removals — wrong methods return 405.

Related