Skip to main content
Examples

Errors & rate limits

Every status code a generated API returns, the error body shape, and how per-key and per-endpoint rate limits work (429 with Retry-After) - with retry examples.

Error body

Errors that have a body return an error message, plus the constraint, column or query parameter involved when there is one:

JSON
{ "error": "Invalid API key." }
{ "error": "Column 'title' is required.", "column": "title" }

Some responses have no body: 404 for a missing row or unknown route, and 405. Check the status code first, then read error if there is a body.

Status codes

Status When Body
200 Read succeeded the row or array of rows
201 Row created {"id": <new key>}
204 Row updated or deleted empty
400 Malformed request: body not valid JSON or not an object, unknown filter / sort / fields column or bad count (parameter), PATCH with an empty body or a column it cannot change, malformed composite key, PUT with nothing to update error
401 Missing, unknown, revoked or expired credential; private OpenAPI document without a key error
403 IP not allowed, or role too low for the method error
404 No row with that key, no such route, or a disabled / deleted endpoint empty
405 Method not routed (e.g. POST to a row URL) empty, Allow header
409 Duplicate key or unique value; DELETE of a row that is still referenced error, constraint
413 Request body over 1 MB error
422 A value the column cannot take: wrong type, required column missing, reference to a missing row, check constraint, too long / out of range error, column or constraint
429 Caller quota, address ceiling or concurrent token / certificate check limit exceeded error, Retry-After header
503 The token issuer's signing keys could not be fetched, or credential configuration is too stale error, Retry-After header
500 Any other database or server error error

400 means the request itself is malformed; 409 and 422 mean it was well-formed but the data conflicts with what is stored or with the column's rules. The full messages are listed on Authentication and Create, update & delete.

Rate limits

Every authenticated caller has a per-minute request quota on the API:

Caller Counted per Quota set in
API key key the key, under Auth & Keys
JWT token subject (sub); tokens without one, per client IP address Auth & Keys → JWT Bearer → Rate limit, default 60
Client certificate (mTLS) certificate (SHA-256 fingerprint) Auth & Keys → Mutual TLS → Rate limit, default 60

Quotas are capped at your plan's per-key limit. An endpoint can also have its own per-minute limit (Endpoints page), counted per caller on that route, and on Public endpoints per client IP address. Anonymous requests to a Public endpoint also count toward the address ceiling described below.

Every request counts, including failed ones after authentication. Each response to a limited request says where the caller stands:

HTTP
HTTP/1.1 200
Content-Type: application/json
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 1
X-RateLimit-Reset: 60

When the quota is used up, the API answers 429 until the window resets:

HTTP
HTTP/1.1 429
Content-Type: application/json; charset=utf-8
Retry-After: 60
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60

{"error":"Rate limit exceeded. Try again later."}
  • The window is a fixed one-minute window that starts with the caller's first request in it. X-RateLimit-Reset and Retry-After are the seconds until it resets, rounded up.
  • When both the caller's quota and the endpoint's limit apply, both are counted and the first one used up answers 429. The headers show the one closest to running out.
  • The counters are shared by all instances of your API. If that shared store is briefly unreachable, each instance counts on its own until it is back, so a caller can get more requests through during that time; requests are never rejected because of it.

Failed authentication and floods

Traffic that does not authenticate is counted per client address: an IPv4 address, or an IPv6 /64, with larger allowances for the surrounding /56 and /48. Once the address group is over this ceiling, JWTs and uncached client certificates receive 429 before signature or certificate-chain validation, even if the credential is valid. They can be retried after the window ends. API keys and cached certificate results bypass this validation gate; authenticated requests still have their own caller and endpoint quotas.

Limit Counts Default Then
Unauthenticated requests requests with no credential, a rejected credential, or anonymous calls to Public endpoints and public documentation 600 per minute per address 429 until the minute ends
Concurrent token / certificate checks JWT signature checks and new client certificates being verified at the same time 4 per IPv4 address or IPv6 /64; 16 per /56 and 64 per /48 429 with Retry-After: 1
HTTP
HTTP/1.1 429
Content-Type: application/json; charset=utf-8
Retry-After: 41

{"error":"Too many unauthenticated requests from this address. Try again later."}
  • Shared addresses. Valid API keys, including new, rotated and idle keys, and cached certificates keep working when another caller exceeds the address ceiling. JWTs and uncached certificates wait until that window ends. If all validation slots are occupied, these expensive credentials receive 429 with Retry-After: 1 until a slot is free. Without a trusted proxy, the address ceiling is disabled and one runtime-wide cap of 32 validations applies.
  • One credential per request. If a request carries several (an API key, a bearer token and a client certificate), only the first is checked, in that order, among the methods the endpoint accepts. The others are ignored, even when the first is rejected: a stale X-API-Key next to a valid bearer token is a 401. Send one credential.
  • Changes take effect within 30 seconds. A new key works at once (or within 10 seconds); a revoked key and changed endpoint, method or mTLS settings take effect within 30 seconds; a published or unpublished documentation portal too. An expired key, token or certificate is refused at its expiry. If configuration refreshes keep failing, credential checks and documentation return 503 after ten refresh intervals (5 minutes by default), until a full refresh succeeds.
  • A bearer token is only checked when its issuer is the configured one and, with a JWKS, its kid, when present, is one of the issuer's current keys. A token without kid is checked against the issuer's current keys under the same validation limits. When the issuer's keys cannot be fetched, the answer is 503 with Retry-After, not 401: retry later.
  • 429 from these limits has no X-RateLimit-* headers; retry after Retry-After.

Retrying

Retry 429 after Retry-After, and network failures or 502/503/504 with backoff. Do not retry other 4xx responses or a 500 from the database: they fail the same way every time.

# curl retries 429 and 5xx, honouring Retry-After
curl -s --retry 3 --retry-max-time 200 \
  "$RESTWARD_API_URL/api/public/artist?limit=3" -H "X-API-Key: $RESTWARD_API_KEY"
async function getWithRetry(url, init, attempts = 3) {
  for (let i = 1; ; i++) {
    const res = await fetch(url, init);
    if (res.status !== 429 || i === attempts) return res;
    const seconds = Number(res.headers.get("Retry-After") ?? 60);
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}
import time

def get_with_retry(session, url, attempts=3, **kwargs):
    for i in range(1, attempts + 1):
        res = session.get(url, **kwargs)
        if res.status_code != 429 or i == attempts:
            return res
        time.sleep(int(res.headers.get("Retry-After", 60)))
async Task<HttpResponseMessage> GetWithRetryAsync(HttpClient http, string url, int attempts = 3)
{
    for (var i = 1; ; i++)
    {
        var res = await http.GetAsync(url);
        if (res.StatusCode != HttpStatusCode.TooManyRequests || i == attempts) return res;
        await Task.Delay(res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(60));
    }
}

Handling errors

status=$(curl -s -o body.json -w '%{http_code}' \
  "$RESTWARD_API_URL/api/public/artist/abc" -H "X-API-Key: $RESTWARD_API_KEY")
[ "$status" -ge 400 ] && echo "HTTP $status: $(cat body.json)"
# HTTP 422: {"error":"Value 'abc' is not valid for column 'artist_id' of type 'integer'.","column":"artist_id"}
const res = await fetch(`${process.env.RESTWARD_API_URL}/api/public/artist/abc`, { headers });
if (!res.ok) {
  const text = await res.text();
  const message = text ? JSON.parse(text).error : res.statusText;
  throw new Error(`HTTP ${res.status}: ${message}`);
}
res = session.get(f"{api}/api/public/artist/abc")
if not res.ok:
    message = res.json().get("error") if res.content else res.reason
    raise RuntimeError(f"HTTP {res.status_code}: {message}")
var res = await http.GetAsync("/api/public/artist/abc");
if (!res.IsSuccessStatusCode)
{
    var text = await res.Content.ReadAsStringAsync();
    var message = text.Length > 0 ? JsonDocument.Parse(text).RootElement.GetProperty("error").GetString() : res.ReasonPhrase;
    throw new HttpRequestException($"HTTP {(int)res.StatusCode}: {message}");
}

Reconnecting to server...

Please wait while we restore your connection

Connection lost

Retrying in seconds...

Unable to reconnect

The server connection has been lost.
You may need to log in again.

Session Paused

The server has temporarily paused your session.

Session Expired

Your session could not be resumed.
Please reload the page to continue.