# plaingateway documentation # Quickstart ## 1. Get a key A person does this step once. It needs an email click and a card. 1. Sign up at [plaingateway.com/signup](https://plaingateway.com/signup) with email or Google. Email sign-up sends a confirmation link; click it. 2. Add at least $5 USD by card. Today, the first top-up of at least $5 USD adds $5 USD in bonus credit. 3. Open [/settings/keys](https://plaingateway.com/settings/keys), press **New key**, and copy the key. It starts with `sk-pg-v1-` and is shown only once. 4. Keep the key in an environment variable on your server, never in a browser or a repository: ```sh export PLAINGATEWAY_API_KEY="sk-pg-v1-..." ``` ## 2. Use the base URL | Client | Base URL | |---|---| | OpenAI-compatible clients and plain HTTP | `https://api.plaingateway.com/api/v1` | | Anthropic SDK (it appends `/v1/messages`) | `https://api.plaingateway.com/api` | Send the key as `Authorization: Bearer $PLAINGATEWAY_API_KEY`. Model ids look like `openai/gpt-5.6-luna`. See every id and price on [Models](https://plaingateway.com/models). ## 3. Send a request with curl ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Say hello in five words."}], "max_tokens": 32}' ``` The answer (values vary): ```json { "id": "gen-6f2d0c5e9a1b4c7d8e3f2a10", "object": "chat.completion", "model": "openai/gpt-5.6-luna", "provider": "OpenAI", "choices": [ { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "Hello! How are you today?" } } ], "usage": { "prompt_tokens": 12, "completion_tokens": 11, "total_tokens": 23, "cost": 0.0000156 } } ``` ## 4. Python Install the OpenAI SDK with `pip install openai`. ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.plaingateway.com/api/v1", api_key=os.environ["PLAINGATEWAY_API_KEY"], ) r = client.chat.completions.create( model="openai/gpt-5.6-luna", messages=[{"role": "user", "content": "Say hello in five words."}], ) print(r.choices[0].message.content) print(r.usage.cost) # the charge in USD ``` ## 5. TypeScript Install the OpenAI SDK with `npm install openai`. ```typescript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.plaingateway.com/api/v1", apiKey: process.env.PLAINGATEWAY_API_KEY, }); const r = await client.chat.completions.create({ model: "openai/gpt-5.6-luna", messages: [{ role: "user", content: "Say hello in five words." }], }); console.log(r.choices[0].message.content); ``` ## 6. See the cost of each response Every response carries two headers. `X-PG-Host` names the host that served it. `X-PG-Cost-USD` is the charge in USD. Add `-i` to the curl command to see them. On a stream, `X-PG-Cost-USD` arrives as a trailer after the last event. `usage.cost` in the body holds the same charge. The [Activity](https://plaingateway.com/activity) page lists every request. ## 7. Read an error Errors are JSON, and `error.code` is the HTTP status: ```json { "error": { "message": "User not found.", "code": 401 } } ``` 401 means a bad key and 402 means the balance is too low. [Errors](https://plaingateway.com/docs/api/errors) lists every status. # For AI agents This page is the whole bootstrap for a program or AI agent. The same facts are at [/llms.txt](https://plaingateway.com/llms.txt), and the API is described in [/openapi.json](https://plaingateway.com/openapi.json). Every docs page also answers as Markdown: add `.md` to its URL, or send `Accept: text/markdown`. All pages in one file: [/llms-full.txt](https://plaingateway.com/llms-full.txt). ## Facts | Fact | Value | |---|---| | Base URL, OpenAI-compatible | `https://api.plaingateway.com/api/v1` (also `https://api.plaingateway.com/v1`) | | Base URL, Anthropic SDK | `https://api.plaingateway.com/api` | | Auth | `Authorization: Bearer `, or `X-Api-Key: ` | | Key format | `sk-pg-v1-` followed by 64 hex characters | | Model id format | `author/slug`, for example `openai/gpt-5.6-luna`; append `:floor` (cheapest host first) or `:nitro` (fastest host first) | | Model list | `GET https://api.plaingateway.com/api/v1/models`, no key needed; `data[].id` is the model id | | Endpoints | `/chat/completions`, `/completions`, `/messages`, `/responses`, `/models`, `/models/{author}/{slug}/endpoints`, `/generation`, `/credits`, `/auth/key` | | Health | `GET https://api.plaingateway.com/healthz` returns `{"status": "ok"}` or `{"status": "degraded"}` | The site host, `plaingateway.com`, serves no API. ## First call ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Say hello in five words."}], "max_tokens": 32}' ``` A success is HTTP 200 with this shape (values vary): ```json { "id": "gen-6f2d0c5e9a1b4c7d8e3f2a10", "object": "chat.completion", "model": "openai/gpt-5.6-luna", "provider": "OpenAI", "choices": [{ "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "Hello! How are you today?" } }], "usage": { "prompt_tokens": 12, "completion_tokens": 11, "total_tokens": 23, "cost": 0.0000156 } } ``` `usage.cost` and the `X-PG-Cost-USD` header are the charge in USD. `X-PG-Host` names the serving host. ## Errors Errors are JSON. `error.code` is the HTTP status: ```json { "error": { "message": "account balance is too low for this request; top up to continue", "code": 402 } } ``` | Status | Meaning | Agent action | |---|---|---| | 400 | Bad request or unknown model id | Fix the request; read `GET /models` for ids | | 401 | Missing or bad key | Ask the human for a key (below) | | 402 | Balance too low, or the key's spend limit reached | Ask the human to top up; do not retry | | 404 | No host left after `provider` filters, or unknown path | Relax the filters or fix the path | | 408, 502 | The host timed out or failed | Retry with backoff | | 413 | Body over 32 MiB | Send less | | 429 | The host rate limited the request | Wait `Retry-After` seconds when present, then retry | | 503 | A gateway store is briefly unavailable | Retry after a few seconds | `/messages` answers errors in Anthropic's shape: `{"type": "error", "error": {"type": "...", "message": "..."}}`. ## Rate limits The gateway sets no request rate limit of its own. Each request first reserves its largest possible cost from the balance, so many large requests at once can get 402 before the balance is really spent; a lower `max_tokens` reserves less. A 429 comes from a host. Before answering 429, 408 or 5xx, the gateway tries the model's next eligible host. You get the 429 only when the last host also refused, with that host's `Retry-After`. When the account turned **Allow fallbacks** off, or the request sets `provider.allow_fallbacks: false`, only one host per model is tried. ## The one human step A key needs a person: sign-up confirms an email address and the first top-up needs a card. There is no free tier and no API that creates keys. If you have no key, show your human this text: ```text I need a plaingateway API key to continue. 1. Sign up at https://plaingateway.com/signup and click the link in the confirmation email. 2. Add at least $5 USD of credit by card. 3. Open https://plaingateway.com/settings/keys, press "New key", and copy the key (it starts with sk-pg-v1-). 4. Give me the key, or set it as the environment variable PLAINGATEWAY_API_KEY. ``` # FAQ ## What is the smallest top-up? $5 USD, in whole dollars. See [Wallet and top-ups](https://plaingateway.com/docs/billing/credits). ## How much can a new account add on its first day? At most $50 USD in its first 24 hours, counting open and pending checkouts. ## Is there a free tier? No. Every request is paid from the prepaid balance. ## Which host served my request? The host is the company that runs the model for that request, such as OpenAI or DeepInfra. `X-PG-Host` on the response names it. ## Is my input used for training? plaingateway does not train on request data. Each host has its own policy; see [Provider logging](https://plaingateway.com/docs/privacy/provider-logging). ## Where is the cost? Read `usage.cost` in the body, the `X-PG-Cost-USD` header, or [generation details](https://plaingateway.com/docs/api/generation). ## Where is the API? `https://api.plaingateway.com/api/v1`. See [Quickstart](https://plaingateway.com/docs/overview/quickstart). # Image inputs Check that `image` is in the model's `architecture.input_modalities` in [GET /models](https://plaingateway.com/docs/api/models) before sending images. The whole request body can be at most 32 MiB. ## Chat completions Use a content array with a text part and an `image_url` part. The URL can be a public `https://` link or a `data:` URL with base64 data. ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": [{"type": "text", "text": "What is in this image?"}, {"type": "image_url", "image_url": {"url": "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"}}]}]}' ``` For a local file, send `"url": "data:image/png;base64,"`. ## Messages Use an `image` content block with a `base64` or `url` source: ```json {"role": "user", "content": [ {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ""}}, {"type": "text", "text": "What is in this image?"} ]} ``` When the host serves the other API shape, the gateway converts the image between these two forms. # Models A model ID is `author/slug`, for example `openai/gpt-5.6-luna`. The author is the company that made the model; the slug names the model. A host is a company that runs the model, such as OpenAI, DeepInfra or Novita. One model can have several hosts. - [GET /models](https://plaingateway.com/docs/api/models) lists every routed model with its context limit, modalities, supported parameters and our prices. It needs no key. - [GET /models/{author}/{slug}/endpoints](https://plaingateway.com/docs/api/endpoints) lists a model's hosts with their prices, quantization (the number precision the host runs the model at), and the gateway's own uptime, latency and throughput figures. - [plaingateway.com/models](https://plaingateway.com/models) shows the same list with our price next to list price. Models and hosts change, so read the list instead of hard-coding it. # Principles ## Compatibility The API follows OpenRouter's paths, fields, response shapes and error bodies. Chat completions, completions and Responses follow OpenAI's shapes; Messages follows Anthropic's. ## Pricing Our price is list price less one global discount, the same for every model. Each model page shows both. Every completion reports its own cost in `usage.cost` and in the `X-PG-Cost-USD` header; on a stream, the header is a trailer after the last event. ## Host choice Each model has a pinned host: the gateway sends the model's requests to the same first host every time, which keeps prompt caches warm. When that host fails, times out or rate limits, the gateway tries the model's next host, unless **Allow fallbacks** is off in your [account settings](https://plaingateway.com/docs/routing/provider-routing#account-settings). `X-PG-Host` names the host that answered. ## Data plaingateway does not train on request data. Each host has its own retention policy; see [Provider logging](https://plaingateway.com/docs/privacy/provider-logging). Service health and host latency are on [/latency](https://plaingateway.com/latency). # Current key GET `/auth/key` returns the limit and usage of the key that sends the request. It never creates, lists or changes keys. No API does that: create and manage keys in the dashboard at [/settings/keys](https://plaingateway.com/settings/keys). `label` shows the first 8 and last 3 characters of the key, for example `sk-pg-v1...3fa`. `limit`, `limit_reset` and `limit_remaining` are `null` when the key has no spend limit. `usage` is all the key has spent, in USD. `usage_daily`, `usage_weekly` and `usage_monthly` cover the last 1, 7 and 30 days. `is_free_tier` is always `false`: there is no free tier, and `free_model_daily_requests` is always 0. The `byok_*`, `guardrail_id`, `budget_id` and `rate_limit` fields exist for OpenRouter compatibility; they are always zero, null or fixed. ### Request ```sh curl https://api.plaingateway.com/api/v1/auth/key \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" ``` ### Response A key with a 50 USD monthly spend limit: ```http HTTP/1.1 200 { "data": { "label": "sk-pg-v1...3fa", "is_management_key": false, "is_provisioning_key": false, "limit": 50, "limit_reset": "monthly", "limit_remaining": 49.999164718, "include_byok_in_limit": false, "budgets": [ { "scope": "key_limit", "guardrail_id": null, "budget_id": null, "limit_usd": 50, "spend_usd": 0.000835282, "remaining_usd": 49.999164718, "reset_interval": "monthly", "resets_at": "2026-11-01T00:00:00Z", "include_byok": false } ], "effective_budget": { "scope": "key_limit", "guardrail_id": null, "budget_id": null, "limit_usd": 50, "spend_usd": 0.000835282, "remaining_usd": 49.999164718, "reset_interval": "monthly", "resets_at": "2026-11-01T00:00:00Z", "include_byok": false }, "usage": 0.000835282, "usage_daily": 0.000835282, "usage_weekly": 0.000835282, "usage_monthly": 0.000835282, "byok_usage": 0, "byok_usage_daily": 0, "byok_usage_weekly": 0, "byok_usage_monthly": 0, "is_free_tier": false, "expires_at": null, "creator_user_id": "10000000-0000-0000-0000-000000000001", "organization_id": null, "workspace_id": "10000000-0000-0000-0000-000000000001", "allowed_data_regions": [ "global" ], "free_model_daily_requests": { "used": 0, "limit": 0, "remaining": 0 }, "rate_limit": { "requests": -1, "interval": "10s", "note": "This field is deprecated and safe to ignore." } } } ``` Errors: 401 for a missing or unknown key, 503 when the billing store cannot be read. # Chat completions POST `/chat/completions` takes `model` and `messages`, and the optional fields in the table below. A string `prompt` is also accepted in place of `messages`. The answer keeps the OpenAI shape. plaingateway adds `usage.cost`, the charge in USD. Any field the gateway does not read, such as `temperature`, goes to the host unchanged. ## Streaming With `"stream": true` the answer is server-sent events, ending with `data: [DONE]`. While the host has not answered yet, the gateway sends `: PLAINGATEWAY PROCESSING` comment lines every 15 seconds so proxies keep the connection open. Ignore lines that start with `:`. The last chunk carries `usage` with `cost`. `X-PG-Cost-USD` arrives as an HTTP trailer after the last event. A failure after the stream started arrives as a last event with an `error` object; see [Errors](https://plaingateway.com/docs/api/errors). ```sh curl -N https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Count to three."}], "stream": true}' ``` ## Example ### Request ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5.6-luna", "messages": [ { "role": "user", "content": "Say hello in five words." } ], "max_tokens": 32 }' ``` ### Response ```http HTTP/1.1 200 { "id": "gen-6f2d0c5e9a1b4c7d8e3f2a10", "object": "chat.completion", "created": 1700000000, "model": "openai/gpt-5.6-luna", "provider": "OpenAI", "system_fingerprint": null, "service_tier": "default", "choices": [ { "index": 0, "logprobs": null, "finish_reason": "stop", "native_finish_reason": "completed", "message": { "role": "assistant", "content": "Hello! How are you today?", "refusal": null, "reasoning": null } } ], "usage": { "prompt_tokens": 12, "completion_tokens": 11, "total_tokens": 23, "cost": 1.56e-05, "is_byok": false, "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0, "audio_tokens": 0, "video_tokens": 0 }, "cost_details": { "upstream_inference_cost": 1.56e-05, "upstream_inference_prompt_cost": 2.4e-06, "upstream_inference_completions_cost": 1.32e-05 }, "completion_tokens_details": { "reasoning_tokens": 0, "image_tokens": 0, "audio_tokens": 0 } } } ``` ## OpenRouter-compatible request fields | Parameter | Type | Semantics | |---|---|---| | `model` | string | Primary model ID. | | `models` | string[] | Fallback model IDs after `model`, or the only IDs if `model` is absent. | | `messages` | object[] | Conversation turns for chat or Messages. | | `prompt` | string | Accepted in place of `messages`. | | `max_tokens` | integer | Most output tokens. The balance hold is sized from it. | | `user` | string | Your end user's id; stored as `external_user` on the [generation](https://plaingateway.com/docs/api/generation). | | `stream` | boolean | Return server-sent events. | | `tools` | object[] | Available function tools. | | `tool_choice` | string or object | Select tool use. | | `response_format` | object | Request a structured output format. | | `reasoning` | object | `effort`, `max_tokens`, and `exclude` reasoning controls. | | `usage.include` | boolean | Ask for usage; plaingateway always includes usage and adds `cost`. | | `provider` | object | Host filters and order; see [Provider routing](https://plaingateway.com/docs/routing/provider-routing). | | `plugins`, `transforms` | | Not supported. Any value other than `null` returns 400. | Request headers are on [Headers](https://plaingateway.com/docs/api/headers). Host pinning means the gateway sends a model's requests to the same first host each time, so prompt caches on that host keep working. Host pinning, `usage.cost`, `X-PG-Host`, `X-PG-Cost-USD`, and `X-PG-Request-Id` are plaingateway only. # Completions POST `/completions` takes `model`, `prompt` as a string, and `max_tokens`. It is the legacy text completion API: use it only for a client that needs `choices[].text`. New code should use [Chat completions](https://plaingateway.com/docs/api/chat-completions). `messages` is also accepted in place of `prompt`. Routing fields, streaming and errors work as on chat completions. ### Request ```sh curl https://api.plaingateway.com/api/v1/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5.6-luna", "prompt": "Say hello in five words.", "max_tokens": 32 }' ``` ### Response ```http HTTP/1.1 200 { "id": "gen-6f2d0c5e9a1b4c7d8e3f2a10", "object": "text_completion", "created": 1700000000, "model": "openai/gpt-5.6-luna", "provider": "OpenAI", "choices": [ { "index": 0, "text": "Hello, it’s nice to meet you!", "reasoning": null, "logprobs": null, "finish_reason": "stop", "native_finish_reason": "completed" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 13, "total_tokens": 25, "cost": 1.8e-05, "is_byok": false, "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0, "audio_tokens": 0, "video_tokens": 0 }, "cost_details": { "upstream_inference_cost": 1.8e-05, "upstream_inference_prompt_cost": 2.4e-06, "upstream_inference_completions_cost": 1.56e-05 }, "completion_tokens_details": { "reasoning_tokens": 0, "image_tokens": 0, "audio_tokens": 0 }, "service_tier": "default" } } ``` # Credits GET `/credits` returns two account totals in USD. `total_credits` is the balance plus everything spent, so the balance you can spend now is `total_credits - total_usage`. `total_usage` is everything the account has spent. Errors: 401 for a bad key, 503 when the billing store cannot be read. ### Request ```sh curl https://api.plaingateway.com/api/v1/credits \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" ``` ### Response ```http HTTP/1.1 200 { "data": { "total_credits": 50, "total_usage": 0.177992694 } } ``` # Endpoints GET `/models/{author}/{slug}/endpoints` returns one entry per host for the model, for example `/models/openai/gpt-5.6-luna/endpoints`. It needs no key. An unknown model id returns 404; a catalog entry the gateway cannot read returns 500. Prices are USD per token at our price, as in [GET /models](https://plaingateway.com/docs/api/models). `uptime_last_5m`, `uptime_last_30m` and `uptime_last_1d` are the percentage of the gateway's requests to that host that did not get a 5xx answer in that window, or `null` when the gateway sent it none. `latency_last_30m` (milliseconds to first token) and `throughput_last_30m` (tokens per second) are percentiles of the gateway's own requests in the last 30 minutes, or `null` with none. The list comes from the model catalog and can name a host the gateway does not route to. `X-PG-Host` on a completion names the host that served it. The response does not state a host's data retention. ### Request ```sh curl https://api.plaingateway.com/api/v1/models/openai/gpt-5.6-luna/endpoints ``` ### Response One host, shortened. The prices shown are list prices. ```http HTTP/1.1 200 { "data": { "id": "openai/gpt-5.6-luna", "name": "OpenAI: GPT-5.6 Luna", "created": 1783590864, "endpoints": [ { "name": "OpenAI | openai/gpt-5.6-luna-20260709", "model_id": "openai/gpt-5.6-luna", "model_name": "OpenAI: GPT-5.6 Luna", "context_length": 1050000, "pricing": { "prompt": "0.0000001", "completion": "0.0000006", "input_cache_read": "0.00000001", "input_cache_write": "0.000000125", "discount": 0 }, "provider_name": "OpenAI", "tag": "openai/flex", "quantization": "unknown", "max_completion_tokens": 128000, "max_prompt_tokens": 922000, "supported_parameters": ["max_tokens", "reasoning", "response_format", "tool_choice", "tools"], "status": 0, "uptime_last_30m": 100, "uptime_last_5m": 100, "uptime_last_1d": 99.65, "latency_last_30m": { "p50": 1210, "p75": 1669.5, "p90": 2564.4, "p99": 9103.2 }, "throughput_last_30m": { "p50": 122.5, "p75": 80, "p90": 47.1, "p99": 20.7 }, "perf_last_30m_by_workload": { "text_generation": { "latency": { "p50": 1210, "p75": 1669.5, "p90": 2564.4, "p99": 9103.2 }, "throughput": { "p50": 122.5, "p75": 80, "p90": 47.1, "p99": 20.7 }, "request_count": 1547 } } } ] } } ``` # Errors Every endpoint except `/messages` returns errors in this shape. `error.code` is the HTTP status, not a provider code. ```json { "error": { "message": "No endpoints found matching your provider.only list", "code": 404, "metadata": { "routing_funnel": [], "failed_routing_step": "Filter by Only" } }, "user_id": "10000000-0000-0000-0000-000000000001" } ``` `metadata` is present only when there is detail: `provider_name` on a host error, `routing_funnel` and `failed_routing_step` when routing filters leave no host. `user_id` (your account id) is sent on 400 answers once the key is known. `/messages` uses Anthropic's shape: `{"type": "error", "error": {"type": "authentication_error", "message": "...", "error_type": "authentication"}, "request_id": null}`. ## Statuses | Status | Cause | What to do | Charged | |---|---|---|---| | 400 | Invalid JSON, no input, a missing or unknown model id, an unsupported variant, a bad `provider` object, `plugins` or `transforms` set, or a request no host can take | Fix the request | No | | 401 | No key, or a key that is unknown, disabled, expired or revoked | Send a valid key | No | | 402 | The balance cannot cover this request, or the key reached its spend limit | Top up at `/settings/credits`, or raise the key's limit | No | | 404 | Unknown path, unknown generation id, unknown model on `/endpoints`, or no host left after routing filters | Check the id, or relax the `provider` filters | No | | 408 | The host sent no answer in time | Retry | No | | 413 | The request body is over 32 MiB | Send a smaller body | No | | 429 | The last host tried rate limited the request | Wait for `Retry-After` when present, then retry | No | | 500 | A catalog entry could not be read | Retry later | No | | 502 | The host returned an error, or a response cut off, unreadable or over 32 MB | Retry; the message carries the request id | See below | | 503 | The gateway's key, billing or usage store is briefly unavailable | Retry after a short wait | No | A host's own 4xx answer, such as a context length error, keeps its status. The message is `Provider returned error`, and `metadata.raw` carries the host's body. A host's 401 or 403 means the host refused the gateway's credentials, so you get 502. An error answered before the host produced output is not charged. A 502 for a response the host did finish, but that was over 32 MB or could not be translated, can be charged for the tokens the host reported. [Usage accounting](https://plaingateway.com/docs/features/usage-accounting) explains charges. Before answering 408, 429 or 502 for a host that failed, timed out or rate limited, the gateway tries the next eligible host; see [Rate limits](https://plaingateway.com/docs/features/rate-limits). A 502 for a response that arrived but was unreadable comes back at once, without trying another host. With **Allow fallbacks** off in your [account settings](https://plaingateway.com/docs/routing/provider-routing#account-settings), the gateway tries one host per model. ## Errors inside a stream Once a stream has started, the status is already 200, so a failure arrives as a last event with the error body and `metadata.provider_name`, and the stream ends. Its `error.code` is 408 for a timeout and 502 otherwise. ## Example ### Request ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer sk-pg-v1-not-a-real-key" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Say hello in five words."}]}' ``` ### Response ```http HTTP/1.1 401 { "error": { "message": "User not found.", "code": 401 } } ``` # Generation GET `/generation?id=` returns the cost and token counts of one earlier request. Take the id from the `X-Generation-Id` header of that request, or from the `id` of a chat, completions or Responses answer. A `/messages` answer has a `msg_` id, so use the header there. An id that does not start with `gen-` returns 400. An id from another account, or one not yet recorded, returns 404; a new id can take a moment to appear. `model` is the id of the model that served the request, without a `:floor` or `:nitro` suffix. After a [model fallback](https://plaingateway.com/docs/routing/model-fallbacks) it is the fallback model. `total_cost` and `usage` are the charge in USD. `streamed` says whether the answer was streamed. `provider_responses` lists each host tried, in order, with its status. ### Request ```sh curl 'https://api.plaingateway.com/api/v1/generation?id=gen-6f2d0c5e9a1b4c7d8e3f2a10' \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" ``` ### Response ```http HTTP/1.1 200 { "data": { "created_at": "1970-01-01T00:00:00Z", "model": "openai/gpt-5.6-luna", "app_id": null, "external_user": null, "streamed": true, "cancelled": false, "latency": 1036, "moderation_latency": null, "generation_time": 1036, "tokens_prompt": 6, "tokens_completion": 7, "native_tokens_prompt": 12, "native_tokens_completion": 11, "native_tokens_completion_images": 0, "native_tokens_reasoning": 0, "native_tokens_cached": 0, "num_media_prompt": null, "num_input_audio_prompt": null, "num_media_completion": 0, "num_search_results": null, "num_fetches": null, "web_search_engine": null, "origin": "", "is_byok": false, "finish_reason": "stop", "native_finish_reason": "completed", "service_tier": "default", "usage": 1.56e-05, "router": null, "user_agent": "curl/8.7.1", "http_referer": null, "request_id": "req-1792038846-SYcf5HD6fusXxy4YDYmB", "session_id": null, "preset_id": null, "api_type": "completions", "id": "gen-6f2d0c5e9a1b4c7d8e3f2a10", "upstream_id": "resp_0a1b2c3d4e5f60718293a4b5", "provider_responses": [ { "endpoint_id": "eebea444-34a5-4642-a766-cb319471d33a", "id": "resp_0a1b2c3d4e5f60718293a4b5", "is_byok": false, "latency": 426, "model_permaslug": "openai/gpt-5.6-luna-20260709", "provider_name": "OpenAI", "status": 200 } ], "total_cost": 1.56e-05, "cache_discount": null, "upstream_inference_cost": 0, "provider_name": "OpenAI", "response_cache_source_id": null, "data_region": "global", "workspace_id": "4b871218-f911-581f-adab-89998b54c9c1" } } ``` # Headers ## Host and paths The API host is `https://api.plaingateway.com`. Every path is served under both `/api/v1` and `/v1`, so `https://api.plaingateway.com/api/v1/models` and `https://api.plaingateway.com/v1/models` are the same. The site host, `plaingateway.com`, serves no API. ## Request headers | Header | Use | |---|---| | `Authorization: Bearer sk-pg-v1-...` | Your key. | | `X-Api-Key: sk-pg-v1-...` | Your key, as Anthropic SDKs and Claude Code send it. Used when there is no `Authorization` header. | | `HTTP-Referer` | Your app's URL, for [app attribution](https://plaingateway.com/docs/features/app-attribution). | | `X-OpenRouter-Title` or `X-Title` | Your app's name, for app attribution. | | `Anthropic-Version`, `Anthropic-Beta` | Passed to Anthropic hosts on `/messages`. Without `Anthropic-Version` the gateway sends `2023-06-01`. | `GET /models`, `GET /models/{author}/{slug}/endpoints` and `GET /healthz` need no key. Browsers can call the API directly: every response allows any origin (CORS). ## Response headers | Header | Meaning | |---|---| | `X-PG-Host` | The host that served the request, or `none`. | | `X-PG-Cost-USD` | The charge in USD. On a stream it is an HTTP trailer, sent after the last event, because the cost is known only then. | | `X-Generation-Id` | The generation id. Pass it to [GET /generation](https://plaingateway.com/docs/api/generation). | | `X-PG-Request-Id` | The same value as `X-Generation-Id`. | | `Retry-After` | Passed through when the host sent it, usually on 429. | The `id` of a chat, completions or Responses answer is the generation id too. A `/messages` answer keeps a `msg_` id, so read the header there. ## Example ### Request ```sh curl -i https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "HTTP-Referer: https://example.com" \ -H "X-Title: Example app" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Say hello in five words."}], "max_tokens": 32}' ``` ### Response headers ```http HTTP/1.1 200 Content-Type: application/json X-Generation-Id: gen-6f2d0c5e9a1b4c7d8e3f2a10 X-PG-Request-Id: gen-6f2d0c5e9a1b4c7d8e3f2a10 X-PG-Host: OpenAI X-PG-Cost-USD: 0.0000156 Access-Control-Allow-Origin: * ``` # Messages POST `/messages` is the Anthropic Messages API. It takes `model`, `messages` and `max_tokens`, all required, plus Anthropic fields such as `system`, `tools`, `thinking`, `cache_control` and `stream`. Send the key as `x-api-key` or `Authorization: Bearer`. `anthropic-version` and `anthropic-beta` are passed to Anthropic hosts; the gateway sends `anthropic-version: 2023-06-01` when you omit it. A model on a non-Anthropic host is translated to and from chat completions, so Anthropic-only features such as `thinking` and `cache_control` need an Anthropic host. Errors use Anthropic's shape; see [Errors](https://plaingateway.com/docs/api/errors). The Anthropic SDK base URL is `https://api.plaingateway.com/api`; the SDK appends `/v1/messages`. ### Request ```sh curl https://api.plaingateway.com/api/v1/messages \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-haiku-5.5", "max_tokens": 32, "messages": [ { "role": "user", "content": "Say hello in five words." } ] }' ``` ### Response ```http HTTP/1.1 200 { "id": "msg_01XFDUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "container": null, "content": [ { "type": "text", "text": "Hello there, friend, how are you?", "citations": [] } ], "model": "anthropic/claude-haiku-5.5", "stop_reason": "end_turn", "stop_details": null, "stop_sequence": null, "usage": { "input_tokens": 16, "output_tokens": 14, "output_tokens_details": { "thinking_tokens": 0 }, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation": { "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0 }, "inference_geo": "global", "server_tool_use": null, "service_tier": "standard", "speed": "standard", "cost": 8.6e-06, "is_byok": false, "cost_details": { "upstream_inference_cost": 8.6e-06, "upstream_inference_prompt_cost": 1.6e-06, "upstream_inference_completions_cost": 7e-06 } }, "context_management": null, "provider": "Claude Platform on AWS" } ``` # Models GET `/models` returns every model the gateway routes, with its context limit, modalities, supported parameters and prices. It needs no key. Use `id` (`author/slug`) as `model` in a request. Prices are USD per token, as decimal strings, at our price: list price less the current discount. `input_cache_read` and `input_cache_write` are the prices of cached and cache-writing input tokens. `supported_parameters` lists what at least one host accepts; a given host may accept fewer. To list a model's hosts, call [GET /models/{author}/{slug}/endpoints](https://plaingateway.com/docs/api/endpoints) with the `id`. Do not follow `links.details`: for some models it carries a dated slug that the gateway answers with 404. ### Request ```sh curl https://api.plaingateway.com/api/v1/models ``` ### Response One `data` item, shortened. The prices shown are list prices. ```http HTTP/1.1 200 { "data": [ { "id": "openai/gpt-5.6-luna", "canonical_slug": "openai/gpt-5.6-luna-20260709", "hugging_face_id": null, "name": "OpenAI: GPT-5.6 Luna", "created": 1783590864, "context_length": 1050000, "architecture": { "modality": "text+image+file->text", "input_modalities": ["file", "image", "text"], "output_modalities": ["text"], "tokenizer": "GPT", "instruct_type": null }, "pricing": { "prompt": "0.0000002", "completion": "0.0000012", "input_cache_read": "0.00000002", "input_cache_write": "0.00000025" }, "top_provider": { "context_length": 1050000, "max_completion_tokens": 128000, "is_moderated": true }, "per_request_limits": null, "supported_parameters": ["max_tokens", "reasoning", "response_format", "tool_choice", "tools"], "links": { "details": "/api/v1/models/openai/gpt-5.6-luna-20260709/endpoints" } } ], "total_count": 26, "links": { "next": null } } ``` # Responses POST `/responses` is the OpenAI Responses API. It takes `model` and `input`, as a string or a list of input items. A host that serves Responses natively (OpenAI hosts) gets the request as sent. Any other host gets it translated to chat completions, and the answer is translated back. On a translated host, `previous_response_id` and tools other than function tools return 400, because chat completions has no equivalent. `stream`, function tools and the routing fields work on both. ### Request ```sh curl https://api.plaingateway.com/api/v1/responses \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5.6-luna", "input": "Say hello in five words.", "max_output_tokens": 32 }' ``` ### Response ```http HTTP/1.1 200 { "id": "gen-6f2d0c5e9a1b4c7d8e3f2a10", "object": "response", "created_at": 1700000000, "model": "openai/gpt-5.6-luna", "status": "completed", "completed_at": 1700000000, "output": [ { "id": "msg_0a1b2c3d4e5f60718293a4b5", "type": "message", "role": "assistant", "status": "completed", "content": [ { "type": "output_text", "text": "Hello there, nice to meet you!", "annotations": [], "logprobs": [] } ], "phase": "final_answer" } ], "error": null, "incomplete_details": null, "tools": [], "tool_choice": "auto", "parallel_tool_calls": true, "max_output_tokens": 32, "temperature": 1, "top_p": 1, "presence_penalty": 0, "frequency_penalty": 0, "top_logprobs": 0, "max_tool_calls": null, "metadata": {}, "background": false, "previous_response_id": null, "service_tier": "default", "truncation": "disabled", "store": false, "instructions": null, "text": { "format": { "type": "text" } }, "reasoning": { "context": "all_turns", "effort": "medium", "mode": "standard", "summary": "detailed" }, "safety_identifier": null, "prompt_cache_key": null, "prompt_cache_options": null, "usage": { "input_tokens": 12, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 12, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 24, "cost": 1.68e-05, "is_byok": false, "cost_details": { "upstream_inference_cost": 1.68e-05, "upstream_inference_input_cost": 2.4e-06, "upstream_inference_output_cost": 1.44e-05 } } } ``` # API keys Create a key in the dashboard at [/settings/keys](https://plaingateway.com/settings/keys): press **New key**, give it a name, pick an expiry, and optionally set a spend limit. The key starts with `sk-pg-v1-` and is shown only once. Copy it then. No API creates keys; see [Management keys](https://plaingateway.com/docs/auth/management-keys). ## Use a key Send it as `Authorization: Bearer sk-pg-v1-...`, or as `X-Api-Key`. ```sh curl https://api.plaingateway.com/api/v1/auth/key \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" ``` [Current key](https://plaingateway.com/docs/api/api-keys) explains the answer. ## Options | Option | Values | Effect | |---|---|---| | Expiry | never, 1 hour, 1 day, 7, 30, 90 or 180 days, 1 year | An expired key gets 401. | | Spend limit | USD, or none | A key that reached its limit gets 402 until the period resets. | | Limit reset | never, day, week, month | Periods start at 00:00 UTC; a week starts on Monday. | A spend limit belongs to one key. The account balance is shared by all keys. ## Disable, revoke, delete - **Disable** stops a key until you enable it again. - **Revoke** stops a key for good. Revoke any key that may have leaked. - **Delete** removes a key from the list. Changes reach the gateway within about 10 seconds. Keep keys on a server. A key in a browser or a public repository can be copied and spent. # Management keys plaingateway has no management or provisioning keys. No API lists, creates, changes or deletes keys, and `is_management_key` in [GET /auth/key](https://plaingateway.com/docs/api/api-keys) is always `false`. Create and manage keys in the dashboard at [/settings/keys](https://plaingateway.com/settings/keys); see [API keys](https://plaingateway.com/docs/auth/api-keys). # Auto top-up Auto top-up charges your saved card when the balance falls below a threshold you choose. ## Turn it on 1. Make one top-up by card. That checkout saves the card. 2. Open [/settings/credits](https://plaingateway.com/settings/credits) and switch on **Auto top-up**. 3. Set **When balance falls below ($)** and **Add this amount ($)**, then press **Save**. Switch it off in the same place. ## What happens | State | What it means | |---|---| | Checking | Every minute, billing checks accounts below their threshold, and checks the balance again just before charging. | | Pending | A charge was sent and its result is not known yet. No second charge starts while one is pending. | | Paid | The credit is added when the card payment succeeds. | | Declined | The attempt fails. A new attempt can start after 1 hour. | # Bonus and referral credit The amounts below are today's settings. They can change; a change applies to top-ups paid after it. | Credit | Who gets it | Today's amount | |---|---|---| | First top-up bonus | Every account, on its first top-up of at least $5 USD | $5 USD | | Referral credit | The referrer, on each top-up the referred account pays | 2% of the top-up, for 12 months after the referred account's first top-up | A paid top-up is money paid by card. Bonus credit is not a paid top-up, so it earns no referral credit. Being referred adds no extra bonus. A refund or dispute takes back the matching referral credit, and takes back the bonus when it refunds the first top-up. See [Refunds](https://plaingateway.com/docs/billing/refunds). # Wallet and top-ups plaingateway is prepaid: you add credit to a wallet, and requests spend it. ## Add credit 1. Open [/settings/credits](https://plaingateway.com/settings/credits) and choose an amount. 2. Pay by card in Stripe Checkout. The credit appears when the payment clears. | Rule | Today's value | |---|---| | Smallest top-up | $5 USD, whole dollars | | Most a new account can add in its first 24 hours | $50 USD, counting open and pending checkouts | | First top-up bonus | $5 USD on a first top-up of at least $5 USD | These values are settings and can change. A late payment that would pass the first-day limit is refunded in full and adds no credit. ## How usage is charged 1. Each request reserves its largest possible cost from the balance, and gets 402 if the balance cannot cover it. 2. When the request ends, its real cost is counted against the balance at once, so the next request sees it. 3. Every 5 minutes, billing writes the cost of each closed hour (an hour that has ended) to the wallet ledger as one usage row. So the cost of a request lowers your spendable balance at once, and appears as a ledger row after its hour closes. [GET /credits](https://plaingateway.com/docs/api/credits) returns total credits and total usage. # Invoices Every top-up paid through checkout creates a Stripe invoice. Open [/settings/credits](https://plaingateway.com/settings/credits). Each invoice has a link to its hosted page and to its PDF. The page lists your 24 newest invoices. # Refunds To ask for a refund, write to support@plaingateway.com. | Case | Effect on the wallet | |---|---| | A top-up is refunded in full | The top-up amount is taken off the wallet. | | A top-up is refunded in part | The refunded part is taken off. Several partial refunds never take off more than the top-up. | | The first top-up is refunded | The $5 USD first top-up bonus is taken back too. | | A card payment is disputed | Treated as a refund of the disputed amount. | | A late payment passes the first-day $50 USD limit | It is refunded in full and adds no credit. | When the refunded top-up earned a referrer credit, the same share of that credit is taken back from the referrer, never more than is left. A refund can leave the balance below zero. Requests then get 402 until you top up. # Taxes Tax treatment of top-ups is not settled yet. Checkout does not add sales tax today. Each checkout payment has a Stripe invoice, listed at [/settings/credits](https://plaingateway.com/settings/credits). For a tax question about your account, write to support@plaingateway.com. # Changelog ## 2026-10-17 Docs: the API host is `https://api.plaingateway.com`. Earlier pages named the site host, which serves no API. Docs: added the For AI agents page, `/llms.txt`, `/llms-full.txt`, `/openapi.json`, and Markdown copies of every page. Docs: management keys are documented as not available, because no such API exists. ## 2026-10-14 Docs: first documentation set. # App attribution App attribution tags a request with the app that sent it. Send any of these headers: | Header | Meaning | |---|---| | `HTTP-Referer` | Your app's URL. | | `X-OpenRouter-Title` or `X-Title` | Your app's name. | The gateway turns the first header present into a number, `app_id` on the [generation](https://plaingateway.com/docs/api/generation). The same header value always gives the same `app_id`. There is no app registry. The headers do not change routing or price. ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "HTTP-Referer: https://example.com" \ -H "X-Title: Example app" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Hello"}]}' ``` # Error handling Every status, body and cause is on [Errors](https://plaingateway.com/docs/api/errors). This page says what a client should do. | Status | Retry? | |---|---| | 400, 401, 404, 413 | No. Fix the request or the key. | | 402 | No. Top up, or raise the key's spend limit. | | 408, 502 | Yes, with backoff: wait 1 second, then 2, then 4. | | 429 | Yes, after `Retry-After` seconds when the header is present; otherwise with backoff. | | 500, 503 | Yes, after a few seconds. | The gateway already tries the model's other hosts before it answers 408, 429, or a 502 for a failed host; see [Rate limits](https://plaingateway.com/docs/features/rate-limits). With **Allow fallbacks** off in your [account settings](https://plaingateway.com/docs/routing/provider-routing#account-settings), it tries one host per model. To try other models too, list them in `models`; see [Model fallbacks](https://plaingateway.com/docs/routing/model-fallbacks). # Prompt caching A cache read is an input token the host served from its cache, at the lower `input_cache_read` price. A cache write is an input token the host stored for later, at the `input_cache_write` price. Both prices are in [GET /models](https://plaingateway.com/docs/api/models). A cache lives on one host, so the gateway sends a model's requests to the same first host each time (host pinning). ## Anthropic models Mark the end of the part to cache with `cache_control` in a Messages request: ```sh curl https://api.plaingateway.com/api/v1/messages \ -H "x-api-key: $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "anthropic/claude-haiku-5.5", "max_tokens": 64, "system": [{"type": "text", "text": "", "cache_control": {"type": "ephemeral"}}], "messages": [{"role": "user", "content": "Summarize it."}]}' ``` `usage.cache_creation_input_tokens` counts tokens written and `usage.cache_read_input_tokens` counts tokens read. `cache_control` on chat completions content parts is passed to Anthropic hosts too. ## Other models OpenAI hosts cache long prompts on their own. `usage.prompt_tokens_details.cached_tokens` counts the cached input tokens. # Rate limits The gateway sets no request rate limit of its own, and the dashboard shows none. Hosts have their own limits. When a host answers 429, the gateway tries the model's next eligible host. It also moves on after a timeout or a 5xx. When **Allow fallbacks** is off on [/settings/preferences](https://plaingateway.com/settings/preferences), the gateway tries one host per model and returns that host's error; see [Account settings](https://plaingateway.com/docs/routing/provider-routing#account-settings). You get 429 only when the last host tried also refused. `Retry-After` is passed through when that host sent it. A model with one host has no other host to try. Add fallback models with `models` (see [Model fallbacks](https://plaingateway.com/docs/routing/model-fallbacks)) to spread load. The balance is the other limit. Each request reserves its largest possible cost before it starts, from `max_tokens` and the prompt size. Many large requests at once can get 402 even when the balance would cover their real cost. A lower `max_tokens` reserves less. The `rate_limit` field in [GET /auth/key](https://plaingateway.com/docs/api/api-keys) is deprecated and means nothing. # Reasoning tokens Send a `reasoning` object on chat completions: | Field | Type | Meaning | |---|---|---| | `effort` | string | One of the model's `reasoning.supported_efforts` in [GET /models](https://plaingateway.com/docs/api/models), for example `low`, `medium` or `high`. | | `max_tokens` | integer | A token budget for reasoning, for hosts that take a budget. | | `exclude` | boolean | `true` leaves the reasoning text out of the answer. Its tokens are still charged. | ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Is 221 prime?"}], "reasoning": {"effort": "low"}}' ``` Reasoning tokens count as output tokens. Leave room for them in `max_tokens`, or the answer can stop with `finish_reason: "length"`. `usage.completion_tokens_details.reasoning_tokens` counts them when the host reports it. On `/messages`, send Anthropic's `thinking` object instead. It is translated to `reasoning.max_tokens` for a non-Anthropic host. # Structured outputs Structured output means the model must answer with JSON that matches a JSON Schema you send. Check that `structured_outputs` is in the model's `supported_parameters` in [GET /models](https://plaingateway.com/docs/api/models). ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Name a city and its country."}], "response_format": { "type": "json_schema", "json_schema": { "name": "place", "strict": true, "schema": { "type": "object", "properties": {"city": {"type": "string"}, "country": {"type": "string"}}, "required": ["city", "country"], "additionalProperties": false } } } }' ``` The JSON is the text of `choices[0].message.content`. A host that rejects the schema answers 400, and `metadata.raw` carries its message. For a Claude model on an Anthropic host, the gateway passes a `json_schema` format on when that Claude model supports structured output. It drops `json_object`, and drops `json_schema` for a Claude model without that support; check `supported_parameters` first. # Tool calling A tool call has three steps: you send tools, the model answers with a call, and you send the call's result back. ## 1. Send the tools ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Weather in Paris?"}], "tools": [{"type": "function", "function": {"name": "get_weather", "description": "Current weather for a city", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}]}' ``` `tool_choice` is `auto` (the default), `none`, `required`, or `{"type": "function", "function": {"name": "get_weather"}}`. ## 2. Read the call The answer has `finish_reason: "tool_calls"` and the call in the message: ```json {"role": "assistant", "content": null, "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}]} ``` ## 3. Send the result Run the function, then send the whole conversation again with the assistant message and a `tool` message: ```json "messages": [ {"role": "user", "content": "Weather in Paris?"}, {"role": "assistant", "content": null, "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}]}, {"role": "tool", "tool_call_id": "call_1", "content": "18 C, cloudy"} ] ``` Send the same `tools` again. The model then answers in text. ## Other API shapes On `/messages`, use Anthropic's `tools`, `tool_use` and `tool_result` blocks. On `/responses`, use function tools and `function_call_output` items. The gateway translates tool calls between shapes when the host serves another shape. On a translated `/responses` host, only function tools work. # Usage accounting Each successful completion reports its charge in USD three ways: | Where | When | |---|---| | `usage.cost` in the body | In the answer; on a stream, in the last chunk. | | `X-PG-Cost-USD` header | With the answer; on a stream, as an HTTP trailer after the last event. | | [GET /generation](https://plaingateway.com/docs/api/generation) | Shortly after the request ends. | The charge is the token counts the host reported, times our price per token. An error answered before the host produced output costs nothing. A finished host response that the gateway could not translate can still be charged. ## When the wallet changes 1. Before a request starts, the gateway reserves its largest possible cost from the balance. 2. When it ends, the real cost replaces the reserve and lowers your spendable balance at once. 3. Every 5 minutes, billing writes each closed hour's total to the wallet ledger as one usage row. A later correction to an hour is written as an adjustment row; an hour whose usage disappears is credited back. The [Activity](https://plaingateway.com/activity) page lists requests and their cost. # Zero data retention Zero data retention (ZDR) means the host keeps no prompts or outputs after answering. Set `provider.zdr` to `true` to use only hosts the gateway marks as ZDR. Set `provider.data_collection` to `deny` to use only hosts marked as never training on or keeping data. ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "messages": [{"role": "user", "content": "Hello"}], "provider": {"zdr": true}}' ``` Today no routed host is marked zero data retention or as never keeping data, so either filter leaves no host. The answer is then 404 with `failed_routing_step` in the error metadata. The ZDR column on a model page shows the catalog's tag for each listed host. It is not the gateway's routing mark. ## Account setting Turn on **Zero data retention only** at [/settings/privacy](https://plaingateway.com/settings/privacy) to apply `zdr: true` to every request from your account's keys. A request cannot turn it off. A model with no zero data retention host then answers 404, with a message that names this setting. The change reaches the gateway within about 10 seconds. # Aider ```sh export OPENAI_API_BASE="https://api.plaingateway.com/api/v1" export OPENAI_API_KEY="$PLAINGATEWAY_API_KEY" aider --model openai/openai/gpt-5.6-luna ``` Aider reads the first `openai/` as "an OpenAI-compatible server" and drops it. It sends the rest, `openai/gpt-5.6-luna`, as the model ID. So put `openai/` in front of any model ID, for example `openai/anthropic/claude-sonnet-5`. To check the setup, send one short prompt. The request then shows on the [Activity](https://plaingateway.com/activity) page with its host and cost. # Anthropic SDK The SDK appends `/v1/messages` to the base URL, so the base URL stops at `/api`. ```python import os from anthropic import Anthropic client = Anthropic(base_url="https://api.plaingateway.com/api", api_key=os.environ["PLAINGATEWAY_API_KEY"]) msg = client.messages.create( model="anthropic/claude-haiku-5.5", max_tokens=256, messages=[{"role": "user", "content": "Hello"}], ) print(msg.content[0].text) ``` ```typescript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.plaingateway.com/api", apiKey: process.env.PLAINGATEWAY_API_KEY }); const msg = await client.messages.create({ model: "anthropic/claude-haiku-5.5", max_tokens: 256, messages: [{ role: "user", content: "Hello" }], }); ``` `api_key` sends the key as `x-api-key`. `auth_token` sends it as `Authorization: Bearer`. Both work. A model ID is the full `author/slug` string from [Models](https://plaingateway.com/models), for example `openai/gpt-5.6-luna`. Models from other authors work too; the gateway translates the request. # Claude Code ```sh export ANTHROPIC_BASE_URL="https://api.plaingateway.com/api" export ANTHROPIC_AUTH_TOKEN="$PLAINGATEWAY_API_KEY" export ANTHROPIC_API_KEY="" export ANTHROPIC_MODEL="anthropic/claude-opus-5.5" claude ``` Or put the same variables in the `env` object of `~/.claude/settings.json`: ```json { "env": { "ANTHROPIC_BASE_URL": "https://api.plaingateway.com/api", "ANTHROPIC_AUTH_TOKEN": "sk-pg-v1-...", "ANTHROPIC_API_KEY": "", "ANTHROPIC_MODEL": "anthropic/claude-opus-5.5" } } ``` `ANTHROPIC_BASE_URL` stops at `/api` because Claude Code appends `/v1/messages`. `ANTHROPIC_AUTH_TOKEN` carries your key. `ANTHROPIC_API_KEY` stays empty so Claude Code does not send an Anthropic key. `ANTHROPIC_MODEL` is a plaingateway model ID. # Cline 1. In Cline's settings, choose **OpenAI Compatible** as the API provider. 2. Set **Base URL** to `https://api.plaingateway.com/api/v1`. 3. Paste your plaingateway key as the API key. 4. Enter a model ID, for example `openai/gpt-5.6-luna`. Cline adds `/chat/completions` to the base URL, so the base URL must end in `/api/v1`. To check the setup, send one short prompt. The request then shows on the [Activity](https://plaingateway.com/activity) page with its host and cost. # Codex CLI ```toml # ~/.codex/config.toml model = "openai/gpt-5.6-luna" model_provider = "plaingateway" [model_providers.plaingateway] name = "plaingateway" base_url = "https://api.plaingateway.com/api/v1" env_key = "PLAINGATEWAY_API_KEY" wire_api = "responses" ``` Then export the key and start Codex: ```sh export PLAINGATEWAY_API_KEY="sk-pg-v1-..." codex ``` `wire_api = "responses"` makes Codex call `/responses`. `env_key` names the environment variable that holds the key. # Continue Add a model to `~/.continue/config.yaml`: ```yaml models: - name: GPT-5.6 Luna via plaingateway provider: openai model: openai/gpt-5.6-luna apiBase: https://api.plaingateway.com/api/v1 apiKey: sk-pg-v1-... useResponsesApi: false ``` Continue adds `/chat/completions` to `apiBase`. `useResponsesApi: false` keeps it on chat completions. To check the setup, send one short prompt. The request then shows on the [Activity](https://plaingateway.com/activity) page with its host and cost. # Cursor 1. Open **Settings** > **Models**. 2. Under **OpenAI API Key**, paste your plaingateway key. 3. Turn on **Override OpenAI Base URL** and enter `https://api.plaingateway.com/api/v1`. This sends Cursor's OpenAI requests to plaingateway instead of OpenAI. 4. Add a custom model with a model ID, for example `openai/gpt-5.6-luna`, and select it. To check the setup, send one short prompt. The request then shows on the [Activity](https://plaingateway.com/activity) page with its host and cost. # Kilo Code 1. In **Settings** > **Providers**, add a **Custom** provider. 2. Set **Provider API** to **OpenAI Compatible** and **Base URL** to `https://api.plaingateway.com/api/v1`. 3. Paste your plaingateway key as the API key. 4. Under **Models**, add model IDs, for example `openai/gpt-5.6-luna`. A model ID is the full `author/slug` string from [Models](https://plaingateway.com/models), for example `openai/gpt-5.6-luna`. To check the setup, send one short prompt. The request then shows on the [Activity](https://plaingateway.com/activity) page with its host and cost. # LangChain Install `langchain-openai`, export `PLAINGATEWAY_API_KEY`, then: ```python import os from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="openai/gpt-5.6-luna", api_key=os.environ["PLAINGATEWAY_API_KEY"], base_url="https://api.plaingateway.com/api/v1", ) print(llm.invoke("Say hello in five words.").content) ``` # LibreChat 1. Add `PLAINGATEWAY_API_KEY=sk-pg-v1-...` to LibreChat's `.env` file. 2. Add an endpoint to `librechat.yaml`: ```yaml endpoints: custom: - name: "plaingateway" apiKey: "${PLAINGATEWAY_API_KEY}" baseURL: "https://api.plaingateway.com/api/v1" models: default: ["openai/gpt-5.6-luna"] fetch: true titleConvo: true titleModel: "openai/gpt-5.6-luna" ``` 3. Restart LibreChat. `fetch: true` loads the model list from `GET /models`, which needs no key. `default` is the list shown if that fails. # n8n 1. Create an **OpenAI** credential (the OpenAI API credential type). 2. Set **API Key** to your plaingateway key and **Base URL** to `https://api.plaingateway.com/api/v1`. Save. 3. In an **OpenAI Chat Model** node, select that credential. 4. Set the model to a model ID, for example `openai/gpt-5.6-luna`, using the expression field if the list does not show it. A model ID is the full `author/slug` string from [Models](https://plaingateway.com/models), for example `openai/gpt-5.6-luna`. # Open WebUI 1. Open **Admin Settings** > **Connections** > **OpenAI** > **Add Connection**. 2. Enter `https://api.plaingateway.com/api/v1` as the URL and your plaingateway key as the key. Save. Open WebUI reads the model list from `GET /models`, so every routed model appears in the model picker. To check the setup, send one short prompt. The request then shows on the [Activity](https://plaingateway.com/activity) page with its host and cost. # OpenAI SDK ```python import os from openai import OpenAI client = OpenAI(base_url="https://api.plaingateway.com/api/v1", api_key=os.environ["PLAINGATEWAY_API_KEY"]) r = client.chat.completions.create( model="openai/gpt-5.6-luna", messages=[{"role": "user", "content": "Hello"}], ) print(r.choices[0].message.content, r.usage.cost) ``` ```typescript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.plaingateway.com/api/v1", apiKey: process.env.PLAINGATEWAY_API_KEY }); const r = await client.chat.completions.create({ model: "openai/gpt-5.6-luna", messages: [{ role: "user", content: "Hello" }], }); ``` A model ID is the full `author/slug` string from [Models](https://plaingateway.com/models), for example `openai/gpt-5.6-luna`. `client.responses.create` works too, on `/responses`. # OpenCode Put this in `opencode.json`: ```json { "$schema": "https://opencode.ai/config.json", "provider": { "plaingateway": { "npm": "@ai-sdk/openai-compatible", "name": "plaingateway", "options": { "baseURL": "https://api.plaingateway.com/api/v1", "apiKey": "{env:PLAINGATEWAY_API_KEY}" }, "models": { "openai/gpt-5.6-luna": { "name": "GPT-5.6 Luna" } } } } } ``` `plaingateway` is the provider ID; each key under `models` is a plaingateway model ID. Start OpenCode and run `/models`, then pick `plaingateway/openai/gpt-5.6-luna`. # SillyTavern 1. In **API Connections**, choose **Chat Completion** as the API type. 2. Choose **Custom (OpenAI-compatible)** as the Chat Completion Source. 3. Enter `https://api.plaingateway.com/api/v1` as the custom endpoint URL and your plaingateway key as the API key. 4. Press **Connect**, then pick a model ID, for example `openai/gpt-5.6-luna`. A model ID is the full `author/slug` string from [Models](https://plaingateway.com/models), for example `openai/gpt-5.6-luna`. # Vercel AI SDK Install `ai` and `@ai-sdk/openai-compatible`, export `PLAINGATEWAY_API_KEY`, then: ```typescript import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { generateText } from "ai"; const plaingateway = createOpenAICompatible({ name: "plaingateway", apiKey: process.env.PLAINGATEWAY_API_KEY, baseURL: "https://api.plaingateway.com/api/v1", }); const { text } = await generateText({ model: plaingateway("openai/gpt-5.6-luna"), prompt: "Say hello in five words.", }); console.log(text); ``` # From Anthropic Change two settings and the model ID: | Setting | Before | After | |---|---|---| | Base URL | `https://api.anthropic.com` | `https://api.plaingateway.com/api` | | Key | `sk-ant-...` | `sk-pg-v1-...` | | Model | `claude-haiku-5-5` | `anthropic/claude-haiku-5.5` | ```python import os from anthropic import Anthropic client = Anthropic(base_url="https://api.plaingateway.com/api", api_key=os.environ["PLAINGATEWAY_API_KEY"]) ``` The SDK appends `/v1/messages`. The key works as `x-api-key` or `Authorization: Bearer`. Responses, streams and errors keep Anthropic's shape. `anthropic-version` and `anthropic-beta` reach Anthropic hosts. `thinking` and `cache_control` need a model on an Anthropic host. Claude Code has its own page: [Claude Code](https://plaingateway.com/docs/integrations/claude-code). # From OpenAI Change two settings and the model ID: | Setting | Before | After | |---|---|---| | Base URL | `https://api.openai.com/v1` | `https://api.plaingateway.com/api/v1` | | Key | `sk-...` | `sk-pg-v1-...` | | Model | `gpt-5.6-luna` | `openai/gpt-5.6-luna` | ```python import os from openai import OpenAI client = OpenAI(base_url="https://api.plaingateway.com/api/v1", api_key=os.environ["PLAINGATEWAY_API_KEY"]) ``` [Chat completions](https://plaingateway.com/docs/api/chat-completions), [Completions](https://plaingateway.com/docs/api/completions) and [Responses](https://plaingateway.com/docs/api/responses) keep OpenAI's shapes. `/responses` on a non-OpenAI model is translated to chat completions; there `previous_response_id` and non-function tools return 400. Read `usage.cost` for the charge and `X-PG-Host` for the host. On a stream, `X-PG-Cost-USD` is a trailer after the last event. # From OpenRouter plaingateway uses OpenRouter's paths, fields, response shapes and errors, so most clients need two changes. 1. Change the base URL from `https://openrouter.ai/api/v1` to `https://api.plaingateway.com/api/v1`. 2. Use a plaingateway key (`sk-pg-v1-...`). 3. Check that each model ID you use is in [GET /models](https://plaingateway.com/docs/api/models). An ID the gateway does not route returns 400. 4. Check the variants you use. `:floor` and `:nitro` work. Any other suffix, such as `:free`, `:thinking`, `:extended` or `:online`, returns 400 unless the full ID is itself a routed model. 5. Remove `plugins` and `transforms`. Any value other than `null` returns 400. 6. Keep only these `provider` fields: `order`, `only`, `ignore`, `sort`, `allow_fallbacks`, `data_collection`, `zdr`, `quantizations`, `max_price` (with `prompt` and `completion`). Any other field returns 400. To estimate savings, paste your usage into [/migrate](https://plaingateway.com/migrate). It shows what the same tokens cost on OpenRouter and on plaingateway, model by model. # Data policies | Party | What it keeps | |---|---| | plaingateway | Request metadata: time, key, model, host, token counts, cost and status. Not prompts or outputs. Each request row is kept 7 days; hourly totals are kept for billing. plaingateway does not train on request data. | | The host | Its own policy. Hosts differ; see [Provider logging](https://plaingateway.com/docs/privacy/provider-logging). | To limit hosts per request, set `provider.data_collection` to `deny` or `provider.zdr` to `true`. See [Zero data retention](https://plaingateway.com/docs/features/zero-data-retention) for what these filters match today. To apply the zero data retention filter to every request, turn on **Zero data retention only** at [/settings/privacy](https://plaingateway.com/settings/privacy). A request cannot turn it off. # Data Processing Addendum A signed data processing addendum (DPA) is not published yet. To ask for one, write to support@plaingateway.com with your company name. In a DPA, you are the controller of the data you send, and plaingateway is the processor that passes it to the host. # Provider logging A host is the company that runs the model for a request. Each host has its own logging and retention policy, which plaingateway does not set. `X-PG-Host` on each response names the host that saw the request. To keep a request on chosen hosts, use `provider.only` or `provider.ignore` (see [Provider routing](https://plaingateway.com/docs/routing/provider-routing)), or the retention filters in [Zero data retention](https://plaingateway.com/docs/features/zero-data-retention). Read the host's own policy before sending sensitive input. # Subprocessors | Subprocessor | Purpose | |---|---| | The model hosts | Run the model for each request. `X-PG-Host` names the host of each request. | | Supabase | Accounts, sign-in and the database. | | Stripe | Card payments and invoices. | | Amazon Web Services | Runs the gateway and billing, and sends email. | | Vercel | Runs the website. | For locations and transfer terms, write to support@plaingateway.com. # Referral dashboard Open [/earn](https://plaingateway.com/earn) and the **Refer** tab. | Section | What it shows | |---|---| | Your code and link | Your referral code and share link, created with your account. | | Share card | Post text to copy. | | Referred accounts | Each referred account: when it joined, first top-up, status, days left in its 12 months, its top-ups to date, and what you earned. | | By month | What you earned each month. | Referral credit goes into your wallet, so it also shows in the wallet ledger at [/settings/credits](https://plaingateway.com/settings/credits). # How referral works 1. Every account gets a referral code and link when it is created; see [Referral dashboard](https://plaingateway.com/docs/referral/dashboard). 2. A new account signs up through your link, or enters your code before its first top-up. 3. The new account gets the usual first top-up bonus, today $5 USD on a first top-up of at least $5 USD. Being referred adds no extra bonus. 4. You earn a share of every top-up it pays: today 2%, for 12 months after its first top-up. 5. The credit appears in your wallet when the card payment clears (Stripe confirms it). There is one level: you earn on accounts you refer, not on the accounts they refer. The 2% and the 12 months are today's settings and can change. [Referral rules](https://plaingateway.com/docs/referral/rules) lists what blocks a referral. # Payouts Referral earnings are paid as wallet credit, within about a minute of the referred account's cleared card payment. The credit spends like any other credit. See it at [/settings/credits](https://plaingateway.com/settings/credits) and on the **Refer** tab of [/earn](https://plaingateway.com/earn). Cash payouts are not available. # Referral rules | Rule | Example | Effect | |---|---|---| | No self-referral | You refer a second account you own | No credit | | Card fingerprint (the card's unique id at Stripe) | The referred account pays with a card you or another of your referred accounts also used | No credit | | Company email domain | More than 50 accounts, referred or not, share the referred account's email domain (free mail such as gmail.com does not count) | No credit | | Disputes | Disputed referred accounts pass 5% of your referred accounts that paid | You are paused and earn no new credit; the referred accounts keep their bonus | | Refunds and disputes | A referred top-up is refunded | The credit it earned is taken back | | One level | An account you referred refers another | You earn nothing on the second | | 12 months | A top-up paid after the referred account's 12 months | No credit | To ask about a blocked referral, write to support@plaingateway.com. # Model fallbacks Set `model` to the model you want and `models` to the ones to try next, in order. `models` alone also works; its first entry is then the first choice. ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5.6-luna", "models": ["deepseek/deepseek-v4-flash", "z-ai/glm-5.3-flash"], "messages": [{"role": "user", "content": "Hello"}]}' ``` The gateway lists every eligible host of the first model, then of the next, and tries them in that order. An eligible host is one left after the `provider` filters. It moves to the next host on a connection error, a timeout before the host answers, a 5xx, a 429 or a 408. With `provider.allow_fallbacks: false`, or with **Allow fallbacks** off in your [account settings](https://plaingateway.com/docs/routing/provider-routing#account-settings), each model gets one host. The models in `models` are still tried in turn. It does not move on after another 4xx, such as a context length error: you get that error. Once a stream has started, it never moves on. The `model` field of the answer and `X-PG-Host` show what served the request. # Model variants A variant is a suffix on the model ID that changes the order the gateway tries hosts in. | Variant | Example | Host order | |---|---|---| | none | `deepseek/deepseek-v4-flash` | The pinned host first, then the others | | `:floor` | `deepseek/deepseek-v4-flash:floor` | Cheapest host first, the same as `provider.sort: "price"` | | `:nitro` | `deepseek/deepseek-v4-flash:nitro` | Highest throughput first, the same as `provider.sort: "throughput"` | `:nitro` uses each host's configured throughput figure. Where no host has one, the order does not change. Any other suffix, such as `:free`, `:thinking`, `:extended` or `:online`, returns 400, unless the full ID with its suffix is itself a model in [GET /models](https://plaingateway.com/docs/api/models). ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "deepseek/deepseek-v4-flash:floor", "messages": [{"role": "user", "content": "Hello"}]}' ``` # Provider routing The `provider` object chooses which hosts can serve a request and in what order. A provider here means a host, named as in `X-PG-Host` (for example `DeepInfra`) or by its tag. Requests without it use the model's default order, with the pinned host first. | Field | Type | Effect | |---|---|---| | `order` | string[] | Try these hosts first, in this order, then the other eligible hosts. | | `only` | string[] | Allow only these hosts. | | `ignore` | string[] | Never use these hosts. | | `sort` | string, or object with `by` | Order by `price`, `throughput` or `latency`. Throughput and latency use each host's configured figures. | | `allow_fallbacks` | boolean | `false` tries only the hosts named in `order`, or only the first host without `order`. | | `data_collection` | string | `deny` keeps only hosts marked as never keeping data. | | `zdr` | boolean | `true` keeps only hosts marked zero data retention. | | `quantizations` | string[] | Keep hosts running these quantizations; `unknown` matches a host that states none. | | `max_price` | object | Highest `prompt` and `completion` price, in USD per 1M tokens, at our price. | ```sh curl https://api.plaingateway.com/api/v1/chat/completions \ -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "deepseek/deepseek-v4-pro", "messages": [{"role": "user", "content": "Hello"}], "provider": {"order": ["Novita"], "ignore": ["DeepInfra"], "max_price": {"prompt": 1, "completion": 4}}}' ``` Any other field in `provider` or `max_price`, a bad `sort` value, or a `data_collection` other than `allow` or `deny` returns 400. ## Account settings Two account settings apply to every request from your account's keys. Changes reach the gateway within about 10 seconds. | Setting | Page | Effect | |---|---|---| | **Allow fallbacks** off | [/settings/preferences](https://plaingateway.com/settings/preferences) | The same as `provider.allow_fallbacks: false`: one host per model, and that host's error is returned. A request that sets `provider.allow_fallbacks` overrides it. | | **Zero data retention only** on | [/settings/privacy](https://plaingateway.com/settings/privacy) | The same as `provider.zdr: true`, and a request cannot turn it off. See [Zero data retention](https://plaingateway.com/docs/features/zero-data-retention). | The other choices on the Routing page (host order, allowed and ignored hosts, max price) are saved but do not change API requests. Send them in `provider`. ## When no host is left Filters run in this order: API shape, `only`, `ignore`, `quantizations`, `data_collection`, `zdr`, `max_price`. The first filter that leaves no host ends routing with 404: ```json { "error": { "message": "No endpoints found matching your provider.only list", "code": 404, "metadata": { "routing_funnel": [{"step": "Initial Endpoints", "endpoint_count": 2}, {"step": "Filter by API Shape", "endpoint_count": 2}], "failed_routing_step": "Filter by Only" } } } ``` `routing_funnel` lists how many hosts were left after each step that ran. `failed_routing_step` names the step that left none.