API documentation ยท 4 of 4
Limits: rate limits, timeouts and safe retries
Each API key may create 30 orders and make 120 status polls per minute; the rest of this page is how to stay inside those two budgets and retry without ever buying a number twice.
On this page
Rate limits
Two budgets are counted separately for each API key, each over one minute.
| Budget | Limit | Requests counted |
|---|---|---|
| Order creation | 30 / minute | POST /orders, including requests that end in an error |
| Status polls | 120 / minute | GET /orders/{id} |
A request over the limit is refused with 429 and the code rate_limited. It is not queued and it costs nothing. Any endpoint can answer 429, so handle it the same way everywhere.
Rate-limit headers
Responses to the two counted requests carry three headers that tell you where you stand, so you can slow down before you are refused.
| Header | Type | Description |
|---|---|---|
| RateLimit-Limit | integer | Requests allowed per minute in the budget this request counted against. |
| RateLimit-Remaining | integer | Requests you have left in the current minute. |
| RateLimit-Reset | integer | Seconds until the budget is full again. |
| Retry-After | integer | Only on a 429 (and on cancel_too_early). Seconds to wait before sending the request again. |
When you exceed a limit
HTTP/1.1 429 Too Many Requests
Content-Type: application/json; charset=utf-8
Retry-After: 12
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 12
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Try again in 12 seconds."
}
}Wait the number of seconds in Retry-After, then continue. Do not retry sooner: refused requests do not shorten the wait.
Polling cadence
Poll an active order every 3 to 5 seconds. Faster polling does not make a code arrive sooner; it only spends the budget. The interval you choose decides how many orders one key can watch at the same time:
| Polling interval | Polls per order per minute | Orders one key can watch at once |
|---|---|---|
| 3 s | 20 | 6 |
| 4 s | 15 | 8 |
| 5 s | 12 | 10 |
If you run more orders in parallel than that, lengthen the interval. Stop polling an order the moment its status is no longer active; a finished order never changes again. The orders guide explains what each status means.
Timeouts
POST /orders: at least 30 seconds. The call reads the live price and obtains a number before it answers. A shorter timeout makes your client give up on orders that were about to succeed.- Every other request: around 10 seconds is a sensible client timeout.
Retrying safely
429: waitRetry-Afterseconds, then send the same request.- 5xx, timeouts and dropped connections: retry with exponential backoff, for example 1, 2, 4 and 8 seconds with some random jitter, and give up after a fixed number of attempts.
POST /orders: always send anIdempotency-Key, and reuse the same one on every retry of that purchase. Within 24 hours it returns the original order instead of creating another. See how idempotency keys work.GETandDELETE: safe to repeat as they are. Reading changes nothing, and a second cancel of the same order answersnot_cancellableinstead of refunding twice.- Other 4xx: do not retry unchanged. The error reference says which errors are safe to retry.
Backoff example code
One helper that applies the rules above: it honours Retry-After, backs off exponentially with jitter on 5xx and network failures, and stops after five attempts.
import { randomUUID } from "node:crypto";
import { setTimeout as sleep } from "node:timers/promises";
const BASE = "https://fastotp.io/api/v1";
const headers = { Authorization: `Bearer ${process.env.FASTOTP_KEY}` };
// Retries 429, 5xx, timeouts and network errors. Any other response goes
// straight back to the caller, because sending it again would fail the same way.
async function request(url, init = {}, { attempts = 5, timeoutMs = 10_000 } = {}) {
for (let attempt = 1; ; attempt++) {
let res = null;
try {
res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
if (res.status !== 429 && res.status < 500) return res;
} catch (err) {
if (attempt === attempts) throw err; // timeout or network error, out of attempts
}
if (attempt === attempts) return res; // still failing: let the caller see it
// Wait as long as the server asks. Otherwise 1 s, 2 s, 4 s, 8 s, capped at
// 30 s, with jitter so parallel workers do not all come back at once.
const asked = Number(res?.headers.get("retry-after"));
const backoff = Math.min(30, 2 ** (attempt - 1));
await sleep(1000 * (asked > 0 ? asked : backoff / 2 + (Math.random() * backoff) / 2));
}
}
// Creating an order: a 30-second timeout, and one Idempotency-Key that
// stays the same for every attempt made inside request().
const res = await request(
`${BASE}/orders`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
body: JSON.stringify({ service: "whatsapp", country: "indonesia", max_price: "0.15" }),
},
{ timeoutMs: 30_000 },
);import os
import random
import time
import uuid
import requests
BASE = "https://fastotp.io/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['FASTOTP_KEY']}"
def request(method, url, attempts=5, timeout=10, **kwargs):
"""Retries 429, 5xx, timeouts and network errors.
Any other response goes straight back to the caller, because sending it
again would fail the same way.
"""
for attempt in range(1, attempts + 1):
response = None
try:
response = session.request(method, url, timeout=timeout, **kwargs)
if response.status_code != 429 and response.status_code < 500:
return response
except requests.RequestException:
if attempt == attempts:
raise # timeout or network error, out of attempts
if attempt == attempts:
return response # still failing: let the caller see it
# Wait as long as the server asks. Otherwise 1 s, 2 s, 4 s, 8 s, capped at
# 30 s, with jitter so parallel workers do not all come back at once.
asked = int(response.headers.get("Retry-After", 0)) if response is not None else 0
backoff = min(30, 2 ** (attempt - 1))
time.sleep(asked or random.uniform(backoff / 2, backoff))
# Creating an order: a 30-second timeout, and one Idempotency-Key that
# stays the same for every attempt made inside request().
response = request(
"POST",
f"{BASE}/orders",
timeout=30,
headers={"Idempotency-Key": str(uuid.uuid4())},
json={"service": "whatsapp", "country": "indonesia", "max_price": "0.15"},
)Checklist
- Poll each active order every 3 to 5 seconds, and stop when it ends.
- Set a client timeout of at least 30 seconds on
POST /orders. - Send an
Idempotency-Keywith everyPOST /ordersand keep it across retries. - On
429, wait forRetry-After. - On 5xx or no response, back off exponentially and cap the attempts.
- Send
max_priceso a price rise is refused instead of charged.