ZEX API
Build with ZEX
The ZEX API gives your applications the same models you use in ZEX, through an OpenAI-compatible chat completions endpoint. Requests use the credits on your ZEX account.
https://your-zex-domain/api/v1Authorization: Bearer zex_sk_…JSON · Server-Sent Events for streamingAuthentication
Create a key in Settings → API keys. Keys start with zex_sk_ and are shown once — ZEX stores only a hash. You can have up to 10 active keys, rotate a key (the old one stops working immediately) or revoke it at any time. Each key shows when it was last used.
Send the key in the Authorization header. Never put keys in browser code or commit them to source control; call the API from your server.
Authorization: Bearer $ZEX_API_KEY
Quickstart
Replace MODEL_ID with an id from GET /models.
curl https://your-zex-domain/api/v1/chat/completions \
-H "Authorization: Bearer $ZEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID",
"messages": [
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Explain rate limiting in one paragraph."}
]
}'
// Node.js 18+ (built-in fetch)
const res = await fetch("https://your-zex-domain/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ZEX_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "MODEL_ID",
messages: [{ role: "user", content: "Explain rate limiting in one paragraph." }]
})
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (${data.error.request_id})`);
console.log(data.choices[0].message.content);
console.log("credits charged:", data.zex.credits_charged);
# Python 3.8+ — pip install requests
import os, requests
res = requests.post(
"https://your-zex-domain/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['ZEX_API_KEY']}"},
json={
"model": "MODEL_ID",
"messages": [{"role": "user", "content": "Explain rate limiting in one paragraph."}],
},
timeout=120,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["choices"][0]["message"]["content"])
print("credits charged:", data["zex"]["credits_charged"])
GET /api/v1/models
Returns the models your key can use right now: enabled, configured on this deployment and included in your plan.
curl https://your-zex-domain/api/v1/models -H "Authorization: Bearer $ZEX_API_KEY"
{
"object": "list",
"data": [
{
"id": "llama-3.3-70b",
"object": "model",
"created": 1790000000,
"owned_by": "Cloudflare Workers AI",
"name": "Llama 3.3 70B",
"context_length": 24000,
"capabilities": { "vision": false, "documents": false, "streaming": true, "tools": false },
"pricing": { "inputCreditsPer1k": 0.2, "outputCreditsPer1k": 1 }
}
]
}
Models on this deployment
Live from the public catalog. Availability depends on configured providers and your plan.
| Model id | Name | Context | Minimum plan | Credits / 1K tokens (in / out) |
|---|---|---|---|---|
| Loading… | ||||
POST /api/v1/chat/completions
Creates a model response for a conversation.
Request body
| Field | Type | Description |
|---|---|---|
model required | string | A model id from GET /api/v1/models. |
messages required | array | 1–200 messages. Roles: system, developer (treated as system), user, assistant. content is a string or an array of {"type":"text","text":…} parts; other part types (image_url, …) are not supported and return 400 unsupported_content rather than being dropped. The last message must be from user. Up to 500,000 characters in total. |
stream | boolean | If true, the response is sent as Server-Sent Events. Default false. |
max_tokens / max_completion_tokens | integer | 1–32000. Use one token-limit field, not both. Clamped to the model’s maximum. |
top_p | number | 0–1. Supported by OpenAI, native Anthropic and native Gemini adapters. |
presence_penalty, frequency_penalty | number | −2–2. OpenAI and native Gemini adapters. |
seed | integer | Safe integer. OpenAI and native Gemini; best-effort reproducibility, not a determinism guarantee. |
stop | string or array | 1–4 non-empty strings, up to 1024 characters each. OpenAI, native Anthropic and native Gemini. |
user | string | Optional opaque client ID (max 256 characters). Accepted locally but not stored, logged or forwarded to providers; not authentication. |
stream_options | object | With stream: true, supports boolean include_usage. |
temperature | number | 0–2. Sampling temperature. |
Other OpenAI parameters (tools, response_format, n, …) are not supported. The optional sampling fields above are only enabled for the named adapters; other OpenAI-compatible providers and Workers AI return 400 unsupported_parameter until explicitly supported. Individual upstream models can impose additional restrictions. Fallbacks that cannot honour the supplied fields are excluded. Sending one returns 400 unsupported_parameter — the request is refused rather than silently answered with different semantics. stream must be a real boolean.
Response
{
"id": "chatcmpl-3f9a0c1e2b7d4a8c9e0f1a2b",
"object": "chat.completion",
"created": 1790000000,
"model": "llama-3.3-70b",
"choices": [
{ "index": 0, "message": { "role": "assistant", "content": "Rate limiting caps…" }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 24, "completion_tokens": 88, "total_tokens": 112 },
"zex": {
"credits_charged": 1,
"provider": "Cloudflare Workers AI",
"model": "llama-3.3-70b",
"fallback_used": false,
"usage_estimated": false,
"request_id": "3f9a0c1e-2b7d-4a8c-9e0f-1a2b3c4d5e6f"
}
}
The zex object is a ZEX extension. model is the model that actually answered: if the requested model's provider fails before any text is produced, ZEX retries once and may fall back to the model's configured fallback (fallback_used: true). When a provider does not report token counts, ZEX estimates them (usage_estimated: true).
Streaming
With "stream": true the response is text/event-stream. Each event is a data: line with a chat.completion.chunk. The final chunk has finish_reason: "stop" plus usage and zex; the stream ends with data: [DONE]. For stream_options: {"include_usage": true}, preceding chunks have usage: null and a separate final chunk has choices: [] and aggregate usage. Set include_usage: false to omit usage. Streamed content identifies the actual responding model; terminal zex.fallback_used and zex.requested_model make fallback explicit.
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1790000000,"model":"llama-3.3-70b","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk",…,"choices":[{"index":0,"delta":{"content":"Rate limiting"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk",…,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{…},"zex":{…}}
data: [DONE]
If the provider fails mid-stream, an event data: {"error":{"code":"provider_error",…}} is sent before [DONE]. Text already streamed is charged; nothing is charged if no text was produced.
const res = await fetch("https://your-zex-domain/api/v1/chat/completions", {
method: "POST",
headers: { "Authorization": `Bearer ${process.env.ZEX_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: "MODEL_ID", stream: true, messages: [{ role: "user", content: "Write a haiku." }] })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let i;
while ((i = buffer.indexOf("\n\n")) >= 0) {
const line = buffer.slice(0, i).trim();
buffer = buffer.slice(i + 2);
if (!line.startsWith("data: ")) continue;
const payload = line.slice(6);
if (payload === "[DONE]") break;
const event = JSON.parse(payload);
if (event.error) throw new Error(event.error.message);
process.stdout.write(event.choices[0].delta.content ?? "");
}
}
import os, json, requests
with requests.post(
"https://your-zex-domain/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['ZEX_API_KEY']}"},
json={"model": "MODEL_ID", "stream": True,
"messages": [{"role": "user", "content": "Write a haiku."}]},
stream=True, timeout=300,
) as res:
res.raise_for_status()
for line in res.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
payload = line[6:]
if payload == "[DONE]":
break
event = json.loads(payload)
if "error" in event:
raise RuntimeError(event["error"]["message"])
print(event["choices"][0]["delta"].get("content", ""), end="", flush=True)
curl -N https://your-zex-domain/api/v1/chat/completions \
-H "Authorization: Bearer $ZEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "MODEL_ID", "stream": true, "messages": [{"role": "user", "content": "Write a haiku."}]}'
Errors
Errors use HTTP status codes and a consistent JSON body. Include the request_id (also sent as the X-Request-Id header) when contacting support.
{ "error": { "code": "insufficient_credits", "message": "You do not have enough credits…", "request_id": "3f9a0c1e-…" } }
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request, invalid_json | The body is malformed or a parameter is out of range. |
| 401 | invalid_api_key, revoked_api_key | Missing, unknown or revoked key. |
| 402 | insufficient_credits | Your balance is too low. Credits renew with your monthly allowance. |
| 403 | model_not_in_plan, api_disabled, account_suspended | The model needs a higher plan, the API is disabled, or the account is suspended. |
| 404 | model_not_found | Unknown or disabled model id. |
| 413 | payload_too_large | The request exceeds size limits. |
| 429 | rate_limited | Too many requests. Wait for Retry-After seconds. |
| 502 | provider_error | The provider (and any fallback) failed. No credits are charged if no text was produced; partial output is billable. |
| 503 | model_unavailable, maintenance, service_unavailable | The model's provider is not configured, ZEX is in maintenance, or a dependency is down. |
Rate limits
API requests are limited per account per minute according to your plan. Every response includes:
| Header | Description |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window. |
X-RateLimit-Remaining | Requests left in the window. |
X-RateLimit-Reset | When the window resets (Unix time, seconds). |
Retry-After | On 429 only: seconds to wait. |
| Plan | API requests / minute | Monthly credits |
|---|---|---|
| Loading… | ||
Credits & usage
Each successful request is charged max(1, ceil((input_tokens × input_rate + output_tokens × output_rate) / 1000)) credits, using the per-model rates listed above. Failed requests that produce no text are not charged. Charges are idempotent per request and your balance never goes below zero.
See every request, its tokens and cost in Settings → Plan & usage.
Using OpenAI SDKs
Because the endpoint is OpenAI-compatible, the official SDKs work by changing the base URL.
// npm install openai
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.ZEX_API_KEY, baseURL: "https://your-zex-domain/api/v1" });
const completion = await client.chat.completions.create({
model: "MODEL_ID",
messages: [{ role: "user", content: "Hello, ZEX" }]
});
console.log(completion.choices[0].message.content);
# pip install openai
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["ZEX_API_KEY"], base_url="https://your-zex-domain/api/v1")
completion = client.chat.completions.create(
model="MODEL_ID",
messages=[{"role": "user", "content": "Hello, ZEX"}],
)
print(completion.choices[0].message.content)
Data handling
The API does not store your prompts or completions. ZEX records usage metadata only — time, model, token counts, credits, status and latency — so you can audit usage. Keys are stored as SHA-256 hashes. Requests are forwarded to the model's provider to generate the response.