Docs

API Documentation

Integration guide for the IWKey unified gateway: available models, quick start, endpoints, code examples, SDK setup, and FAQ.

Text call · cURL
curl https://iwkey.com/v1/chat/completions \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-opus-5-5", "messages": [ {"role": "user", "content": "Hello"} ] }'
Video model task · cURL
curl https://iwkey.com/videos/v1/videos/generations \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "doubao-seedance-2.0", "prompt": "A cinematic product shot", "duration": 5, "resolution": "480p", "aspect_ratio": "16:9" }'

01One key for text and video models

One place for Claude, ChatGPT, and video model endpoints, model ID lookup, and status; see the models page for the full catalog.

API definition: /docs/openapi.json. Model details follow live /v1/models and the model market.

curl · List models
curl https://iwkey.com/v1/models \ -H "Authorization: Bearer $IWKEY_KEY"

Last updated: 2026-10-13

02Three steps to integrate

STEP 01

Request an API key

Log in to the console to create an API key.

STEP 02

Choose an endpoint

Text models swap the SDK base URL; video models submit tasks through /videos/v1/videos/generations.

STEP 03

Review results and usage

Text requests return in real time; video tasks are polled until complete, then results can be downloaded and roll up into usage records.

Last updated: 2026-09-04

03Endpoints

GET

/v1/models

Query current callable model IDs before integrating. ChatGPT model IDs follow this list.

POST

/v1/chat/completions

OpenAI-compatible endpoint. After swapping the base URL, the OpenAI SDK, Cursor, Codex CLI, opencode, and LangChain can all call through here.

POST

/v1/messages

Anthropic Messages-native endpoint. Claude Code and the Anthropic SDK can switch directly to the IWKey base URL.

POST

/videos/v1/videos/generations

Video model async task endpoint. Submit a prompt, duration, resolution, and aspect ratio to receive a job_id and poll_url.

GET

/videos/v1/videos/jobs/{job_id}

Prefer the poll_url returned by submission; it provides task status, completed output, failure details, and the video download URL.

POST

/videos/api/v3/contents/generations/tasks

Volcano Ark native-format entry for video models. model accepts the official IDs (doubao-seedance-2-0-260128 / doubao-seedance-2-0-fast-260128 / doubao-seedance-2-5-260628); unsupported top-level parameters return an explicit error and are never silently dropped.

GET

/videos/api/v3/contents/generations/tasks/{id}

Ark native-format task query entry; returns Ark status values (queued / running / succeeded / failed / cancelled / expired). On success content.video_url is the generation engine's official link (typically valid ~24 hours), identical to video_url on the standard endpoint. When a platform copy exists, content.platform_video_url and content.platform_expires_at are also included (kept for 7 days on our side; these two fields are returned additionally by this platform and are absent from the Ark native format).

Developers already using the Volcano Ark native request format can use the Ark-compatible endpoint above directly; see the specific differences from Ark native in the Video Model API Integration Guide's "Ark-compatible endpoint: differences from Ark native".

Last updated: 2026-09-24

04Advanced scenario examples

Advanced text capabilities and video model async task examples — each scenario shown in cURL / Python / JavaScript.

cURL · streaming response (SSE)
curl -N https://iwkey.com/v1/chat/completions \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-opus-5-5", "messages": [{"role": "user", "content": "Write a short poem about clouds"}], "stream": true }'
Python · streaming response
from openai import OpenAI client = OpenAI(base_url="https://iwkey.com/v1", api_key="$IWKEY_KEY") stream = client.chat.completions.create( model="claude-opus-5-5", messages=[{"role": "user", "content": "Write a short poem about clouds"}], stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="")
JavaScript · streaming response
import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://iwkey.com/v1", apiKey: process.env.IWKEY_KEY, }); const stream = await client.chat.completions.create({ model: "claude-sonnet-5-5", messages: [{ role: "user", content: "Write a short poem about clouds" }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0].delta.content ?? ""); }
cURL · video model async task
# 1. Submit job and capture the returned poll_url POLL_URL=$(curl -sS https://iwkey.com/videos/v1/videos/generations \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "doubao-seedance-2.0", "prompt": "A cinematic product shot on a clean studio desk", "duration": 5, "resolution": "480p", "aspect_ratio": "16:9" }' | jq -r '.poll_url') # 2. Poll the exact URL returned by the API curl "$POLL_URL" \ -H "Authorization: Bearer $IWKEY_KEY"
cURL · Volcano Ark native format
# 1. Submit the task (Ark native shape: content array + ratio field) TASK_ID=$(curl -sS https://iwkey.com/videos/api/v3/contents/generations/tasks \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "doubao-seedance-2-0-260128", "content": [ { "type": "text", "text": "A cinematic product shot on a clean studio desk" } ], "duration": 5, "resolution": "480p", "ratio": "16:9" }' | jq -r '.id') # 2. Query the task (content.video_url = official link, ~24h; # content.platform_video_url is our 7-day copy when present) curl "https://iwkey.com/videos/api/v3/contents/generations/tasks/$TASK_ID" \ -H "Authorization: Bearer $IWKEY_KEY"
Python · video model async task
import os import time import requests headers = { "Authorization": f"Bearer {os.environ['IWKEY_KEY']}", "Content-Type": "application/json", } job = requests.post( "https://iwkey.com/videos/v1/videos/generations", headers=headers, json={ "model": "doubao-seedance-2.0", "prompt": "A cinematic product shot on a clean studio desk", "duration": 5, "resolution": "480p", "aspect_ratio": "16:9", }, ) job.raise_for_status() poll_url = job.json()["poll_url"] while True: result = requests.get(poll_url, headers=headers) result.raise_for_status() data = result.json() if data["status"] in {"completed", "failed", "cancelled", "timeout"}: print(data) break time.sleep(5)
JavaScript · video model async task
const headers = { Authorization: `Bearer ${process.env.IWKEY_KEY}`, "Content-Type": "application/json", }; const submit = await fetch("https://iwkey.com/videos/v1/videos/generations", { method: "POST", headers, body: JSON.stringify({ model: "doubao-seedance-2.0", prompt: "A cinematic product shot on a clean studio desk", duration: 5, resolution: "480p", aspect_ratio: "16:9", }), }); const job = await submit.json(); while (true) { const res = await fetch(job.poll_url, { headers }); const result = await res.json(); if (["completed", "failed", "cancelled", "timeout"].includes(result.status)) { console.log(result); break; } await new Promise((resolve) => setTimeout(resolve, 5000)); }
Video task parameter reference
Parameter Type Description
model string doubao-seedance-2.0 / doubao-seedance-2.0-fast / doubao-seedance-2.5 / MiniMax-H3
prompt string Text prompt, required
duration integer Duration in seconds: doubao-seedance-2.0 / doubao-seedance-2.0-fast and MiniMax-H3 are 4–15, doubao-seedance-2.5 is 4–30
resolution string Per model: doubao-seedance-2.0 takes 480p / 720p / 1080p / 4k; doubao-seedance-2.0-fast takes 480p / 720p; doubao-seedance-2.5 takes 480p / 720p / 1080p; MiniMax-H3 takes 768p / 2k
image_urls array ≤9 (≤30 for doubao-seedance-2.5) Reference images. Each item is either a URL string (default role = first_frame, image-to-video) or an object {"url":"…","role":"first_frame|last_frame|reference_image"}
video_urls array ≤3 (≤10 for doubao-seedance-2.5) Reference videos
audio_urls array ≤3 (≤10 for doubao-seedance-2.5) Reference audio; doubao-seedance-2.5 accepts audio on its own; for doubao-seedance-2.0, doubao-seedance-2.0-fast and MiniMax-H3 it cannot be used alone — must pair with a role:"reference_image" image or video_urls; first/last-frame images cannot be combined with audio
generate_audio boolean Whether output includes audio, default false
cURL · with reference image and audio
curl https://iwkey.com/videos/v1/videos/generations \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "doubao-seedance-2.0", "prompt": "Product showcase video, soft lighting, slow push-in", "duration": 5, "resolution": "720p", "image_urls": [ {"url": "https://example.com/product.jpg", "role": "reference_image"} ], "audio_urls": ["https://example.com/bgm.mp3"], "generate_audio": true }'

Billing: pre-held on submission, then settled on actual usage, with any difference refunded or charged; failed tasks are fully refunded.

Video link fields
Field Description
video_url Original video link from the generation engine, valid for about 24 hours (expires_at is the authoritative expiry).
platform_video_url Platform storage link, kept for 7 days from job completion; once expired the file is deleted and cannot be recovered. null when the job has no platform copy — use video_url instead.
platform_expires_at Expiry time for platform_video_url.
cURL · tool calling (Function Calling)
curl https://iwkey.com/v1/chat/completions \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-opus-5-5", "messages": [{"role": "user", "content": "What's the weather like in Hangzhou?"}], "tools": [{ "type": "function", "function": { "name": "get_weather", "parameters": { "type": "object", "properties": {"city": {"type": "string"}} } } }] }'
Python · tool calling
from openai import OpenAI client = OpenAI(base_url="https://iwkey.com/v1", api_key="$IWKEY_KEY") resp = client.chat.completions.create( model="claude-opus-5-5", messages=[{"role": "user", "content": "What's the weather like in Hangzhou?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, }, }, }], ) print(resp.choices[0].message.tool_calls)
JavaScript · tool calling
import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://iwkey.com/v1", apiKey: process.env.IWKEY_KEY, }); const resp = await client.chat.completions.create({ model: "claude-sonnet-5-5", messages: [{ role: "user", content: "What's the weather like in Hangzhou?" }], tools: [{ type: "function", function: { name: "get_weather", parameters: { type: "object", properties: { city: { type: "string" } }, }, }, }], }); console.log(resp.choices[0].message.tool_calls);

The platform uses official standard model IDs. Reasoning depth is controlled via API parameters — Anthropic uses the thinking object, OpenAI uses reasoning_effort.

Anthropic Claude · Enable Extended Thinking
curl https://iwkey.com/v1/messages \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 16000, "thinking": { "type": "adaptive", "display": "summarized" }, "output_config": { "effort": "high" }, "messages": [{ "role": "user", "content": "Explain the mechanism of quantum entanglement" }] }' # Claude Opus 5.5 / Sonnet 5 / Fable 5.1: use thinking "adaptive"; set depth with output_config.effort (low / medium / high / xhigh / max)
Python · Anthropic SDK
import anthropic client = anthropic.Anthropic( base_url="https://iwkey.com", api_key="$IWKEY_KEY", ) resp = client.messages.create( model="claude-opus-5-5", max_tokens=16000, thinking={"type": "adaptive", "display": "summarized"}, output_config={"effort": "high"}, messages=[{"role": "user", "content": "Explain the mechanism of quantum entanglement"}], ) for block in resp.content: if block.type == "thinking": print("[thinking]", block.thinking[:200], "...") elif block.type == "text": print("[answer]", block.text)
OpenAI · reasoning_effort parameter
curl https://iwkey.com/v1/chat/completions \ -H "Authorization: Bearer $IWKEY_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-6-sol", "reasoning_effort": "high", "messages": [{ "role": "user", "content": "Explain the mechanism of quantum entanglement" }] }' # reasoning_effort: "low" | "medium" | "high" (default: medium) # Applies to gpt-6-sol and other reasoning models; check /v1/models for current IDs
Python · OpenAI SDK
from openai import OpenAI client = OpenAI(base_url="https://iwkey.com/v1", api_key="$IWKEY_KEY") resp = client.chat.completions.create( model="gpt-6-sol", reasoning_effort="high", messages=[{"role": "user", "content": "Explain the mechanism of quantum entanglement"}], ) print(resp.choices[0].message.content)

Last updated: 2026-09-29

05Client SDK configuration examples

Claude Code

All Claude models
⚠ ANTHROPIC_BASE_URL must stop at the hostname https://iwkey.com — do not add /v1 (unlike OpenAI; adding it would form /v1/v1/messages → 404)
# Set environment variables, then launch directly export ANTHROPIC_BASE_URL=https://iwkey.com export ANTHROPIC_API_KEY=your-iwkey-key claude

Cursor

Claude + ChatGPT
⚠ Base URL must include /v1 (Cursor appends /chat/completions directly to this OpenAI-protocol endpoint — unlike Claude Code's ANTHROPIC_BASE_URL, which does not take /v1)
# Settings → Models → Override OpenAI Base URL Base URL: https://iwkey.com/v1 API Key: your-iwkey-key # Claude: use model IDs directly (e.g. claude-sonnet-5-5); ChatGPT model IDs: check /v1/models first

Anthropic Python SDK

All Claude models
import anthropic client = anthropic.Anthropic( base_url="https://iwkey.com", api_key="your-iwkey-key", ) msg = client.messages.create( model="claude-opus-5-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], )

OpenAI Python SDK

Claude + ChatGPT
# Swap providers by changing the model name from openai import OpenAI client = OpenAI( base_url="https://iwkey.com/v1", api_key="your-iwkey-key", ) # Claude resp = client.chat.completions.create( model="claude-opus-5-5", messages=[{"role": "user", "content": "Hello"}], ) # ChatGPT — same client; check /v1/models for the current model ID resp = client.chat.completions.create( model="gpt-6-sol", messages=[{"role": "user", "content": "Hello"}], )

Video model API

Seedance / Video tasks
# Standard HTTP call: submit a task, then poll poll_url curl https://iwkey.com/videos/v1/videos/generations \ -H "Authorization: Bearer your-iwkey-key" \ -H "Content-Type: application/json" \ -d '{"model":"doubao-seedance-2.0","prompt":"A product video","duration":5,"resolution":"480p","aspect_ratio":"16:9"}'

Codex CLI

ChatGPT series (Responses API)
⚠ base_url must stop at /v1 — do not add /chat/completions, or Codex will build a bad path and return 404
⚠ Use model gpt-6-sol. Codex uses the Responses API (not chat/completions) — declare wire_api = "responses" in config.toml
# ~/.codex/config.toml model = "gpt-6-sol" model_provider = "iwkey" [model_providers.iwkey] name = "IWKey" base_url = "https://iwkey.com/v1" wire_api = "responses" env_key = "IWKEY_KEY"
export IWKEY_KEY="sk-your-api-key" codex

opencode

Claude + ChatGPT
⚠ options.baseURL must include /v1 — @ai-sdk/openai-compatible appends /chat/completions directly and does not add /v1 for you
# ~/.config/opencode/opencode.json { "$schema": "https://opencode.ai/config.json", "provider": { "iwkey": { "npm": "@ai-sdk/openai-compatible", "name": "IWKey", "options": { "baseURL": "https://iwkey.com/v1", "apiKey": "{env:IWKEY_KEY}" }, "models": { "claude-sonnet-5-5": { "name": "Claude Sonnet 5" }, "gpt-6-sol": { "name": "GPT-6 Sol" } } } } }
# Set the env var, then run /connect to store the credential under provider id "iwkey" export IWKEY_KEY="your-iwkey-key" opencode # In the TUI: /connect → Other → paste your key under provider id "iwkey" # Then /models to pick one of the models configured above

LangChain

All Claude models
from langchain_anthropic import ChatAnthropic llm = ChatAnthropic( base_url="https://iwkey.com", api_key="your-iwkey-key", model="claude-opus-5-5", )

Last updated: 2026-10-13

06Video models

Video generation is asynchronous with asset library support. See the dedicated guide: /en/docs/video-model/ (same API key, same base URL).

  1. 1Get your first video running in 3 minutes
  2. 2Reference images
  3. 3Asset library: the real-person channel
  4. 4Naming people & mention syntax
  5. 5API reference
    1. 5.1Submit a generation job · POST /videos/v1/videos/generations
    2. 5.2Estimate before submitting · POST /videos/v1/videos/generations/estimate
    3. 5.3Query a job · GET /videos/v1/videos/jobs/{job_id}
    4. 5.4List jobs · GET /videos/v1/videos/jobs
    5. 5.5Usage stats · GET /videos/v1/videos/jobs/stats
    6. 5.6Export · GET /videos/v1/videos/jobs/export.csv
    7. 5.7Asset library
    8. 5.8Upload endpoint (video workbench only — a platform integration usually doesn't need it)
    9. 5.9Ark-compatible endpoint: differences from Ark native
  6. 6Error codes
    1. 6.1Client & validation errors
    2. 6.2Auth & server errors
    3. 6.3Retry guidance
  7. 7Tenancy & isolation
  8. 8Quotas & limits
  9. 9Integration FAQ

Last updated: 2026-10-20

07Account balance

Check it before you start making calls, or from your own scheduler, to warn early when the balance is low, so requests are not rejected midway for insufficient balance. Read-only; querying is free.

GET https://iwkey.com/v1/usage

api.infwave.com works too. Authenticate with the same API key you call models with: Authorization: Bearer <your key> (the x-api-key header is also accepted).

curl example · check balance
curl https://iwkey.com/v1/usage \ -H "Authorization: Bearer <your API Key>"
Response example (regular key, illustrative values, common fields only)
{ "isValid": true, "mode": "unrestricted", "balance": 61.66, "remaining": 61.66, "unit": "USD", "usage": { "today": { "requests": 12, "total_tokens": 48210, "actual_cost": 0.42 }, "total": { "requests": 1893, "total_tokens": 9120000, "actual_cost": 38.34 }, "rpm": 0, "tpm": 0 } }

What you get back depends on the key: the same endpoint returns different things for the three kinds of key below, so confirm which one you use before integrating.

Key typeWhat is returned
Regular keyThe account's wallet balance: planName is a fixed wallet-balance label (returned in Chinese; the example above omits it), balance / remaining hold the balance, and unit is USD
Key with a quota cap or rate limitThat key's own remaining quota, not the account balance
Key in a subscription-plan groupThe plan's remaining allowance, plus daily / weekly / monthly usage, the matching limits and the expiry time

All three also include usage: today's and cumulative requests, tokens and cost, plus rpm and tpm.

Polling frequency: balances change slowly, so querying once an hour is enough.

Set a User-Agent when calling from code: when you call from Python or similar, send a User-Agent header; otherwise the request may be blocked by Cloudflare (a 403, or a failed connection). curl is not affected. The example script below already sets one.

Balance alerts

Get notified early when the balance runs low, so calls are not interrupted by insufficient balance. Run a scheduled job that calls the endpoint above once an hour and, when the balance falls below your own line, sends the alert to your Feishu group, DingTalk group, WeCom group, phone (SMS), Telegram or another channel. The example uses only the Python standard library; the NOTIFY_CHANNEL environment variable switches between Feishu, DingTalk, WeCom and Telegram, and SMS goes through your own SMS provider (see the table).

PlatformPrepare firstEnvironment variables
Feishu (feishu)Add a “Custom Bot” to the group and copy its webhook URL; if the bot has “Signature verification” on, copy its secret tooFEISHU_WEBHOOK; add FEISHU_SECRET if signature verification is on
DingTalk (dingtalk)Add a “Custom Robot” to the group and copy its webhook URL; if you chose “Sign” in its security settings, copy the secret tooDINGTALK_WEBHOOK; add DINGTALK_SECRET if “Sign” is on
WeCom (wecom)Add a “Group Bot” to the group and copy its webhook URLWECOM_WEBHOOK
SMSSign up with an SMS provider such as Alibaba Cloud SMS or Tencent Cloud SMS and get a sender signature and message template approved first. The example does not include SMS; replace its send function with your provider's send APIAs required by your provider (e.g. access key, signature, template ID, recipient phone number)
Telegram (telegram)Create a bot with @BotFather to get its token, add the bot to the target group or chat, and get the chat_id. Servers in mainland China usually cannot reach Telegram directly; you need a network that canTG_BOT_TOKEN, TG_CHAT_ID

Also set IWKEY_KEY (the API key you call models with); LOW_USD / CRIT_USD are the low / critical lines in US dollars, defaulting to 40 / 10. Treat the webhook URL and bot token like passwords and do not share them. The example reads balance: for a regular key that is the account wallet balance; for a key with a quota cap it is that key's remaining quota, so read your alert line accordingly; keys in a subscription-plan group return a different structure and are not covered by this example. Behaviour of the example: one alert each for Low and Critical, with no repeats inside a tier; it counts as recovered only after the balance climbs back above 1.2 times the low line; a failed query is not treated as a low balance, and three failures in a row raise a “monitor is down” alert; a message that fails to send is not recorded and is retried on the next run. Run it hourly with crontab: 0 * * * *.

Python example · push to a group when the balance is below your line (Feishu / DingTalk / WeCom / Telegram)
#!/usr/bin/env python3 """IWKey balance alert: check the balance hourly and push to Feishu / DingTalk / WeCom / Telegram when it falls below your line. Python standard library only. Environment variables: IWKEY_KEY the API key you call models with NOTIFY_CHANNEL feishu / dingtalk / wecom / telegram (pick one) Feishu: FEISHU_WEBHOOK (add FEISHU_SECRET if the bot has signature verification) WeCom: WECOM_WEBHOOK DingTalk: DINGTALK_WEBHOOK (add DINGTALK_SECRET if the bot uses "Sign" security) Telegram: TG_BOT_TOKEN, TG_CHAT_ID LOW_USD / CRIT_USD low / critical line in USD, default 40 / 10 Schedule it (crontab, hourly): 0 * * * * /usr/bin/python3 /path/to/balance_alert.py >> balance_alert.log 2>&1 Behaviour: one alert for Low and one for Critical; a failed query is not treated as low balance, and 3 failures in a row raise a 'monitor is down' alert.""" import base64 import hashlib import hmac import json import os import time import urllib.parse import urllib.request from pathlib import Path API = "https://iwkey.com/v1/usage" KEY = os.environ.get("IWKEY_KEY", "") CHANNEL = os.environ.get("NOTIFY_CHANNEL", "feishu") # feishu / wecom / dingtalk / telegram LOW = float(os.environ.get("LOW_USD", "40")) CRIT = float(os.environ.get("CRIT_USD", "10")) STATE = Path(os.environ.get("STATE_FILE", str(Path.home() / ".iwkey_balance_alert.json"))) TIERS = ["Normal", "Low", "Critical"] UA = "balance-alert/1.0" # a User-Agent is required, otherwise Cloudflare may block the request (403 / connection failure) def http_json(url, payload=None, headers=None): data = json.dumps(payload).encode() if payload is not None else None h = {"User-Agent": UA, **(headers or {})} if data: h["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=h) with urllib.request.urlopen(req, timeout=15) as r: return json.loads(r.read().decode()) def query_balance(): data = http_json(API, headers={"Authorization": f"Bearer {KEY}"}) usd = float(data.get("balance", data.get("remaining"))) return usd, f"${usd:.2f}" def send_once(text): if CHANNEL == "feishu": # Feishu custom-bot webhook payload = {"msg_type": "text", "content": {"text": text}} secret = os.environ.get("FEISHU_SECRET") if secret: # only needed if the bot has signature verification on ts = str(int(time.time())) key = f"{ts}\n{secret}".encode() payload["timestamp"] = ts payload["sign"] = base64.b64encode(hmac.new(key, b"", hashlib.sha256).digest()).decode() return http_json(os.environ["FEISHU_WEBHOOK"], payload).get("code") == 0 if CHANNEL == "wecom": # WeCom group-bot webhook res = http_json(os.environ["WECOM_WEBHOOK"], {"msgtype": "text", "text": {"content": text}}) return res.get("errcode") == 0 if CHANNEL == "dingtalk": # DingTalk group custom robot webhook url = os.environ["DINGTALK_WEBHOOK"] secret = os.environ.get("DINGTALK_SECRET") if secret: # only if the bot uses "Sign" security ts = str(int(time.time() * 1000)) digest = hmac.new(secret.encode(), f"{ts}\n{secret}".encode(), hashlib.sha256).digest() url += f"&timestamp={ts}&sign={urllib.parse.quote_plus(base64.b64encode(digest))}" res = http_json(url, {"msgtype": "text", "text": {"content": text}}) return res.get("errcode") == 0 if CHANNEL == "telegram": # Telegram bot (usually unreachable from mainland-China servers) url = f"https://api.telegram.org/bot{os.environ['TG_BOT_TOKEN']}/sendMessage" return http_json(url, {"chat_id": os.environ["TG_CHAT_ID"], "text": text}).get("ok") is True raise SystemExit("NOTIFY_CHANNEL must be feishu, wecom, dingtalk or telegram") def notify(text): for i in range(3): try: if send_once(text): return True print("message rejected, will retry") except Exception as e: print(f"send error: {type(e).__name__}") # log the exception type only, never the URL, so tokens stay out of logs time.sleep(2 ** i) return False def load_state(): try: return json.loads(STATE.read_text()) except Exception: return {"tier": 0, "fails": 0, "down_alerted": False} def main(): if not KEY: raise SystemExit("Set IWKEY_KEY first") st = load_state() try: usd, shown = query_balance() except Exception as e: st["fails"] += 1 print(f"query failed (#{st['fails']}): {type(e).__name__}") if st["fails"] >= 3 and not st["down_alerted"]: if notify("[IWKey balance monitor] 3 consecutive balance queries failed. The monitor may be down; check the network or API key."): st["down_alerted"] = True STATE.write_text(json.dumps(st)) return st["fails"], st["down_alerted"] = 0, False tier = 2 if usd < CRIT else 1 if usd < LOW else 0 if tier > st["tier"]: # alert only when it drops into a worse tier; no repeats within a tier line = CRIT if tier == 2 else LOW msg = (f"[IWKey balance {TIERS[tier].lower()}] Available balance is {shown}, " f"below the {TIERS[tier].lower()} line of ${line:.0f}. Please top up to avoid interrupted calls.") if notify(msg): st["tier"] = tier # record the tier only after a successful send; a failed send retries next run elif tier < st["tier"] and (tier > 0 or usd >= LOW * 1.2): # recovered only above 1.2x the low line, to avoid flapping around it st["tier"] = tier if tier == 0: notify(f"[IWKey balance recovered] Available balance is {shown}.") print(f"balance ${usd:.2f}, tier {TIERS[st['tier']]}") STATE.write_text(json.dumps(st)) if __name__ == "__main__": main()

Last updated: 2026-10-12

08Frequently asked questions

Do responses match official provider formats?
Yes. Responses pass through the upstream native fields (Anthropic's request_id, OpenAI's id and system_fingerprint). Text requests store no prompts or completions; see the transparency page for the full boundary.
How is billing handled?
Text models are billed by token usage; video models are billed by model and duration. Log in to the console to see real-time balance and usage detail, filterable by model, date, or key. Bank transfer and VAT invoice are supported.
Do you store my prompts and responses?
Text prompts and responses are not stored. Video models are async tasks — task parameters, status, cost, result URL, and failure reason are kept for task tracking and billing. See the transparency page for the full boundary.
Do you support streaming?
Text models support Anthropic-native SSE streaming and OpenAI-compatible streaming — set "stream": true in the request. Video models are not streaming responses; after submission, prefer the response's poll_url. Its current path is /videos/v1/videos/jobs/{job_id}.
How do I call video models?
Use the same API key to call POST /videos/v1/videos/generations. The response returns job_id and poll_url. After the task completes, check the polling response or "Video Model Usage" for the result, download URL, and failure reason.
How do I enable Extended Thinking or reasoning_effort?
The platform provides only official standard model IDs — suffix variants like -thinking/-high/-low are not offered. Reasoning depth is controlled via API parameters:
  • Anthropic Claude: for claude-opus-5-5, claude-sonnet-5-5 and claude-fable-5-1, add "thinking": {"type": "adaptive"} and set depth with "output_config": {"effort": "high"} (options: low/medium/high/xhigh/max).
  • OpenAI (reasoning models): add "reasoning_effort": "high" to the request body (options: low/medium/high). Supported models: gpt-6-sol and others — check GET /v1/models to confirm.
See the full example under the Code Examples → Extended Thinking tab.
How do I enable Prompt Caching?
💡 Prompt Caching: Claude models support prompt caching, saving up to ~90% on input cost for repeated long context (system prompts, agent loops, codebase context, etc). To enable: use the Anthropic-native /v1/messages format and mark cache_control breakpoints in the messages or system prompt. Note: the OpenAI-compatible /v1/chat/completions format does not support Claude prompt caching — every request is billed at full input price.
How is this different from calling Anthropic / OpenAI directly?
The text endpoints are wire-compatible with Anthropic / OpenAI, including streaming and tool calling. Video models use the IWKey async task endpoint. One key, one balance, and one usage record across both, plus CNY bank transfer and VAT invoices.

Last updated: 2026-10-08