The Doubao API offers an OpenAI-compatible interface, but "compatible" does not mean you can copy an OpenAI configuration verbatim. Three things trip people up most often: writing the Base URL as /v1, filling model with a display name instead of a model ID or endpoint ID, and enabling deep thinking on the first call so the output gets truncated. This article runs through curl and Python in order, then covers streaming, tool calling, common errors, and a migration checklist.
Note: Doubao, Volcengine Ark, Volcengine, and ByteDance are trademarks or product names of ByteDance and its affiliates; OpenAI is a trademark of its owner. This article is a technical integration guide only and implies no affiliation, agency, or endorsement. Interfaces, models, and quotas may change; refer to the official documentation and the on-site model pricing page.
1. Confirm three things before you start
| Item | Volcengine Ark direct | Unified gateway |
|---|---|---|
| Base URL | https://ark.cn-beijing.volces.com/api/v3 |
https://ai.zhongshangqihang.com/v1 |
| Authentication | Authorization: Bearer <ARK_API_KEY> |
Authorization: Bearer <gateway key> |
| model | Model ID or ep- endpoint ID |
Follow the on-site model list |
| Best for | Doubao only, self-managed verification, top-up, and invoicing | Multiple models, unified billing and quota |
The key and Base URL must be used as a pair. An official key pointed at a gateway address, or a gateway key pointed at the official address, usually returns 401 immediately. When switching entry points, change both together.
2. Base URL and request path: /api/v3, not /v1
The official data-plane address is https://ark.cn-beijing.volces.com/api/v3, the chat completion path is /chat/completions, and the full address is https://ark.cn-beijing.volces.com/api/v3/chat/completions. It does not have the /v1 prefix common in OpenAI setups; appending another /v1 returns 404.
The region is part of the domain (cn-beijing). If your resources are in another region, use the address given in the console and documentation. The official documentation also warns that cases such as a Coding Plan use a different Base URL; filling in the wrong one may incur extra charges, so check before switching plans.
3. What to put in model: model ID or endpoint ID
The official documentation describes two calling modes:
- Preset inference: put the model ID directly, for example
doubao-seed-2-1-pro-260915; no endpoint needs to be created in advance. - Custom inference endpoint: put the endpoint ID, usually starting with
ep-; this suits fixed deployments, fine-grained management, or LoRA fine-tuned models.
Model IDs carry a version date. Do not use display names such as "Doubao 2.1 Pro", and do not fill in suffixes from memory. When using Access Key (AK/SK) signatures, model must be an endpoint ID; with an API key, you may use a model ID or endpoint ID as documented. If you get InvalidEndpointOrModel.ModelIDAccessDisabled, the account does not support direct model-ID calls and you need to create an endpoint.
4. Run a minimal request with curl
Turn off deep thinking first to reduce variables. Example against the official endpoint:
export ARK_API_KEY="your Volcengine Ark API key"
export ARK_MODEL_ID="doubao-seed-2-1-pro-260915"
curl "https://ark.cn-beijing.volces.com/api/v3/chat/completions" \
-H "Authorization: Bearer $ARK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "'"$ARK_MODEL_ID"'",
"messages": [{"role": "user", "content": "Reply with OK only"}],
"thinking": {"type": "disabled"},
"max_tokens": 32
}'
A 200 response with content in choices[0].message.content means the key, address, and model are all correct. On a 404, first check whether model includes the version suffix and whether you need an ep- endpoint; on a 401, check the key and request header before suspecting the balance.
5. Call from Python with the OpenAI SDK
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["ARK_API_KEY"],
base_url="https://ark.cn-beijing.volces.com/api/v3",
)
resp = client.chat.completions.create(
model=os.environ.get("ARK_MODEL_ID", "doubao-seed-2-1-pro-260915"),
messages=[
{"role": "system", "content": "You are a concise technical assistant."},
{"role": "user", "content": "Explain what an inference endpoint is in three sentences."},
],
max_tokens=512,
extra_body={"thinking": {"type": "disabled"}},
)
print(resp.choices[0].message.content)
print(resp.usage)
The OpenAI SDK does not recognize the thinking field, so it is passed through extra_body; if the gateway or model does not support it, simply remove the field. Once the call works, look at the input and output tokens in usage before deciding on timeouts, concurrency, and budget.
6. Streaming: delta, reasoning_content, and [DONE]
With stream=True, the server returns server-sent events chunk by chunk, with response objects of type chat.completion.chunk. The body text is in choices[0].delta.content, thinking content is in delta.reasoning_content, finish_reason appears only in the last chunk, and the stream ends with data: [DONE].
stream = client.chat.completions.create(
model=os.environ["ARK_MODEL_ID"],
messages=[{"role": "user", "content": "Write a Python example that reads a CSV file"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
Thinking chunks may carry empty content, so do not assume every chunk has body text. A max_tokens value that is too small truncates output. If the network drops, content already received is still valid; the client should be able to save partial results and decide whether to retry.
7. Tool calling: tools, tool_calls, and message order
tools works much like OpenAI: each tool contains type: "function" and a function object, where name, description, and parameters are required, and parameters uses JSON Schema. When the model decides to call a tool, the response's finish_reason is tool_calls, and message.tool_calls[i].function.arguments is a JSON string that you must parse yourself.
The order of messages back to the model matters: first add the assistant message containing tool_calls to the conversation, then append one role: "tool" message per call with the matching tool_call_id and result, then request the model again until finish_reason is no longer tool_calls. parallel_tool_calls defaults to true; set it to false to allow only one tool call at a time. In streaming mode, tool-call deltas carry an index, and arguments must be accumulated by index.
8. Common errors: locating 401, 404, and 429
| HTTP | Typical error code | Meaning | Action |
|---|---|---|---|
| 401 | AuthenticationError |
Key missing or invalid, or the header lacks Bearer |
Copy the key again and check the Authorization header |
| 404 | InvalidEndpointOrModel.NotFound |
Model or endpoint does not exist, or no permission in this workspace | Copy the full ID from the console and check region and workspace |
| 404 | ModelNotOpen |
The model is not enabled in the account | Enable the model in the Ark console |
| 404 | InvalidEndpointOrModel.ModelIDAccessDisabled |
The account does not support direct model-ID calls | Create a custom endpoint and use the ep- ID |
| 400 | InvalidParameter, MissingParameter |
Missing parameter, wrong type, or parameter unsupported by the model | Compare with the documentation, remove or fix fields, and run the minimal request first |
| 403 | AccessDenied, AccountOverdueError |
No permission, or the account balance is negative | Check key and workspace permissions, and resolve the arrears |
| 429 | RateLimitExceeded.* |
RPM/TPM limit exceeded or a traffic spike | Reduce concurrency, use exponential backoff, request a quota increase if needed |
| 500/503 | InternalServiceError, ServerOverloaded |
Upstream error or overload | Wait briefly and retry, and prepare a fallback channel |
9. Migration checklist
When moving from direct official access to a gateway, or from one gateway to another, check in this order:
- Fix a set of real prompts and record current output, latency, and
usageas a baseline. - Change
base_urlandapi_keytogether, never just one. - Reconfirm model name mapping against the on-site model pricing; do not keep old names.
- Verify streaming: time to first token,
delta.content,finish_reason, and[DONE]. - Verify tool calling:
tools,tool_calls,tool_call_idround-trips, and multi-turn ordering. - Verify whether extension parameters such as
thinkingandreasoning_effortare supported and whether defaults differ. - Set connect and read timeouts, retry only on 429, 5xx, and timeouts, and make write operations idempotent.
- Move 5 to 10 percent of traffic first and observe for 3 to 7 days, keeping direct official access as a rollback path.
If you need a unified entry point, you can create a test key on the sign-up page; see model pricing for model coverage and billing. ZSQH AI Gateway is an independent third-party service with no affiliation with ByteDance.
Related reading:
FAQ
Should the Base URL include /v1?
No. The OpenAI-compatible endpoint of Volcengine Ark is https://ark.cn-beijing.volces.com/api/v3, and the chat path is /chat/completions; appending /v1 returns 404. Gateway addresses usually end in /v1, so do not mix the two.
Should model be a model ID or an ep- endpoint ID?
Use the model ID for preset inference, the ep- ID for a custom endpoint, and an endpoint ID when using Access Key signatures. If you get ModelIDAccessDisabled, you need to create an endpoint, not change the key.
Should I enable deep thinking on the first call?
It is best to turn it off and confirm the connection with the shortest possible request. Some Doubao models enable deep thinking by default; with it on, thinking content is produced first and a small max_tokens can truncate the actual answer. Enable it per task once the connection is confirmed.
Why does streaming return no content?
First confirm stream=True, then check whether you are reading only delta.content and ignoring empty chunks; thinking content is in delta.reasoning_content. Also check max_tokens, network buffering, and whether a proxy is collapsing the SSE stream into a single response.
Why must the assistant tool_calls message be sent back?
The protocol requires the assistant message to carry tool_calls, and each subsequent role: "tool" message must match a result by tool_call_id. Skipping this step can cause the model to repeat tool calls or return a 400. In streaming mode, parameters must also be accumulated by index.
