API overview
One gateway, three compatible dialects. Point an OpenAI, Anthropic or Gemini client at Asale, change the base URL and the key, and the rest of your code stays as it is.
Asale's gateway answers in three wire formats. Whichever one your client already speaks, you keep your SDK, your request bodies and your streaming code — you change two lines: the base URL and the key.
Everything on this page is the same for all three. The per-dialect pages carry the endpoints and the runnable samples:
- OpenAI-compatible —
chat/completions,responses - Anthropic-compatible —
messages - Gemini-compatible —
generateContent
Base URLs
Each dialect wants its base URL cut at a different depth, because each vendor's client appends a different suffix. Use the value for the SDK you are holding.
| Dialect | Base URL | Endpoint it calls |
|---|---|---|
| OpenAI | https://gw.asale.ai/v1 | POST /v1/chat/completions |
| Anthropic | https://gw.asale.ai | POST /v1/messages |
| Gemini | https://gw.asale.ai | POST /v1beta/models/{model}:generateContent |
gw.asale.ai is the inference gateway. api.asale.ai is the console API — sessions, wallet, market data — and it does not accept sk-asale- keys. Sending inference there returns 404.
Authentication
Three headers, one per dialect, so each vendor's SDK authenticates unmodified. Any of them is accepted on any endpoint.
Authorization: Bearer sk-asale-...
x-api-key: sk-asale-...
x-goog-api-key: sk-asale-...
Keys are managed on the API keys page: every account gets one at sign-up, and you can add more, name them, give them an expiry, disable one without deleting it, and read any of them back when you lose the copy.
One key per account is marked default. That is the key the desktop app hands to the AI tools it is buying through — it has no effect on requests you make yourself, which are authenticated by whichever key you send.
Models
Model names are the platform's, not a vendor's SKU list. Ask the gateway what it will match:
curl https://gw.asale.ai/v1/models -H "Authorization: Bearer $ASALE_API_KEY"
{
"object": "list",
"data": [
{ "id": "claude-sonnet-4-5", "object": "model", "owned_by": "anthropic", "context_length": 200000 }
]
}
A model that is in the catalog but has nobody selling it right now answers 503 no_supply rather than 404 — the name is valid, the supply is not there this second. The market page shows what is liquid.
Streaming
Server-sent events, in each dialect's own event shape, with no re-framing: stream: true for OpenAI and Anthropic, :streamGenerateContent for Gemini. Your existing stream parser works unchanged.
What a request costs
Before a request goes out, the gateway holds an estimate — your declared max_tokens at the model's output price. When the response finishes, it is priced on real usage and the difference returns to your available balance immediately.
Nothing is charged when the request errors, when usage comes back zero, or when you disconnect mid-stream. See metering for how tokens are counted.
Errors
Every failure is JSON with the same shape, at the matching HTTP status:
{
"error": {
"message": "insufficient balance",
"code": "payment_required",
"key": "errors.wallet.insufficientBalance"
}
}
Branch on code, never on message — the prose is localized and reworded freely.
| Status | code | What to do |
|---|---|---|
| 401 | unauthorized | The key is missing, malformed or unknown. Check the header and the key. |
| 403 | forbidden | The key is disabled, or the account is blocked from trading. |
| 402 | payment_required | Top up the wallet. |
| 429 | rate_limited | Back off; Retry-After says for how long. |
| 503 | no_supply | Nobody is selling that model right now. Retry, or pick another model. |
| 502 | upstream | The provider that served the request failed. Safe to retry. |
An expired key answers 401 with errors.apiKey.expired; a disabled one answers 403 with errors.apiKey.disabled. The two have different fixes, which is why they are different statuses.
Rate limits
Each key carries its own requests-per-minute and tokens-per-minute ceiling. Exceeding either returns 429 with Retry-After. These are per key, so splitting a workload across two keys splits the limits with it.