Skip to content

Rate limit & Idempotency

Two mechanisms Zorio uses to protect the API and to help your CRM retry safely.

Rate limit

Goals

  • Protect the Zorio platform from request floods (accidental or DDoS).
  • Keep things fair between customer accounts — no single account should monopolize resources.
  • Encourage CRMs to cache and batch instead of hammering endpoints.

Default limits

Counted per token (each token has its own bucket):

Endpoint groupLimitWindow
/api/pbx/calls/click-to-call10 req/min60-second sliding window
/api/pbx/* (every other endpoint)60 req/min60-second sliding window
/api/telesales/*120 req/min60-second sliding window
/api/autocall/*120 req/min60-second sliding window
/api/auth/login5 req/min60-second sliding window (brute-force protection)
/api/auth/* (other endpoints)30 req/min60-second sliding window

Need higher limits?

Enterprise customers can request higher limits through their SLA. Reach out to the Zorio team.

Response headers

Every response includes:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1782706011
HeaderMeaning
X-RateLimit-LimitTotal allowed requests in the window
X-RateLimit-RemainingRequests still available in the window
X-RateLimit-ResetUTC epoch seconds when the quota resets to Limit

When you exceed the limit

HTTP 429 with body:

json
{ "message": "Too many requests. Retry after 25 seconds." }

Headers:

Retry-After: 25
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1782706036

CRM best practices

1. Respect Retry-After

js
const response = await fetch(url, options);
if (response.status === 429) {
  const retryAfter = parseInt(response.headers.get('Retry-After'), 10) || 60;
  await sleep(retryAfter * 1000);
  return retry();
}

2. Exponential backoff + jitter

Avoid the thundering herd when many clients retry at once:

js
async function callWithBackoff(fn, attempt = 0) {
  try {
    return await fn();
  } catch (err) {
    if (err.status !== 429 && err.status < 500) throw err;
    if (attempt >= 5) throw err;

    const baseDelay = Math.min(1000 * Math.pow(2, attempt), 30_000);
    const jitter = Math.random() * 1000;
    await sleep(baseDelay + jitter);
    return callWithBackoff(fn, attempt + 1);
  }
}

3. Use batch endpoints where available

Several endpoints support batching:

  • POST /api/telesales/campaigns/{id}/leads/bulk-import — import N leads at once
  • PATCH /api/telesales/leads/bulk — update N leads at once
  • DELETE /api/telesales/leads/bulk — delete N leads at once

Batching saves rate-limit budget and runs faster than looping single calls.

4. Cache responses

Slow-changing resources (e.g. caller-ids, queues, users) — cache for 1-5 minutes on the CRM side. Don't poll every second.

5. Monitor X-RateLimit-Remaining

When Remaining < 5 — slow down proactively, don't wait until you hit 0.

Idempotency

Problem

When a client retries a POST/PATCH/DELETE due to a timeout, how do you guarantee:

  • No duplicate resources (e.g. two leads with the same phone).
  • No double charge (e.g. two calls fired from one click).
  • No double action (e.g. disposition recorded twice for the same call).

Zorio's solution: Idempotency-Key

Clients send an Idempotency-Key: <UUID v4> header on each retry-able request.

bash
curl -X POST 'https://app.zorio.vn/api/pbx/calls/click-to-call' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890' \
  -d '{"from_extension":"1001","to_phone":"0987654321"}'

How Zorio handles it

  1. First time Zorio sees the Idempotency-Key → processes the request normally and caches the response under that key (TTL 24h).
  2. Subsequent requests with the same key → return the cached response immediately, no reprocessing.

Endpoints that support Idempotency-Key

Method + endpointWhy you need it
POST /api/pbx/calls/click-to-callAvoid firing two calls on a network glitch
POST /api/telesales/campaignsAvoid creating two campaigns with the same name
POST /api/telesales/campaigns/{id}/leadsAvoid duplicate leads
POST /api/autocall/campaignsAvoid creating two campaigns with the same name
POST /api/integrations/smsAvoid sending the same SMS twice (double charge)

Idempotency-Key requirements

  • Format: UUID v4 (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx).
  • Unique per request: DO NOT reuse a key for a different request — you'll get the old response back.
  • TTL 24 hours: after 24h the key expires; a retry with the same key may then create a new resource.

Node.js integration sample

js
const { v4: uuidv4 } = require('uuid');

async function clickToCall(fromExt, toPhone) {
  const key = uuidv4();

  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      const res = await api.post('/pbx/calls/click-to-call', {
        from_extension: fromExt,
        to_phone: toPhone,
      }, {
        headers: { 'Idempotency-Key': key },
      });
      return res.data;
    } catch (err) {
      if (attempt === 2) throw err;
      await sleep(1000 * (attempt + 1));
    }
  }
}

→ Even with 3 retries, only one call is created.

Notes

  • The request body must be identical across retries (with the same Idempotency-Key). A different body → HTTP 422 with "message": "Idempotency-Key was used with a different body.".
  • Endpoints that do not support Idempotency-Key silently ignore the header — no error.

Concurrency control

Optimistic locking with If-Match

Some update endpoints support If-Match to prevent races:

PUT /api/pbx/extensions/1001
If-Match: "etag-abc123"

If the resource has changed between the client's GET and PUT → ETag mismatch → HTTP 412 { "message": "Resource has changed, please GET again." }. Your CRM must re-fetch and re-apply changes.

Connection limit

  • Max 20 concurrent connections per token.
  • Exceed → HTTP 429 with body { "message": "Too many concurrent connections." }.

Cấp phép theo điều khoản sử dụng của Zorio.