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-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxIssued 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.
- Run the two free self-checks, /v1/models and /v1/credits/balance — they prove network, catalog, key and balance in one go;
- Wire up the SDK;
- If you need streaming, flip one parameter;
- 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.
| Form | Result |
|---|---|
| https://gw.example.cn/v1 | Correct |
| https://gw.example.cn/v1/ | Most SDKs tolerate it, but manual string concatenation yields //chat/completions |
| https://gw.example.cn | Missing /v1 → 404 |
| The console (admin) URL | The 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).
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.
Authorization: Bearer wxk-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| # | Rule | Response when violated |
|---|---|---|
| 1 | The Authorization value must start with Bearer (case-sensitive) | 401 + code: missing_api_key |
| 2 | Passing the key as a URL query parameter (e.g. ?api_key=) is not supported | 401 + code: missing_api_key |
| 3 | No other auth header names are read — sending one is equivalent to sending nothing | 401 + code: missing_api_key |
| 4 | The key does not exist or has been revoked | 401 + code: invalid_api_key |
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
Step 1 verifies network and catalog (no authentication, no key):
curl -sS "https://gw.cangyunai.com/v1/models"A 200 with a body like:
{"object":"list","data":[{"id":"model-a","object":"model"},{"id":"model-b","object":"model"}]}Step 2 verifies key and balance (with the key, not billed):
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:
{
"tenant_id": "t-example",
"balance_credits": 3800,
"inflight_credits": 1.25,
"available_credits": 3798.75,
"settle_interval_s": 300
}| Verified | How failure shows up |
|---|---|
| The network path to the gateway works and the base URL is right | connection timeout / 404 |
| The key is valid and not revoked | 401 (missing_api_key / invalid_api_key) |
| The account has usable balance | read 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:
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.
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)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 changes | Propagation |
|---|---|
| The operator lists or delists a model | immediately (hot configuration) |
| The upstream vendor delists a model | removed from the catalog once the operator syncs |
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.
curl -sS "https://gw.cangyunai.com/v1/prices"The response looks like:
{
"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.
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.
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: [DONE]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):
| Endpoint | Returns |
|---|---|
| GET /v1/credits/balance | balance, in-flight holds and usable balance (balance_credits / inflight_credits / available_credits) |
| GET /v1/credits/ledger?limit=N | entry-by-entry ledger: purchase / consume / adjust / refund / expire, with model and ref_id on consume entries |
| GET /v1/credits/usage?group_by=model | usage 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).
{
"error": {
"code": "insufficient_credits",
"message": "积分余额不足,请前往门户充值",
"balance_credits": 0.42,
"inflight_credits": 0.9,
"recharge_url": "https://www.cangyunai.com/console/billing"
}
}| HTTP | code | Meaning | Client action |
|---|---|---|---|
| 401 | missing_api_key | Authorization absent, or its value is not a Bearer token (case-sensitive) | Send Authorization: Bearer <key>; do not retry |
| 401 | invalid_api_key | The key is invalid or revoked | Check the key; if revoked, ask the operator to reissue; do not retry |
| 402 | insufficient_credits | Not enough usable credits (balance − in-flight holds ≤ 0) | Top up via error.recharge_url; retrying is useless |
| 404 | model_not_priced | The 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 endpoint | Check the base URL ends with /v1 and the path spelling |
Errors from the upstream or network stage:
| HTTP | Meaning | Client action |
|---|---|---|
| 5xx | Upstream model service failure or platform error | Report with the X-Cy-Request-Id; a few backed-off retries are fine |
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).
Troubleshooting checklist
Work down the list — most problems are located within the first four rows:
| Symptom | Check first | Notes |
|---|---|---|
| Connection timeout / refused | Host and port of the base URL | Confirm the address with the operator; confirm egress firewall allows it |
| 404 (no error.code) | Whether the base URL ends with /v1 | See the misspelling table in the base URL section |
| 401 missing_api_key | Whether Authorization is absent, or Bearer was written lowercase | The platform only accepts the exact Bearer prefix (with one space) |
| 401 invalid_api_key | Whether the key is complete and not revoked | Truncation while copy-pasting is the usual cause |
| 402 insufficient_credits | available_credits via GET /v1/credits/balance | Service resumes seconds after a top-up; do not retry |
| 404 model_not_priced | Whether the model name is in the GET /v1/models list | Absent = not listed or not priced; switch to a listed model |
| Balance did not change immediately | The settlement cycle (≤5 minutes, about 15 at peak) | Ledger and balance are the settled view — check again shortly |
| Streaming is not incremental | Whether curl has -N, whether the client buffers | See the streaming section |
| 5xx | Note the X-Cy-Request-Id | Report with it so the operator can find the platform log |
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.