X-Trader API error codes explained
Every X-Trader Public API error returns a JSON body with a stable machine-readable error.code (17 possible values), a human-readable error.message, optional field-level error.fields[], and a request_id. Clients should branch on error.code, honour Retry-After on 429 and 503, and keep X-Request-Id or request_id for support reports. The spec declares the statuses 400, 401, 403, 404, 409, 429, 500 and 503 for every operation.
Primary source: X-Trader API reference. This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader.
Error body
Section titled “Error body”{ "error": { "code": "validation_error", "message": "Request validation failed", "fields": [ { "path": "config.buy_amount", "reason": "invalid_string" } ] }, "request_id": "req_0123456789abcdef"}The values above are illustrative; the spec defines the shape, not concrete messages or reasons.
| Field | Type | Required | Meaning |
|---|---|---|---|
error.code |
enum string | yes | Stable code; branch on this |
error.message |
string | yes | English summary for humans; do not parse |
error.fields |
array, max 20 | no | Field-level validation failures |
error.fields[].path |
string | yes (per item) | Dot-separated JSON path; $ is the whole request body |
error.fields[].reason |
string | yes (per item) | Stable reason produced by the request schema |
request_id |
string | yes | Request identifier for support |
The error object is strict and never contains database or internal implementation details.
Headers on errors
Section titled “Headers on errors”| Header | When | Use |
|---|---|---|
Retry-After |
429 and 503 responses |
Minimum wait before retrying |
X-Request-Id |
Every response (or request_id in the body) |
Quote it when reporting a problem |
Status mapping
Section titled “Status mapping”The spec uses one shared ErrorResponse schema for all error statuses and does not publish a per-code status table or response examples. The status column below is therefore documented only where the spec ties a code to a status (404 not_found, and the Retry-After statuses 429 and 503). The remaining assignments are inferred from the declared status set and standard HTTP semantics, and are marked “inferred”. Handle errors by error.code, not by status.
All error codes
Section titled “All error codes”error.code |
HTTP status | What triggers it | What to do |
|---|---|---|---|
invalid_request |
400 (inferred) | Malformed request that is not a schema field error, for example unparseable JSON, a body over 64 KiB, an invalid query or header value. The spec does not enumerate triggers. | Fix the request; do not retry unchanged. |
invalid_api_token |
401 (inferred) | The bearer token is missing, malformed or not accepted. A token replaced by rotation or revoked is presumably reported this way; the spec does not say so explicitly. | Check the Authorization: Bearer xt_live_<id>.<secret> header; rotate or create a token if needed. |
insufficient_scope |
403 (inferred) | The token lacks the operation’s required scope, for example sniper:delete for DELETE /sniper/configs/{id}. |
Call GET /me to see scopes; issue a token with the needed scope. |
ip_not_allowed |
403 (inferred) | The request comes from an address outside the token’s IP allowlist. | Call from an allowlisted address or update the allowlist in the web app. |
rate_limit_exceeded |
429 (inferred from Retry-After) |
Too many requests. The spec does not publish the limits. | Wait for Retry-After, then retry with backoff. |
validation_error |
400 (inferred) | Body fails the strict schema: unknown property, wrong type, JSON number instead of decimal string, more than 12 fractional digits, value out of range, forbidden field such as user_id, version, template or (on templates) risk_label_id, empty PATCH. A failed revalidation on resume is probably reported the same way; the spec does not name that code. |
Read error.fields[] for path and reason, fix the body. |
not_found |
404 | Resource does not exist or belongs to another account; also a foreign or missing risk_label_id in a body or label_id filter. |
Verify the id, the product path and that the token belongs to the owning account. |
target_already_exists |
409 (inferred) | The spec does not describe the trigger. The name suggests a config for the same target_wallet already exists in that product for this account. |
List configs with q=<wallet> and update the existing one instead of creating another. |
template_name_exists |
409 (inferred) | The spec does not describe the trigger. The name suggests a template name collision when creating or renaming a template. | Choose a different name. |
label_name_exists |
409 (inferred) | Creating or renaming a label to a name already used in the account (names are unique per account). | Choose a different name or reuse the existing label. |
quota_exceeded |
409 (inferred) | A limit is reached: the shared config quota (quota.max in GET /me), the 1,000-template limit, or the 21st label. |
Delete unused resources, or check GET /me before creating. |
config_must_be_paused |
409 (inferred) | Changing config or linked_wallets, or deleting, while the config is active. |
POST /{id}/pause, perform the change, then resume. |
version_conflict |
409 (inferred) | If-Match does not match the current version. |
GET the resource, re-evaluate, resend with the new ETag. |
idempotency_required |
400 (inferred) | A create request without an Idempotency-Key header. |
Add a key of 8-128 characters from A-Za-z0-9._:-. |
idempotency_conflict |
409 (inferred) | The same Idempotency-Key was reused within 24 hours with a different body. |
Resend the original body, or use a new key for a new resource. |
service_unavailable |
503 (inferred from Retry-After) |
The service is temporarily unable to handle the request. | Wait for Retry-After; retry only safe operations. |
internal_error |
500 (inferred) | Unexpected server-side failure. | Retry cautiously if the operation is safe to repeat; report with request_id. |
The 400/409 split for the last group is the least certain part of the table: the spec lists 409 for every operation, which fits the conflict-type codes, but it does not confirm any individual assignment.
Retry policy
Section titled “Retry policy”| Situation | Retry? |
|---|---|
rate_limit_exceeded, service_unavailable |
Yes, after Retry-After, with backoff |
internal_error |
Only if repeating is safe |
Create with the same Idempotency-Key and body |
Safe to retry within 24 hours |
PATCH, pause, resume, delete |
Safe to resend with the same If-Match: if the first attempt succeeded, the retry fails with version_conflict instead of applying twice |
validation_error, invalid_request, not_found, insufficient_scope, ip_not_allowed, *_exists, quota_exceeded |
No, fix the cause first |
Practical guidance: a version_conflict on a retried write may mean your own previous attempt succeeded. GET the resource to check before doing anything else.
Example: handling errors
Section titled “Example: handling errors”curl --include --request DELETE "https://x-trader.ai/api/v1/sniper/configs/$CONFIG_ID" \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'If-Match: "v4"'import osimport timeimport requests
BASE = "https://x-trader.ai/api/v1"H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"}
def call(method, path, retries=3, headers=None, **kw): hdrs = {**H, **(headers or {})} for attempt in range(retries + 1): r = requests.request(method, f"{BASE}{path}", headers=hdrs, timeout=15, **kw) if r.ok: return r err = r.json().get("error", {}) code = err.get("code") if code in ("rate_limit_exceeded", "service_unavailable") and attempt < retries: time.sleep(float(r.headers.get("Retry-After", 2 ** attempt))) continue rid = r.headers.get("X-Request-Id") or r.json().get("request_id") for f in err.get("fields", []): print("field", f["path"], f["reason"]) raise RuntimeError(f"{r.status_code} {code}: {err.get('message')} (request {rid})")
me = call("GET", "/me").json()["data"]Why do I get 404 not_found for my own config id?
Section titled “Why do I get 404 not_found for my own config id?”Check that the token belongs to the same X-Trader account that owns the config and that the id is sent to the right product path (a copy-trade id under /sniper/configs/ is not found). Ids owned by another account, and foreign label ids in risk_label_id or label_id, return the same 404 not_found as missing ids, so the API never reveals whether they exist.
What does version_conflict after PATCH mean?
Section titled “What does version_conflict after PATCH mean?”The If-Match value is older than the stored version because the resource changed since you read it, possibly by your own earlier retry or by the web UI. GET it again, review the current state, then resend with the new ETag if the change still applies.
What is config_must_be_paused?
Section titled “What is config_must_be_paused?”The request needs a paused config: changing config or linked_wallets, or deleting. Pause it with POST /{id}/pause, make the change, then resume if needed. remark and risk_label_id can be changed while active.
How do I handle rate_limit_exceeded?
Section titled “How do I handle rate_limit_exceeded?”Wait for the number of seconds in the Retry-After header before retrying, and add backoff. Retry writes only when they are safe to repeat (same Idempotency-Key, or the same If-Match).
Should I parse the error message text?
Section titled “Should I parse the error message text?”No. Branch on error.code, which is stable. The message is a human-readable English summary and may change.