Asale

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:

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.

DialectBase URLEndpoint it calls
OpenAIhttps://gw.asale.ai/v1POST /v1/chat/completions
Anthropichttps://gw.asale.aiPOST /v1/messages
Geminihttps://gw.asale.aiPOST /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.

StatuscodeWhat to do
401unauthorizedThe key is missing, malformed or unknown. Check the header and the key.
403forbiddenThe key is disabled, or the account is blocked from trading.
402payment_requiredTop up the wallet.
429rate_limitedBack off; Retry-After says for how long.
503no_supplyNobody is selling that model right now. Retry, or pick another model.
502upstreamThe 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.