Horizon API docs
An OpenAI-compatible API. If a library can talk to OpenAI, it can talk to Horizon.
Quickstart
- Sign in with Hack Club. New accounts start with zero quota until an admin approves you.
- Open Keys and create a key. It looks like
sk-...and is shown only once. - Use the base URL below with any OpenAI client.
| Setting | Value |
|---|---|
| Base URL | https://horizon-api.dino.icu/v1 |
| Chat completions | https://horizon-api.dino.icu/v1/chat/completions |
| Model list | https://horizon-api.dino.icu/v1/models |
| Auth header | Authorization: Bearer sk-your-key |
Models
Use the slug as the model value. Live status comes from the status page.
| Model | Slug | Status |
|---|---|---|
| Loading... | ||
Code examples
Replace sk-your-key with your key. Better: put it in an environment variable called HORIZON_API_KEY.
curl https://horizon-api.dino.icu/v1/chat/completions \
-H "Authorization: Bearer $HORIZON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v4.1-flash",
"stream": true,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain DNS in one paragraph."}
]
}'# pip install openai
import os
from openai import OpenAI
client = OpenAI(
base_url="https://horizon-api.dino.icu/v1",
api_key=os.environ["HORIZON_API_KEY"],
)
stream = client.chat.completions.create(
model="deepseek/deepseek-v4.1-flash",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain DNS in one paragraph."},
],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)// npm install openai
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://horizon-api.dino.icu/v1",
apiKey: process.env.HORIZON_API_KEY,
});
const stream = await client.chat.completions.create({
model: "deepseek/deepseek-v4.1-flash",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Explain DNS in one paragraph." },
],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}$body = @{
model = "deepseek/deepseek-v4.1-flash"
messages = @(@{ role = "user"; content = "Explain DNS in one paragraph." })
} | ConvertTo-Json -Depth 4
$answer = Invoke-RestMethod -Method Post -Uri "https://horizon-api.dino.icu/v1/chat/completions" `
-Headers @{ Authorization = "Bearer $env:HORIZON_API_KEY" } -ContentType "application/json" -Body $body
$answer.choices[0].message.contentCoding agents
Any agent or editor that accepts an OpenAI-compatible provider works with Horizon: Cline, Roo Code, Kilo Code, OpenCode, Continue, Aider, Zed and similar tools. Tool calling and streaming are passed through, which is what agents need.
- In the tool's model settings choose OpenAI Compatible (sometimes called "Custom OpenAI" or "OpenAI API").
- Base URL:
https://horizon-api.dino.icu/v1 - API key: your
sk-...key. Model: any slug from the model list, for exampledeepseek/deepseek-v4.1-flash.
Example for Aider and other tools that read the standard environment variables:
export OPENAI_API_BASE=https://horizon-api.dino.icu/v1
export OPENAI_API_KEY=sk-your-key
aider --model openai/deepseek/deepseek-v4.1-flashAgents send many requests in a row. If you hit a limit you get HTTP 429 and the tool retries; the per-user limits are in the Limits section.
Streaming, tools, reasoning
- Streaming: set
"stream": true. While Horizon is switching to a backup endpoint you may see SSE comment lines such as: PING. OpenAI libraries ignore them. - Non-streaming works too. Horizon streams from the provider internally and returns one normal JSON response.
- Tool / function calling:
toolsandtool_choiceare passed through,tool_callscome back as usual. - Reasoning: models that think return it in
reasoning_contentnext tocontent. - Usage: every response carries
usagewith prompt and completion tokens. - Which endpoint answered:
system_fingerprintishorizon-hop-N, where N is the position in the model's fallback chain. Provider names and URLs are never shown. - Unsupported parameters: if a provider rejects an optional parameter (for example
parallel_tool_calls), Horizon retries once without it.
API keys
A key is shown once, when you create it. After that nobody, not even the admin, can show it to you again. Store it in a password manager or an environment variable.
Lost a key? Replace it
- Open Keys and create a new key. Copy it immediately.
- Put the new key into your apps and check they work.
- Delete the old key. Anything still using it stops working right away.
Keys can have a model allowlist, an expiry date and an IP allowlist. You can have up to 5 keys. To give one key a lower personal speed limit, add #rpm=N to its name, for example laptop #rpm=5.
Honest note: keys are stored in plain text in the server database (that is how New API works). Only displaying them is blocked after creation. The admin team keeps the server and its backups locked down.
Limits
These are the defaults; the admin can change them for you. When you hit one you get HTTP 429 with a Retry-After hint where possible.
| Limit | Default |
|---|---|
| Requests per minute | 15 per user |
| Requests per day | 500 per user (resets 00:00 UTC) |
| Tokens per day | 1,000,000 per user (resets 00:00 UTC) |
| Parallel requests | 3 per user |
max_tokens | capped at 16,384 (higher values are lowered) |
| Request size | 2 MB |
| Busy server | you wait in a fair queue up to 30 s, then get horizon_server_busy |
Your usage per day is on the dashboard and in usage logs.
Errors
| Status | Type | Meaning |
|---|---|---|
| 400 | upstream_client_error | The request itself is invalid (for example too long for the model). Fix the request; retrying will not help. |
| 401 | - | Missing or wrong key. |
| 403 / quota | - | Your account is not approved yet or has no quota left. |
| 429 | horizon_user_concurrency, horizon_daily_limit, horizon_daily_token_limit, horizon_user_rate_limited, horizon_key_rate_limited, horizon_server_busy | A limit was hit. Wait and retry. |
| 503 | horizon_all_endpoints_failed, horizon_no_endpoint_available | Every free endpoint for that model failed or is cooling down. Try another model or retry in a minute. |
| 504 | horizon_chain_deadline | No endpoint produced a first token within about 4 minutes. |
| in stream | horizon_stream_interrupted | The provider cut the stream after the answer had started. Retry the request. |
How fallback works
Every model has an ordered chain of free endpoints. For each request Horizon tries them top to bottom:
- An endpoint that produces no real token within 60 seconds is abandoned and the next one is tried.
- Errors such as 429, 5xx, "model not found" or "overloaded" also move to the next endpoint. A genuine 400 does not.
- Endpoints that keep failing rest for a while and are checked again automatically.
- The whole chain has a deadline of about 4 minutes.
- Once the answer has started streaming, Horizon cannot switch endpoints. If the stream breaks you get
horizon_stream_interrupted.
Privacy
Prompts and answers are never stored. Horizon logs only metadata: time, user, key, model, token counts, duration, which hop served the request and error types.
Your request is forwarded to a third-party free provider to be answered. Those providers have their own policies, and one fallback endpoint uses plain HTTP. Do not send passwords, secrets or personal data you would not want a third party to see.