Skip to content
Numes

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.

On this page

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 headerRequiredNotes
Authorization: Bearer <key>YesAn inference key; a management key for /keys and /credits. Keys in query strings aren't accepted
Content-Type: application/jsonFor JSON bodiesMissing or different on a JSON route: 415 unsupported_media_type
X-Request-IdNoYour correlation ID, 1–128 characters of A–Z a–z 0–9 . _ -, unique per request; see Retries and request IDs
X-Session-IdNoKeeps a conversation on the same endpoint, up to 256 characters; session_id in the body takes precedence. See Sessions
HTTP-RefererNoYour app's public URL, for app attribution
X-OpenRouter-Metadata: enabledNoAdds openrouter_metadata (generation ID, request ID, outcome, provider, endpoint) to chat responses and the final stream chunk
Response headerNotes
X-Request-IdOn every response, including errors. Yours if you sent a valid one, otherwise generated
X-Generation-IdOn inference responses once output starts. Equals the chat completion id
Retry-AfterSeconds 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

Buffered
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."}]  }'
Streaming
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

requests
import jsonimport os​import requests​URL = "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:

stream.ts
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

main.go
package main​import (	"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 429 after Retry-After, 500, 502 and 503 with exponential backoff and a small attempt cap, and 408 once.

  • Don't retry 400, 401, 402, 403, 404, 413 or 415 until 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 /generation before 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.

Next steps