Skip to content

No code, no charge. Every route is measured, and failing routes are removed.

How we measure

FastOTP APIdocumentationv1

Base URL https://fastotp.io/api/v1

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.

Rate limits per API key
BudgetLimitRequests counted
Order creation30 / minutePOST /orders, including requests that end in an error
Status polls120 / minuteGET /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.

Rate-limit response headers
HeaderTypeDescription
RateLimit-LimitintegerRequests allowed per minute in the budget this request counted against.
RateLimit-RemainingintegerRequests you have left in the current minute.
RateLimit-ResetintegerSeconds until the budget is full again.
Retry-AfterintegerOnly on a 429 (and on cancel_too_early). Seconds to wait before sending the request again.

When you exceed a limit

HTTPExample response to a request over the 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:

Orders one key can poll at once, by polling interval
Polling intervalPolls per order per minuteOrders one key can watch at once
3 s206
4 s158
5 s1210

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: wait Retry-After seconds, 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 an Idempotency-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.
  • GET and DELETE: safe to repeat as they are. Reading changes nothing, and a second cancel of the same order answers not_cancellable instead 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.

Node.jsNode.js 18 or later
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 },
);
PythonPython with requests
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-Key with every POST /orders and keep it across retries.
  • On 429, wait for Retry-After.
  • On 5xx or no response, back off exponentially and cap the attempts.
  • Send max_price so a price rise is refused instead of charged.