Integrations
Raw HTTP
Call Numes without an SDK from curl, Python, Node.js or Go, with the headers, timeouts and retry rules a client needs.
The API is plain HTTPS and JSON, so any HTTP client works. The machine-readable contract is the OpenAPI document at /docs/openapi.json.
Request basics
Every endpoint lives under https://api.numes.ai/api/v1.
| Request header | Required | Notes |
|---|---|---|
Authorization: Bearer <key> | Yes | An inference key; a management key for /keys and /credits. Keys in query strings aren't accepted |
Content-Type: application/json | For JSON bodies | Missing or different on a JSON route: 415 unsupported_media_type |
X-Request-Id | No | Your correlation ID, 1–128 characters of A–Z a–z 0–9 . _ -, unique per request; see Retries and request IDs |
X-Session-Id | No | Keeps a conversation on the same endpoint, up to 256 characters; session_id in the body takes precedence. See Sessions |
HTTP-Referer | No | Your app's public URL, for app attribution |
X-OpenRouter-Metadata: enabled | No | Adds openrouter_metadata (generation ID, request ID, outcome, provider, endpoint) to chat responses and the final stream chunk |
| Response header | Notes |
|---|---|
X-Request-Id | On every response, including errors. Yours if you sent a valid one, otherwise generated |
X-Generation-Id | On inference responses once output starts. Equals the chat completion id |
Retry-After | Seconds to wait, on 429 rate_limited |
Body size limits depend on the endpoint; chat completions accept up to 10 MiB. See Limits.
This request block generates the same call in each language, and Try it sends it from your browser:
curl -X POST "https://api.numes.ai/api/v1/chat/completions" \ -H "Authorization: Bearer $NUMES_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-6-luna", "max_tokens": 200, "messages": [ { "role": "user", "content": "Name three uses of a message queue." } ] }'curl
curl https://api.numes.ai/api/v1/chat/completions \ -H "Authorization: Bearer $NUMES_API_KEY" \ -H "Content-Type: application/json" \ --max-time 630 \ -d '{ "model": "openai/gpt-6-luna", "messages": [{"role": "user", "content": "Name three uses of a message queue."}] }'curl -N https://api.numes.ai/api/v1/chat/completions \ -H "Authorization: Bearer $NUMES_API_KEY" \ -H "Content-Type: application/json" \ -D - \ -d '{ "model": "openai/gpt-6-luna", "stream": true, "messages": [{"role": "user", "content": "Count to five."}] }'-N disables output buffering so events print as they arrive, and -D - prints the response headers, including X-Request-Id and X-Generation-Id.
Python
import jsonimport osimport requestsURL = "https://api.numes.ai/api/v1/chat/completions"HEADERS = {"Authorization": f"Bearer {os.environ['NUMES_API_KEY']}"}body = {"model": "openai/gpt-6-luna", "messages": [{"role": "user", "content": "Count to five."}]}# Bufferedresponse = requests.post(URL, headers=HEADERS, json=body, timeout=(10, 630))response.raise_for_status()print(response.json()["choices"][0]["message"]["content"])# Streamingwith requests.post(URL, headers=HEADERS, json={**body, "stream": True}, stream=True, timeout=(10, 630)) as stream: stream.raise_for_status() for line in stream.iter_lines(decode_unicode=True): if not line or not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break chunk = json.loads(data) if "error" in chunk: raise RuntimeError(chunk["error"]["message"]) if chunk["choices"]: print(chunk["choices"][0]["delta"].get("content") or "", end="", flush=True) if "usage" in chunk: print("\ncost:", chunk["usage"]["cost"])json= sets Content-Type: application/json. Leaving the with block early closes the connection, which cancels the request.
Node.js
Node.js 18 and later include fetch:
const response = await fetch("https://api.numes.ai/api/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${process.env.NUMES_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "openai/gpt-6-luna", stream: true, messages: [{ role: "user", content: "Count to five." }], }), signal: AbortSignal.timeout(630_000),});if (!response.ok) { const { error } = await response.json(); throw new Error(`${response.status} ${error.metadata?.error_type}: ${error.message}`);}const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();let buffer = "";read: for (;;) { const { value, done } = await reader.read(); if (done) break; buffer += value; let newline: number; while ((newline = buffer.indexOf("\n")) >= 0) { const line = buffer.slice(0, newline).replace(/\r$/, ""); buffer = buffer.slice(newline + 1); if (!line.startsWith("data:")) continue; const data = line.slice(5).trim(); if (data === "[DONE]") break read; const chunk = JSON.parse(data); if (chunk.error) throw new Error(chunk.error.message); process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); if (chunk.usage) console.log(`\ncost: ${chunk.usage.cost}`); }}To cancel, pass an AbortController signal and call abort().
Go
package mainimport ( "bufio" "bytes" "context" "encoding/json" "fmt" "net/http" "os" "strings" "time")type chunk struct { Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` } `json:"choices"` Usage *struct { Cost json.Number `json:"cost"` } `json:"usage"` Error *struct { Message string `json:"message"` } `json:"error"`}func main() { body, _ := json.Marshal(map[string]any{ "model": "openai/gpt-6-luna", "stream": true, "messages": []map[string]string{{"role": "user", "content": "Count to five."}}, }) ctx, cancel := context.WithTimeout(context.Background(), 630*time.Second) defer cancel() request, _ := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.numes.ai/api/v1/chat/completions", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+os.Getenv("NUMES_API_KEY")) request.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() if response.StatusCode != http.StatusOK { panic(fmt.Sprintf("status %d, request %s", response.StatusCode, response.Header.Get("X-Request-Id"))) } scanner := bufio.NewScanner(response.Body) scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) // allow large chunks for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data:") { continue } data := strings.TrimSpace(strings.TrimPrefix(line, "data:")) if data == "[DONE]" { break } var event chunk decoder := json.NewDecoder(strings.NewReader(data)) decoder.UseNumber() if err := decoder.Decode(&event); err != nil { panic(err) } if event.Error != nil { panic(event.Error.Message) } if len(event.Choices) > 0 { fmt.Print(event.Choices[0].Delta.Content) } if event.Usage != nil { fmt.Println("\ncost:", event.Usage.Cost) } } if err := scanner.Err(); err != nil { panic(err) }}Raise the scanner's buffer: one event can carry a large tool-call argument or reasoning block, beyond the default 64 KiB. json.Number keeps money values exact.
Every example stops at data: [DONE], skips the final usage chunk's empty choices array, and treats a chunk with an error object as a failed request. Streaming documents the event format and errors after output has started.
Timeouts
Inference requests, including the whole stream, end after 10 minutes; key, model and generation requests after 10 seconds. Set a connect timeout of about 10 seconds and a request timeout slightly above 10 minutes, such as 630 seconds, so the API's own timeout error reaches you. Streams carry no keep-alive comments, and a model can think for a while before its first token, so don't use a short idle timeout between chunks. See Limits.
Retries and request IDs
Numes already falls back between endpoints of the model before any output is delivered. Retry on your side only when it's safe:
Retry
429afterRetry-After,500,502and503with exponential backoff and a small attempt cap, and408once.Don't retry
400,401,402,403,404,413or415until you've fixed the request, key, balance or limit.Don't retry a stream that already delivered output: a retry is a new request with a new charge.
If the connection dropped after you sent the request, check
GET /generationbefore retrying.
X-Request-Id is a correlation ID, not an idempotency key. Reusing an ID already recorded in your account returns 400 invalid_request, so send a fresh ID on every attempt or omit the header. Log the X-Request-Id of every failed call and quote it when you contact support. Retry strategy has complete retry examples.