/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.Read your rate-limit budget off any response
Every Live headers on a normal
/api/v1/* response carries three headers. Read them on the response you already made — you
do not need a separate “check my quota” call.200:| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per window (100 on the sandbox). |
X-RateLimit-Remaining | Requests left in the current window. Watch this — when it hits 0, the next request is a 429. |
X-RateLimit-Reset | Unix epoch seconds when the window resets and Remaining returns to Limit. |
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 The live Phosra sends both timing headers on a
Prefer
0, then a 429. This is the exact loop that produced the response below —
request 101 was the first to be limited:429 — status line, both timing headers, and the body:429:| Header | Value | Use |
|---|---|---|
Retry-After | 60 | Seconds to wait — the simplest signal, present only on the 429. |
X-RateLimit-Reset | 1783337040 | Absolute Unix time the window resets — present on every response, so you can wait before you ever hit the limit. |
X-RateLimit-Reset (it lets you pace proactively); fall back to Retry-After when it is
absent.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
A429 is retryable; most errors are not. Retrying a deterministic 4xx just returns the same
error and burns your budget.
| Status | Retry? | Strategy |
|---|---|---|
429 | Yes | Wait until X-RateLimit-Reset (or Retry-After), then retry once. |
500 | Yes | Exponential backoff: 1s, 2s, 4s, 8s, max ~4 attempts. |
502 / 503 | Yes | Retry once after 3–5s — sandbox services cold-start. |
401 | Once | Re-sign or refresh the token, then retry exactly once. |
409 replay | No | Reuse the identical payload with the same idempotency key, or mint a fresh key. |
400 / 403 / 404 / 422 | No | Deterministic — fix the request; a retry returns the same error. |
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
| # | Step | Call | Live result |
|---|---|---|---|
| 1 | Read the budget | GET /api/v1/platforms | limit 100 · remaining 92 · reset 1783336980 |
| 2 | Exhaust the window | 130× GET /api/v1/platforms | 429 at request 101, retry-after 60 |
| 3 | Wait + retry once | fetchWithRetry(...) | 200 after the reset |
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.