Developer Docs

Cangyun AI Gateway speaks the standard OpenAI-compatible protocol. One access key plus one base URL is all you need — and the catalog, the price book and your balance all have unauthenticated or unbilled self-check endpoints, so you can verify the path before integrating.

Base URL
https://gw.cangyunai.com/v1
Access key
wxk-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Issued by the operator, prefixed with wxk-, with your tenant id in the middle. Your credits balance decides how much you can call; the model catalog is at /v1/models.

Overview

You get one access key and one base URL — that is all. The gateway speaks the standard OpenAI-compatible protocol, so any OpenAI SDK, framework or tool that lets you set a custom base_url works as-is, with no adapter code.

The key carries two things at once: identity and credits metering — every call is converted into credits by model and usage dimension, and deducted from your account balance.

The endpoints you will use:

  • /v1/models — the model catalog you can call right now; no authentication, not billed;
  • /v1/chat/completions — the main endpoint for chat and code generation, streaming supported;
  • /v1/prices — the public price book and off-peak discount windows; no authentication;
  • /v1/credits/balance, /v1/credits/ledger, /v1/credits/usage — check your balance, ledger and usage with your own key; not billed.
  1. Run the two free self-checks, /v1/models and /v1/credits/balance — they prove network, catalog, key and balance in one go;
  2. Wire up the SDK;
  3. If you need streaming, flip one parameter;
  4. On errors, consult the error-code table, then the troubleshooting checklist.

Base URL & access key

The base URL looks like https://<your-gateway-host>/v1. Note the trailing /v1 — append the rest of the path directly to it.

FormResult
https://gw.example.cn/v1Correct
https://gw.example.cn/v1/Most SDKs tolerate it, but manual string concatenation yields //chat/completions
https://gw.example.cnMissing /v1 → 404
The console (admin) URLThe admin console and the data plane are different entrances; ask your operator for the data-plane address
  • Keys start with wxk-, with a tenant identifier in the middle and random characters at the end;
  • The plaintext is shown exactly once, at issuance. No UI or API can retrieve it afterwards — save it then;
  • If lost, the operator must revoke the old key and issue a new one;
  • Keys do not expire — a key is either active or revoked;
  • When the account runs out of credits, calls return 402 (see the billing section).
Never put the key into a code repository, a front-end bundle, a client installer or logs. Inject it via environment variables, and use export rather than writing it inline on the command line — otherwise it lands in your shell history.
bash
export CANGYUN_BASE_URL="https://gw.cangyunai.com/v1"
export CANGYUN_API_KEY="wxk-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Authentication: exactly one header

The platform accepts exactly one form: Authorization: Bearer <key>. Note that Bearer is case-sensitive (a lowercase bearer is treated as absent), with exactly one space before the key.

http
Authorization: Bearer wxk-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#RuleResponse when violated
1The Authorization value must start with Bearer (case-sensitive)401 + code: missing_api_key
2Passing the key as a URL query parameter (e.g. ?api_key=) is not supported401 + code: missing_api_key
3No other auth header names are read — sending one is equivalent to sending nothing401 + code: missing_api_key
4The key does not exist or has been revoked401 + code: invalid_api_key
If you are migrating from an older version of this document: the x-api-key form is retired. It is ignored when present — the outcome depends solely on Authorization being set correctly. Keep only Authorization.

Endpoint scope: POSTed OpenAI-compatible endpoints (chat completions, responses, embeddings, audio, images, rerank, …) are proxied upstream after authentication and admission; GET /v1/models and GET /v1/prices are answered by the platform itself without authentication; the self-service GET /v1/credits/balance family uses the same key.

Quick start: two free self-checks

The first two steps produce no usage, are not billed and do not touch your balance — run them as often as you like.

Step 1 verifies network and catalog (no authentication, no key):

bash
curl -sS "https://gw.cangyunai.com/v1/models"

A 200 with a body like:

json
{"object":"list","data":[{"id":"model-a","object":"model"},{"id":"model-b","object":"model"}]}

Step 2 verifies key and balance (with the key, not billed):

bash
curl -sS "https://gw.cangyunai.com/v1/credits/balance" \
  -H "Authorization: Bearer wxk-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

A 200 containing available_credits means the key is valid and the account has balance:

json
{
  "tenant_id": "t-example",
  "balance_credits": 3800,
  "inflight_credits": 1.25,
  "available_credits": 3798.75,
  "settle_interval_s": 300
}
VerifiedHow failure shows up
The network path to the gateway works and the base URL is rightconnection timeout / 404
The key is valid and not revoked401 (missing_api_key / invalid_api_key)
The account has usable balanceread available_credits; at 0, model calls return 402

The first two steps prove "network + catalog + key + balance". They do not prove a given model actually produces output upstream. Proving that requires one real request, and real requests are billed. To keep the cost negligible, cap max_tokens in single digits:

bash
curl -sS "https://gw.cangyunai.com/v1/chat/completions" \
  -H "Authorization: Bearer wxk-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "model-a",
    "messages": [{"role": "user", "content": "hi"}],
    "max_tokens": 8
  }'

A 200 with a non-empty choices[0].message.content means the path works end to end.

OpenAI SDK

The SDK renders apiKey as Authorization: Bearer … — exactly the one authentication form the platform supports, so no extra auth configuration is needed.

python
import os
from openai import OpenAI

client = OpenAI(
    base_url=os.environ["CANGYUN_BASE_URL"],   # https://gw.cangyunai.com/v1
    api_key=os.environ["CANGYUN_API_KEY"],
)

# 免费自检:当前模型目录(不计费)
for m in client.models.list().data:
    print(m.id)

# 真实调用(计费)
resp = client.chat.completions.create(
    model="model-a",
    messages=[{"role": "user", "content": "你好"}],
)
print(resp.choices[0].message.content)
ts
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: process.env.CANGYUN_BASE_URL,
  apiKey: process.env.CANGYUN_API_KEY,
});

// 免费自检(不计费)
const models = await client.models.list();
console.log(models.data.map((m) => m.id));

// 真实调用(计费)
const resp = await client.chat.completions.create({
  model: "model-a",
  messages: [{ role: "user", content: "你好" }],
});
console.log(resp.choices[0]?.message?.content);

GET /v1/models: the global catalog

This endpoint is answered by the platform itself, without authentication; the request never reaches the upstream model vendor. Therefore:

  • It produces no token usage and is not billed — call it as often as you like;
  • It responds fast (unaffected by upstream latency);
  • It returns the global catalog: every customer sees the same list — it is not filtered per key.

The response is a standard OpenAI Models list, so models.list() in any OpenAI SDK works out of the box.

When the list changesPropagation
The operator lists or delists a modelimmediately (hot configuration)
The upstream vendor delists a modelremoved from the catalog once the operator syncs
Do not hard-code the list. Fetch /v1/models at startup or periodically and drive model selection from it; a model that is not in the list will most likely fail.
The model list is authoritative via GET /v1/models; calls to models outside it are not guaranteed to work. A model without configured pricing is refused with 404 model_not_priced — never silently free.

See the pricing page for per-model credit prices →

One edge: GET /v1/models/{model} (single-model detail) is not supported. To read the list, use GET /v1/models without a model name.

GET /v1/prices: the public price book & discount windows

No authentication required. It returns the current pricing snapshot: coefficients per model pattern × usage dimension, plus the off-peak/peak discount windows in effect. One credit is ¥0.01 at face value.

bash
curl -sS "https://gw.cangyunai.com/v1/prices"

The response looks like:

json
{
  "snapshot_id": 3,
  "credit_face": "1 积分 = ¥0.01",
  "prices": [
    {
      "model_pattern": "qwen/*",
      "coefficients": [
        {"dimension": "input_token", "unit_divisor": 1000000, "credits_per_unit_batch": 260},
        {"dimension": "cached_input_token", "unit_divisor": 1000000, "credits_per_unit_batch": 104},
        {"dimension": "output_token", "unit_divisor": 1000000, "credits_per_unit_batch": 390},
        {"dimension": "reasoning_token", "unit_divisor": 1000000, "credits_per_unit_batch": 390}
      ]
    }
  ],
  "discounts": [
    {"model_pattern": "*", "start_min": 1320, "end_min": 360, "factor": 0.5, "timezone": "Asia/Shanghai"}
  ]
}
  • Coefficients are given per model pattern × usage dimension: input_token / cached_input_token / output_token / reasoning_token and so on, in "credits per million tokens";
  • discounts lists the active time-of-day windows: start_min / end_min are minutes in the rule's timezone (1320 = 22:00), factor < 1 is a discount (0.5 = 50% off), applied by whether the request completes inside the window;
  • snapshot_id increases monotonically: every price change bumps the version, and in-flight requests still settle on the snapshot they started with.

The portal pricing page renders this same data; the live API response is authoritative.

Streaming

One extra parameter, nothing else changes. With curl, pass -N (disable buffering) or you will not see the incremental output.

bash
curl -N "https://gw.cangyunai.com/v1/chat/completions" \
  -H "Authorization: Bearer wxk-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "model-a",
    "messages": [{"role": "user", "content": "写一段介绍"}],
    "stream": true
  }'

The response is standard SSE: content-type: text/event-stream, every event prefixed with data: and the stream terminated by data: [DONE]. The platform forwards the upstream stream without buffering or aggregating it.

text
data: {"choices":[{"delta":{"content":"你"}}]}

data: {"choices":[{"delta":{"content":"好"}}]}

data: [DONE]
If a streaming request fails midway, the error may arrive as an SSE event inside the stream rather than as an HTTP status code — the headers were already flushed with the first chunk. Your client needs an error branch inside the stream handler too.

Billing & credits

  • Billed per token: unit prices are expressed as credits per million tokens (see /v1/prices), with input / output / cached input / reasoning priced as separate dimensions;
  • One credit is ¥0.01 at face value; internally the ledger keeps micro-credits, so nothing is lost to rounding below one credit;
  • At admission a conservative hold is placed against the request's upper bound; at settlement the actual usage is charged and the remainder of the hold is released;
  • Deduction is not real time: settlement runs in batches on a cycle (≤5 minutes by default, about 15 minutes at peak), so the balance endpoint reports the settled view;
  • When usable balance (balance − in-flight holds) ≤ 0, calls return 402 insufficient_credits; after a top-up, service resumes within seconds — no new key needed;
  • Requests completed inside a discount window are charged at the window factor, and the ledger entry carries a discount_factor field.

You can self-check with the same key (none of these three endpoints is billed):

EndpointReturns
GET /v1/credits/balancebalance, in-flight holds and usable balance (balance_credits / inflight_credits / available_credits)
GET /v1/credits/ledger?limit=Nentry-by-entry ledger: purchase / consume / adjust / refund / expire, with model and ref_id on consume entries
GET /v1/credits/usage?group_by=modelusage aggregates; group_by can be model / day / dimension / key

The portal console visualizes these three endpoints; for programmatic reconciliation, call the APIs directly.

Error codes in full

Every platform-side error uses one envelope: {"error":{"code","message",...}}. Branch on code, never on the message text (wording may change); some errors carry extra fields inside error (for example, 402 carries balance_credits and recharge_url).

json
{
  "error": {
    "code": "insufficient_credits",
    "message": "积分余额不足,请前往门户充值",
    "balance_credits": 0.42,
    "inflight_credits": 0.9,
    "recharge_url": "https://www.cangyunai.com/console/billing"
  }
}
HTTPcodeMeaningClient action
401missing_api_keyAuthorization absent, or its value is not a Bearer token (case-sensitive)Send Authorization: Bearer <key>; do not retry
401invalid_api_keyThe key is invalid or revokedCheck the key; if revoked, ask the operator to reissue; do not retry
402insufficient_creditsNot enough usable credits (balance − in-flight holds ≤ 0)Top up via error.recharge_url; retrying is useless
404model_not_pricedThe model has no pricing configured, so the platform refuses it (never silently free)Switch to a model from the /v1/models list; do not retry
404(no code)Misspelled path or nonexistent endpointCheck the base URL ends with /v1 and the path spelling

Errors from the upstream or network stage:

HTTPMeaningClient action
5xxUpstream model service failure or platform errorReport with the X-Cy-Request-Id; a few backed-off retries are fine
The platform currently does not rate-limit the data plane: you will never get a 429, and responses carry no rate-limit quota headers. Do not write retry branches around 429; what you need to govern is your own concurrency and budget.

Every response that passed admission carries an X-Cy-Request-Id header — include it when reporting an issue (401 / 402 happen at the admission stage before the header is attached; for those, the error.code and the time of occurrence are enough).

402 is the only signal that takes money to recover from — it does not self-heal. Retrying ten thousand times changes nothing; top up instead.

Troubleshooting checklist

Work down the list — most problems are located within the first four rows:

SymptomCheck firstNotes
Connection timeout / refusedHost and port of the base URLConfirm the address with the operator; confirm egress firewall allows it
404 (no error.code)Whether the base URL ends with /v1See the misspelling table in the base URL section
401 missing_api_keyWhether Authorization is absent, or Bearer was written lowercaseThe platform only accepts the exact Bearer prefix (with one space)
401 invalid_api_keyWhether the key is complete and not revokedTruncation while copy-pasting is the usual cause
402 insufficient_creditsavailable_credits via GET /v1/credits/balanceService resumes seconds after a top-up; do not retry
404 model_not_pricedWhether the model name is in the GET /v1/models listAbsent = not listed or not priced; switch to a listed model
Balance did not change immediatelyThe settlement cycle (≤5 minutes, about 15 at peak)Ledger and balance are the settled view — check again shortly
Streaming is not incrementalWhether curl has -N, whether the client buffersSee the streaming section
5xxNote the X-Cy-Request-IdReport with it so the operator can find the platform log
When reporting an issue include: the X-Cy-Request-Id (if present), HTTP status, error.code, the time (with timezone) and the model name. Do not include the key in plaintext.

Changes & support

  • This page is updated with the platform. The protocol is backwards compatible: new capabilities never change the request/response shape of existing endpoints;
  • Error message wording may change — always branch on HTTP status plus error.code;
  • The model catalog and the price book change with operations — treat GET /v1/models and GET /v1/prices as the live source of truth and never hard-code them;
  • For plan, top-up or unit-price changes, see the portal pricing page or contact the operator.