Skip to main content
Every busy integration eventually hits a rate limit. Handling it well is the difference between a brief pause and a cascade of failed requests. This recipe reads the live rate-limit headers Phosra puts on every /api/v1/* response, drives the sandbox into a real 429, and wraps your calls in a retry that waits exactly as long as the server tells it to — no guessing, no thundering herd. Every request and response below is verbatim live output, captured against https://phosra-api-sandbox-production.up.railway.app. No API key, nothing to install — the sandbox rate-limits anonymous callers on the same headers production uses.
Sandbox-first. These calls run against the open sandbox with no credential. The headers and 429 shape are identical in production: swap the base URL for https://prodapi.phosra.com and add a phosra_live_… key. Only the X-RateLimit-Limit value differs (the sandbox window is 100).

Before you start

The unauthenticated discovery reads — the Trust List, /.well-known/ocss/*, editions — are unmetered and carry no rate-limit headers. Only the /api/v1/* surface is counted. This recipe uses /api/v1/platforms because it needs no body.
1

Read your rate-limit budget off any response

Every /api/v1/* response carries three headers. Read them on the response you already made — you do not need a separate “check my quota” call.
Live headers on a normal 200:
2

Drive a real 429

Exhaust the window and the very next request is rejected. Loop past the limit and you will see the counter fall to 0, then a 429. This is the exact loop that produced the response below — request 101 was the first to be limited:
The live 429 — status line, both timing headers, and the body:
The 429 body is the plain-text string Too Many Requests, not JSON. Do not JSON.parse a 429 — branch on the status code and read the headers. (Every other error class — 400, 401, 403, 404, 409, 422 — returns a JSON body; 429 and 5xx are the exceptions.)
Phosra sends both timing headers on a 429:Prefer X-RateLimit-Reset (it lets you pace proactively); fall back to Retry-After when it is absent.
3

Wrap every call in a reset-aware retry

Put it together: on a 429, sleep until the reset the server named, then retry once. This is the whole contract — a 429 is never a failure you surface to the user, it is a “wait and try again.”
Retry once, not forever. After one wait-and-retry the window has reset, so a second 429 means you are sending faster than the limit sustains — throttle your own concurrency instead of looping. The right ceiling is a bounded retry (1–2 attempts), never an unbounded while.

When to retry — the full table

A 429 is retryable; most errors are not. Retrying a deterministic 4xx just returns the same error and burns your budget.
Async jobs (a 202 or a running enforcement job) are polled, not retried — poll the job endpoint named in the response rather than re-POSTing the trigger. See Change a rule and enforce it for the polling loop.

The whole flow at a glance

Next steps

Error reference

Every status, its class, and whether it is retryable — the full contract behind this table.

Rotate a compromised key

A 401 after a leak is a “rotate, then retry once” — the credential-rotation companion to this recipe.

Change a rule and enforce it

How async enforcement jobs are polled — the case where you never retry the trigger.

Recover a disconnected platform

When retries won’t help because the platform link itself is gone.