# Raw HTTP

> Call Numes without an SDK from curl, Python, Node.js or Go, with the headers, timeouts and retry rules a client needs.

Source: https://0.0.0.0:3000/docs/integrations/http
Documentation index: https://0.0.0.0:3000/docs/llms.txt

The API is plain HTTPS and JSON, so any HTTP client works. The machine-readable contract is the OpenAPI document at [`/docs/openapi.json`](/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](#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](/docs/features/sessions) |
| `HTTP-Referer` | No | Your app's public URL, for [app attribution](/docs/account/activity#attribute-traffic-to-your-apps) |
| `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](/docs/api-reference/limits).

This request block generates the same call in each language, and **Try it** sends it from your browser:

```request
{
  "method": "POST",
  "path": "/chat/completions",
  "body": {
    "model": "openai/gpt-6-luna",
    "max_tokens": 200,
    "messages": [{ "role": "user", "content": "Name three uses of a message queue." }]
  }
}
```

## curl

```bash title="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."}]
  }'
```

```bash title="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

```python title="requests"
import json
import 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."}]}

# Buffered
response = requests.post(URL, headers=HEADERS, json=body, timeout=(10, 630))
response.raise_for_status()
print(response.json()["choices"][0]["message"]["content"])

# Streaming
with 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`:

```typescript title="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

```go title="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](/docs/features/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](/docs/api-reference/limits#timeouts).

## 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`](/docs/api-reference/chat/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](/docs/api-reference/errors#retry-strategy) has complete retry examples.

## Next steps

:::cards
- {waves} [Streaming](/docs/features/streaming) Chunk format, reasoning deltas and cancellation.
- {alert} [Errors](/docs/api-reference/errors) Every status and error type.
- {gauge} [Limits](/docs/api-reference/limits) Body sizes, rate limits and timeouts.
- {code} [OpenAI SDK](/docs/integrations/openai-sdk) When you prefer an SDK.
- {receipt} [Usage accounting](/docs/account/usage-accounting) The generation record and exact costs.
:::
