Errors
Rejected requests return JSON problem details (application/problem+json).
{ "type": "https://api.cotes.ai/problems/unauthorized", "title": "Unauthorized", "status": 401, "detail": "Missing or invalid API key."}type is a stable https://api.cotes.ai/problems/{slug} URI — match on it rather than on detail.
Common status codes
Section titled “Common status codes”| Status | type slug |
When |
|---|---|---|
401 |
unauthorized |
Missing or invalid API key |
404 |
not-found |
Unknown ticker, or no live contest for the series |
422 |
— | Missing or empty series — see below |
429 |
rate-limited |
Throttled — see Rate limits |
429 |
too-many-streams |
Too many open streams for the key or IP — see Streaming |
5xx |
— | Transient server error — retry with backoff |
Malformed requests
Section titled “Malformed requests”A missing or empty series, and any unknown path, are rejected before the API’s own error handling and come back as plain application/json in the framework’s shape — no type, no title:
{ "detail": [{ "type": "missing", "loc": ["query", "series"], "msg": "Field required" }] }Branch on the status code and the Content-Type rather than assuming every failure carries a type.
What to retry
Section titled “What to retry”| Status | Retry? | What to do |
|---|---|---|
401 |
No | Check the key is sent as Authorization: Bearer … and hasn’t been revoked |
402 |
No | Renew the trial or subscription at app.cotes.ai, then retry |
404 on /v1/contests/active |
Later | No contest is live for that series right now; poll again in a few seconds |
404 on /v1/contests/{ticker} |
No | The ticker doesn’t exist; check the full ticker, e.g. KXBTC15M-26OCT081400-00 |
422 |
No | Fix the request: series is missing or empty |
429 |
Yes | Wait Retry-After seconds, then back off with jitter |
5xx |
Yes | Back off: 1 second, doubling to 30, with jitter |
Handling errors in code
Section titled “Handling errors in code”import os, time, randomimport requests
API = "https://api.cotes.ai"HEADERS = {"Authorization": f"Bearer {os.environ['COTES_API_KEY']}"}
def get_active(series="KXBTC15M"): delay = 1 while True: r = requests.get(f"{API}/v1/contests/active", params={"series": series}, headers=HEADERS, timeout=10) if r.ok: return r.json() problem = r.json() if r.headers.get("Content-Type", "").startswith("application/problem+json") else {} kind = problem.get("type", "").rsplit("/", 1)[-1] if r.status_code == 404 and kind == "not-found": return None # no live contest yet if r.status_code == 429 or r.status_code >= 500: wait = float(r.headers.get("Retry-After", delay)) time.sleep(wait + random.random()) delay = min(delay * 2, 30) continue raise RuntimeError(f"{r.status_code} {problem.get('detail') or r.text}")The function treats a 404 as “nothing live yet”, retries rate limits and server errors with backoff, and stops on everything else, since retrying a bad key or a malformed request fails the same way every time.
Successful responses are application/json. See the API reference for per-operation responses.