Skip to content

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": {
"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.

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

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.

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.

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.

Terminal window
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 os
import time
import 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.

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.

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).

No. Branch on error.code, which is stable. The message is a human-readable English summary and may change.