Errors and rate limits
Match on code, never on message. Codes are stable and part of the contract; messages are written for humans and get reworded.
Error shape
→ 422 Unprocessable Entity
{
"error": {
"code": "invalid_address",
"message": "Not a valid Tron address for the named chain.",
"field": "address",
"request_id": "req_5Hn2…",
"docs": "https://fin-techai.com/docs/errors#invalid_address"
}
}
Every error carries a request_id. Quote it in a support request and we can see the exact call, including which node served it.
Status and error codes
| HTTP | code | Meaning and what to do |
|---|---|---|
| 400 | malformed_request | Body is not valid JSON, or a required field is missing. Not retryable. |
| 401 | invalid_key | Key unknown, revoked, or wrong environment — a test key on the production host lands here deliberately. |
| 403 | scope_insufficient | Key is valid but not scoped to this endpoint. Fix the key, do not retry. |
| 403 | ip_not_allowed | Source IP outside the key's CIDR allowlist. Logged with its source. |
| 404 | not_found | Resource does not exist, or belongs to another account. We do not distinguish the two on purpose. |
| 409 | idempotency_conflict | Same Idempotency-Key, different body. Change the key or send the original body. |
| 410 | quote_expired | Quote passed quote_expires_at. Re-quote — we will not silently re-price. |
| 422 | invalid_address | Address fails validation for the named chain. Check chain and address agree. |
| 422 | chain_unsupported | Chain not covered for this module. See integrations. |
| 422 | thin_history | Not enough on-chain history for a credit profile. Treat as unknown, not as bad. |
| 429 | rate_limited | Over your limit. Honour Retry-After. Never billed. |
| 503 | coverage_degraded | The chain you asked about is materially behind head and we would rather fail than serve a stale score silently. Retryable. |
| 500 / 502 / 504 | internal_error | Ours. Retry with jittered backoff. Never billed. |
Rate limits
Limits are per account, not per key — splitting traffic across keys to exceed a sustained limit is a breach of the API terms, not a clever workaround. Every response carries the current state.
X-FTAI-RateLimit-Limit: 600 // sustained, per minute X-FTAI-RateLimit-Remaining: 574 X-FTAI-RateLimit-Reset: 1785363560 // unix seconds Retry-After: 12 // on 429 only
| Plan | Sustained / min | Burst | Batch rows / day |
|---|---|---|---|
| Sandbox | 600 | 60 / s | 10,000 |
| Scale | 6,000 | 300 / s | 500,000 |
| Enterprise | Contracted | Contracted | Contracted |
Illustrative figures — your contracted limits are in your order form. Batch endpoints have their own daily row allowance and do not consume the per-minute limit.
A correct retry policy
// retry: 429, 503, and 5xx. Never 4xx.
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
async function call(fn, attempt = 0) {
const res = await fn();
if (res.ok || !RETRYABLE.has(res.status) || attempt >= 5) return res;
const after = Number(res.headers.get('Retry-After'));
const wait = after
? after * 1000
// exponential with full jitter — not a fixed backoff
: Math.random() * Math.min(30000, 2 ** attempt * 1000);
await new Promise(r => setTimeout(r, wait));
return call(fn, attempt + 1);
}
- Honour
Retry-Afterwhen present. It is not a suggestion, and ignoring it extends your own throttle. - Use full jitter. A fleet retrying on a fixed schedule reconverges into the same spike that caused the 429.
- Never retry a 4xx. Nothing about the request will have changed.
- On
route/execute, always retry with the sameIdempotency-Key. A new key on a retry is how a double-send happens.