Home / Blog / Guides

Guides

FOMO API Errors, Retries, Rate Limits, and Credits

A production error policy for FOMO API clients, including credit headers, bounded exponential backoff, Retry-After, and non-retryable failures.

Pixel-art API requests moving through security, rate-limit, retry, credit, and protected endpoint checkpoints.

A reliable FOMO API client does not retry every non-200 response. It separates authentication, exhausted credits, missing data, rate limits, and temporary upstream failures, then takes a different action for each one.

The error policy in one table

Status Meaning Default action
400 Invalid input, such as an unsupported leaderboard window Fix the request; do not retry unchanged
401 Missing or invalid API key Stop and replace the credential
402 Credit balance exhausted Stop and upgrade or top up
404 Resource or endpoint not found Verify the identifier and route
409 Resource state is not ready on routes that mark it retryable Retry only when the body says retryable: true
429 Rate limited Wait for Retry-After, then retry with jitter
500–504 Temporary service or dependency failure Retry a small number of times with backoff

This policy is intentionally conservative. Retrying an invalid key or an exhausted balance cannot succeed, while aggressive retries during an outage can turn one failed request into a self-inflicted traffic spike.

The current status definitions and error bodies live in the API documentation. Do not hard-code error text when your client can branch on the HTTP status and structured fields.

Read the body without losing the status

Network libraries often throw away useful context when a response is not successful. Preserve both:

type ApiFailure = {
  error?: string;
  message?: string;
  retryable?: boolean;
  remaining?: number;
  upgradeUrl?: string;
};

class FomoApiError extends Error {
  constructor(
    readonly status: number,
    readonly details: ApiFailure,
    readonly retryAfterMs: number | null,
  ) {
    super(details.message || details.error || `HTTP ${status}`);
  }
}

async function readFailure(response: Response): Promise<FomoApiError> {
  const details = await response.json().catch(() => ({})) as ApiFailure;
  const retryAfter = response.headers.get("retry-after");
  const seconds = retryAfter === null ? NaN : Number(retryAfter);
  return new FomoApiError(
    response.status,
    details,
    Number.isFinite(seconds) ? Math.max(0, seconds * 1000) : null,
  );
}

Keep the raw body out of public error messages. It is useful in structured server logs, but a user-facing application should translate it into an action the user can take.

Use bounded exponential backoff

The following helper retries GET requests on 429, on a response explicitly marked retryable, and on common transient 5xx statuses. It caps both attempts and delay:

const API_ORIGIN = "https://api.fomoapi.io";
const TRANSIENT = new Set([500, 502, 503, 504]);

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

function backoff(attempt: number) {
  const ceiling = Math.min(8000, 500 * 2 ** attempt);
  return Math.round(ceiling * (0.5 + Math.random() * 0.5));
}

async function getWithRetry<T>(path: string, apiKey: string): Promise<T> {
  const maxAttempts = 4;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    let response: Response;
    try {
      response = await fetch(new URL(path, API_ORIGIN), {
        headers: { authorization: `Bearer ${apiKey}` },
        signal: AbortSignal.timeout(15_000),
      });
    } catch (error) {
      if (attempt === maxAttempts - 1) throw error;
      await sleep(backoff(attempt));
      continue;
    }

    if (response.ok) return await response.json() as T;

    const failure = await readFailure(response);
    const canRetry = response.status === 429
      || TRANSIENT.has(response.status)
      || failure.details.retryable === true;

    if (!canRetry || attempt === maxAttempts - 1) throw failure;
    await sleep(failure.retryAfterMs ?? backoff(attempt));
  }

  throw new Error("unreachable");
}

Jitter matters because many clients otherwise retry at the same 1, 2, and 4 second boundaries. The cap prevents a background job from occupying a worker forever.

Treat 401 and 402 as configuration states

A 401 means the credential is missing or invalid. Disable the integration, alert its owner, and wait for a new key. Do not include the failed key in logs or notifications.

A 402 means the key is valid but has no credits available. The response can include the current plan, remaining balance, and an upgrade URL. Retrying the same call cannot refill the balance. Surface the billing action and stop dependent jobs until the balance changes.

Current plans and top-up paths belong on the pricing page, not in long-lived client constants.

Record credit headers on successful calls

Authenticated responses report x-credits-cost and x-credits-remaining. Capture both beside the endpoint and request duration:

function creditTelemetry(response: Response) {
  const number = (name: string) => {
    const value = response.headers.get(name);
    return value === null ? null : Number(value);
  };

  return {
    cost: number("x-credits-cost"),
    remaining: number("x-credits-remaining"),
    unmetered: response.headers.get("x-credits-unmetered") === "1",
  };
}

An alert on a low remaining balance is more useful than discovering the problem through the first production 402. Do not combine credits with request counts: endpoints can have different costs.

Preserve incomplete and retryable data states

Some successful responses can still be partial. Trade-history reads can identify incomplete source coverage, and slower live reads can return a structured retryable failure instead of pretending a trader does not exist.

Your data model should preserve fields such as partial, complete, available, and retryable. Converting every non-final state into an empty array makes “nothing exists” indistinguishable from “the source did not finish.”

For user-facing screens:

  • show cached or partial data with an honest status;
  • provide a retry action for explicitly retryable states;
  • do not turn a temporary 503 into a permanent “not found” result;
  • keep the last known good result during a short dependency failure when that is safe.

Avoid the common retry mistakes

  1. Do not retry 401, 402, or 400 without changing something.
  2. Do not retry forever. Bound attempts and total elapsed time.
  3. Do not ignore Retry-After.
  4. Do not run independent retry loops at the HTTP client, job worker, and queue layers without a shared budget.
  5. Do not log Bearer keys or WebSocket URLs containing keys.
  6. Do not label a request successful merely because it returned JSON.
  7. Do not assume a credit allowance equals a fixed number of calls.

The TypeScript quickstart provides the smaller request wrapper. Add this retry policy when the integration moves from an experiment to a worker, backend, or customer-facing product.

Sources

  1. FOMO API OpenAPI 3.1 specification FOMO API, accessed 2026-09-17
  2. FOMO API documentation FOMO API, accessed 2026-09-17

Ship on fomo.family trader data

Both-chain wallets, PnL and holdings, plus two live streams: the app feed on every plan, the on-chain stream on Growth. One API.

Get an API key

FAQ

Should a client retry a 402 response?
No. A 402 means the key has exhausted its available credits. Retrying spends time without changing the outcome. Stop the operation and direct the account owner to the pricing or credit top-up flow.
Which FOMO API failures are normally retryable?
Retry bounded 429 and transient 5xx responses, respecting Retry-After when present. Some JSON errors explicitly include retryable: true. Invalid credentials, invalid requests, exhausted credits, and ordinary not-found responses require a state change rather than a retry.
How can an application monitor credit usage?
Read x-credits-cost and x-credits-remaining from authenticated responses. Record them as request telemetry and alert before the remaining balance can interrupt a production workflow.