Skip to content

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.

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

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.

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
import os, time, random
import 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.