Praxis Cloud developer documentation

2026-10-22

Overview

Run agents directly, or use the base capabilities you need

The agent service encapsulates model calls, tool execution and more, completing business tasks through the agent API. When you need direct control over models, sandbox or search, use the corresponding service on its own.

Get started

01

Choose a service

Run agents, or call models, sandbox and search directly.

02

Create a cloud key

Create an API key in the Praxis Cloud console and learn about authentication and permission scopes.

03

Complete your first call

Open the quickstart of your chosen service, complete a request and view the result.

Choose the service you need

Every service has its own quickstart, guides and API.


Agents

Define responsibilities, provide materials and run tasks. The agent service manages underlying capability calls.

Quickstart

Create and run your first agent

Core concepts

Agents & runs

Sessions, memory & collaboration

Guides

Configure responsibilities & capabilities

Provide files & materials

Use tools & organize collaboration

Integrate your application

Technical reference

Limits & error handling

Agents API


Models

Call models through one unified interface to generate text and process multimodal input.


Sandbox

Execute code, operate files and use a browser in an isolated environment.

Quickstart

Create a sandbox and run one task

Core concepts

Sandboxes & lifecycle

Execution environment & resources

Guides

Run commands · Manage files

Use browser & network

Recycle environments & handle failures

Technical reference

Limits & error handling

Sandbox API


Query for the information you need and get results with sources.

Quickstart

Complete your first search

Core concepts

Search scope & queries

Results, ranking & sources

Guides

Set search criteria

Read & filter results

Use search in your application

Technical reference

Limits & error handling

Search API

Manage your cloud services

Identity & permissions

Manage cloud keys & authorization scopes

Protect service access credentials

Usage & billing

Metering & usage queries

Invoices & cost management

Quotas & limits

Check service quotas

Understand request and resource limits

Observability & troubleshooting

Request IDs, logs & debugging

Locating service failures

API

Agents / Models / Sandbox / Search — request parameters, response structures, error codes and code examples. To verify a call, expand the debug panel on the page, configure and send the request.

Browse all APIs →

第一个请求

不想写代码?直接看接入方式,把 Praxis 接进 Claude Code、Cursor、Cherry Studio 等客户端。

前提

一个 Praxis 账号。新注册账号自带 $1 免费额度,跑完本页绰绰有余,不需要先充值。

1. 创建 API Key

在 控制台 → API Keys 创建一个 Key。名称必填,有效期默认 90 天,创建后立即复制保存。

2. 发出第一个请求

只需要一个地址:https://api.deepraxis.ai。用你已有的 SDK,把 base_url 指过来即可。

示范模型 deepseek-flash:便宜、快、中英文都好。

curl https://api.deepraxis.ai/openai/v1/chat/completions \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek-flash", "messages": [{"role": "user", "content": "你好"}]}'

用你熟悉的语言也行:

Python

from openai import OpenAI

client = OpenAI(
    base_url="https://api.deepraxis.ai/openai/v1",
    api_key="sk-praxis-你的Key",
)

completion = client.chat.completions.create(
    model="deepseek-flash",
    messages=[{"role": "user", "content": "你好"}],
)
print(completion.choices[0].message.content)

Node

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.deepraxis.ai/openai/v1",
  apiKey: process.env.PRAXIS_API_KEY,
});

const res = await client.chat.completions.create({
  model: "deepseek-flash",
  messages: [{ role: "user", content: "你好" }],
});
console.log(res.choices[0].message.content);

3. 读懂响应

响应的 choices[0].message.content 是回复文本。usage 里有 token 数——注意 cost 字段直接给出本次调用的费用。

然后打开 控制台 → 用量与费用:你刚才这一次调用已经记在那里了。

下一步

选择服务

Praxis Cloud 提供多个服务域。你的场景决定用哪个——很多场景只需要一个。

模型(Model)

你要做什么:调用大语言模型(对话、写作、翻译、代码、分析)。

怎么用:通过 OpenAI 或 Anthropic 兼容 API 直接调用。把现有代码的 base_url 指向 Praxis 即可。

适合:已有应用要加 AI 能力、写 SDK 代码、接入第三方工具(Cursor、Cherry Studio 等)。

→ 模型快速开始 · 模型目录

智能体(Agent)

你要做什么:让 AI 在云端持续工作——有上下文、有记忆、能调用工具。

怎么用:通过 Agent API 创建会话、发送事件、获取回复。智能体可以记住之前的对话、执行多步任务。

适合:构建 AI 助手、客服机器人、有状态的工作流。

→ 智能体快速开始

沙箱(Sandbox)

你要做什么:在安全隔离的环境中运行代码。

怎么用:通过 Sandbox API 创建沙箱实例,上传代码并执行,获取结果。

适合:代码执行、自动化测试、数据处理管道。

→ 沙箱快速开始

搜索(Search)

你要做什么:给 AI 提供网络搜索能力。

怎么用:通过 Search API 执行搜索,返回结构化结果。

适合:构建有实时信息能力的 AI 应用。

→ 搜索快速开始

怎么选

你的场景推荐
只需要调模型(对话/生成)模型
需要 AI 有记忆和工具智能体
需要安全执行代码沙箱
需要搜索网络信息搜索
组合使用全部可以组合——智能体可以调用模型和搜索,沙箱可以运行任何代码

不确定的话:从模型开始——最简单、最直接,3 分钟就能跑通。

核心概念

理解这几个概念,使用 Praxis 会更顺手。

一个 Key,所有服务

Praxis Cloud 的所有服务(模型、智能体、沙箱、搜索)共用同一个 API Key。你不需要为不同服务创建不同的 Key——一个 Key 就够了。

兼容协议

Praxis 提供两种协议入口,你已有的代码不需要换库:

  • OpenAI 兼容(https://api.deepraxis.ai/openai/v1)——OpenAI SDK 改 base_url 即用
  • Anthropic 兼容(https://api.deepraxis.ai/anthropic)——Anthropic SDK 改 base_url 即用

两种协议背后的模型池是同一个——你可以在 OpenAI 协议下调 Claude 模型,也可以在 Anthropic 协议下调 DeepSeek 模型。

模型与路由

Praxis 聚合多家供应商的模型,统一以一个 API 提供。每个模型 ID 背后是具体的供应商路由——你不需要关心用的是哪家的服务,只需要选对模型 ID。

两种模型 ID:

  • 滚动入口(如 deepseek-flash)——始终指向最新一代,升级时无需改代码
  • 版本快照(如 deepseek-v4-flash)——固定在某一个版本,适合需要可复现性的场景

按次计费

每次 API 调用按实际 token 用量计费。输入和输出 token 分别计价,推理模型的思维链 token 也计费。不调用不收费——没有月费、没有最低消费。

API 响应中的 usage.cost 字段直接返回本次调用的费用,你可以在应用里实时追踪成本。

想深入某个服务?

Agents

Use agents to answer product questions, retrieve business data, or carry out tool tasks. You configure the model, instructions, and tools. Praxis manages session execution and event history; your application decides who can use the agent and which business operations they may perform.

Who this is for

Add a ready-made conversation interface to an existing product and pass in the signed-in user and current page. For custom interfaces, background tasks, or non-web clients, create sessions and read events through the API. Agents retrieve business data through authorized tools; your business server still enforces order ownership.

If you need a single model call or want to write the model and tool loop yourself, start with Models. Agents suit applications that need reusable configurations, ongoing conversations, and a record of task execution.

How the objects relate

ObjectResponsibilityRelationship
AgentName, instructions, model, and toolsSaving configuration creates a version; one agent can have many sessions
SessionA separate conversation and execution statePins an agent version at creation; later configuration edits do not replace that snapshot
EnvironmentMachine configuration for a sessionAPI sessions can omit the environment for a light machine, or select a sandbox environment
EventUser messages, agent replies, tool calls, and status changesBelongs to a session; accepting input is separate from producing a reply
End userA person using your productAn embed token binds session access to that user and agent

Service agents serve your customers with a separate environment per session. Personal agents bind to a persistent computer. The integration guides focus on service agents. See Agents and sessions for details. Public agent IDs have the form agent_…; use the value returned by creation or retrieval.

Choose a delivery format

FormatYou provideStart here
APIServer authentication, message and event handling, your own UIRun your first agent
PraxisKitA component in your website, identity, and business interactionsEmbed in a web page
iframeA token endpoint and a loader for the hosted UIEmbed with a script
HostedUser authentication and a redirect to a short-lived linkOpen hosted chat

React is one way to use Kit. Public documentation Q&A can use anonymous visitors. For account data, your backend must verify the signed-in user before issuing a token. See Choose an integration for the full comparison.

Continue by task

Next: API reference · Documentation Q&A recipe · Limits and errors

Run your first agent

Create a service-agent conversation through the API and verify its reply from the event history. This example runs on a server. It needs no web component or embed policy.

Prepare Bun or Node.js with native fetch, a Develop organization API Key, access to deepseek-flash, and available balance. Set PRAXIS_API_KEY in the server process; see API keys. If your Key belongs to another environment, align the API address, organization, and model before running. Do not mix environments.

1. Create an agent

Create first-agent.mjs with the code below. Append the next three JavaScript blocks to the same file, in order; do not run them separately. The assistant answers a simple arithmetic question so you can recognize a completed first call. Add business tools after connecting successfully.

POST /agent/v1/agents creates a configuration from its name, kind, model, and instructions. tools: [] declares no additional tools. The request helper checks each response and applies a timeout. Failures print only the status and request identifier, never the Key.

import assert from "node:assert/strict";
import { setTimeout as delay } from "node:timers/promises";

const { PRAXIS_API_KEY } = process.env;
assert(PRAXIS_API_KEY, "Set PRAXIS_API_KEY in the server process");
const base = "https://api-develop.deepraxis.com/agent/v1";
async function request(path, body) {
  const response = await fetch(`${base}${path}`, {
    method: body === undefined ? "GET" : "POST",
    headers: {
      Authorization: `Bearer ${PRAXIS_API_KEY}`,
      "Content-Type": "application/json",
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    signal: AbortSignal.timeout(30_000),
  });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}; request=${response.headers.get("X-Request-Id") ?? response.headers.get("request-id")}`);
  }
  return response.json();
}
const agent = await request("/agents", {
  name: "First support agent", kind: "service", model: "deepseek-flash",
  system: "Answer the user's question briefly. Do not use tools for this exercise.",
  tools: [],
});
assert(agent.id.startsWith("agent_"));
console.log({ agent: agent.id, version: agent.version });

Expected result: Creation returns HTTP 200 with a public id and a version. The script prints both and reuses them in later requests. Do not construct IDs yourself.

2. Open a session

Append this code. POST /agent/v1/sessions pins the version you just created. Omitting environment_id selects a light machine; you do not need to create a sandbox first. This example explicitly sets a budget of 100 US cents. That ceiling is not an estimate of the cost of this question.

const session = await request("/sessions", {
  agent: { type: "agent", id: agent.id, version: agent.version },
  title: "First API conversation",
  budget: { type: "limit", max_list_cost: { amount: "100", currency: "USD" } },
});
assert(session.id);
console.log({ session: session.id });

Expected result: HTTP 201 with the session id. No user question has been sent yet, so an idle status cannot prove that a reply exists. The budget controls whether execution may continue; it does not guarantee a particular answer.

3. Send the first message

Append this code to call POST /agent/v1/sessions/{session_id}/events. The events array contains one user.message with exactly one nonempty text block.

const accepted = await request(`/sessions/${session.id}/events`, {
  events: [{ type: "user.message", content: [{ type: "text", text: "What is 2 + 2?" }] }],
});
assert(accepted.data.some(event => event.type === "user.message"));

Expected result: HTTP 200 with the accepted user event in data. This acknowledges the input; it is not the agent's answer. Do not resend the message just because the answer has not appeared yet.

4. Read and verify the reply

Append this code, then run node first-agent.mjs or bun first-agent.mjs. It reads every page from GET /agent/v1/sessions/{session_id}/events, finds an agent.message, and then finds a subsequent session.status_idle. It does not mistake the initial idle state for completion.

let completed = false;
const deadline = Date.now() + 120_000;
while (Date.now() < deadline) {
  const events = [];
  let page;
  do {
    const query = new URLSearchParams({ order: "asc", limit: "100" });
    if (page) query.set("page", page);
    const result = await request(`/sessions/${session.id}/events?${query}`);
    events.push(...result.data);
    page = result.next_page;
  } while (page && Date.now() < deadline);
  const reply = events.findIndex(event => event.type === "agent.message");
  const idle = events.findIndex((event, i) => i > reply && event.type === "session.status_idle");
  if (reply >= 0 && idle > reply) {
    const text = events.filter(event => event.type === "agent.message")
      .flatMap(event => event.content ?? []).filter(block => block.type === "text")
      .map(block => block.text).join("\n");
    assert(text.trim(), "Expected a nonempty reply");
    console.log({ reply: text, completed: true });
    completed = true;
    break;
  }
  await delay(1500);
}
assert(completed, `No completed reply before timeout; inspect session ${session.id}`);

Completion: The process exits successfully and prints a nonempty reply with completed: true. The answer should express 4. The 120-second deadline belongs to this example, not to the service. A nonempty-text assertion alone does not validate business correctness; prepare expected answers for your actual use case before launching.

Troubleshoot failures

SymptomNext action
HTTP 401Check that the server Key exists, remains valid, and belongs to this environment
HTTP 400Check request fields and model availability; see Limits and errors
HTTP 404Use IDs returned by creation and check the organization and pinned version
HTTP 429 or a temporary upstream failureBack off according to the response; inspect the session before creating resources or sending inputs again
No completed reply before timeoutRetain the session ID and inspect events for tool approval, budget, or execution failures; the creation status alone is insufficient

Running the complete script again creates another agent and session. During troubleshooting, query the IDs already printed first. Once you no longer need the test records, manage them through the API reference. A running session cannot be treated as a record ready for deletion.

Next steps

Configure an agent, read Agents and sessions, or add PraxisKit to a web page.

Give this to your agent:

Follow /en/docs/developers/agent/quickstart on the server.
Ask me for a Develop organization API Key and confirm deepseek-flash access.
Complete when a nonempty agent.message is followed by session.status_idle.
Keep the Key out of browsers and logs. Record the agent ID, version, and session ID.

Agents and sessions

An agent is a reusable configuration. A session is a separate conversation running with that configuration. Keeping these objects distinct helps you determine who a configuration change affects, where to send a message, and how to recognize completion.

Configuration and execution objects

ObjectContainsCreation and use
AgentModel, instructions, tools, MCP configuration, and a versionCreation returns an agent_… ID that multiple sessions can reuse
SessionA pinned version, message and tool events, execution stateCreate it from an agent ID or versioned reference, then send input to it
EnvironmentMachine configuration for a sessionSelect an environment for sandbox execution; basic API sessions can omit it
EventUser input, replies, tool execution, and status changesSubmit input events; read history or subscribe to the event stream
vaultCredentials for accessing tool servicesManage separately from instructions; prompts cannot enforce credential access

Service agents use a separate execution environment for each session. Personal agents bind to a persistent computer for tasks that retain a working directory. Choose the kind at creation. The API quickstart uses a service agent and needs no personal computer.

Versions and the session lifecycle

Saving agent configuration creates a new version. When creating a session, agent can be an ID string to select the latest version at that moment. Alternatively, pass { type: "agent", id, version } to choose explicitly. The session pins that configuration; later agent edits do not upgrade existing sessions.

A session can be idle, running, rescheduling, or terminated. An idle session has no task currently running and can accept another message. A terminated session cannot continue. Archiving is separate: you can still read the record, but cannot send more messages. Decide whether you need to retain history or stop accepting input before cleaning up records.

A successful user.message response only acknowledges input. Read agent.message for the reply and a subsequent session.status_idle to confirm that the turn became idle. If a tool needs approval, handle the pending confirmation event. Waiting for a decision does not mean the API has stopped responding.

Environment and identity boundaries

An API session uses a light machine when environment_id is omitted and no computer is selected. Choosing an environment selects its sandbox configuration. That configuration is fixed when the session is created. File and tool capabilities depend on configuration; the word “environment” does not imply support for arbitrary runtimes.

Keep organization API Keys on your server. Customer-facing pages use embed tokens. Your backend derives external_user_id from verified sign-in state and issues a token for the selected agent. The token scopes access to an agent, version, and end user. A user ID supplied by the browser is not verified identity.

Public documentation Q&A can use a publishable key to obtain anonymous visitor tokens. The same browser storage preserves the visitor identity. This is not a signed-in account and grants no access to private orders. When users change, destroy the old component and obtain fresh credentials; clearing the message list is insufficient.

Tool confirmation records whether the user agrees to an operation. Your business service must still validate the caller's credential and the user's access to each resource. Page context, model-generated arguments, and an approval button cannot replace that check. Instructions and context are also unsuitable for secrets.

Choose your next step

Use Run your first agent to verify the API. Read Configure an agent to change configuration or select an embed version. The API reference defines creation and update fields.

For customer products, choose a delivery format, then connect identity through Embed tokens and users. Investigate execution through Sessions and usage, then apply Limits and errors.

Choose an integration

Choose where conversations appear, then decide who builds the interface. PraxisKit, iframe, and Hosted provide chat interfaces. Use the API when your application supplies its own interface.

Compare integrations

MethodWhere users chatWhat you buildAppearance settingsGet started
Kit componentInside your web pageHost token endpoint and component initializationsetOptions in host codeEmbed in a web page
iframeAn isolated frame in your pageHost token endpoint and one script tagSaved in the Dashboard; script can override layoutEmbed with one line
HostedA separate Praxis chat pageHost authentication, link issuance, and redirectShares saved iframe appearance; displays as a full pageOpen hosted chat
APIYour website, app, or serviceSession and event handling, plus any interfaceYour application controls itRun your first agent

Choose for your task

Choose Kit to configure the component in code, control when it opens, or connect it to page interactions. Kit is a Web Component that you can also mount in a React application. Your host code supplies its interface options.

Choose iframe to insert a ready-made chat interface and maintain its appearance in the Dashboard. “One line” refers to the loader script. You still implement the backend token endpoint for signed-in users. A bare iframe address does not authenticate a user.

Choose Hosted when a button should open a separate chat page. Your backend creates a short-lived link for the current user, then redirects their browser immediately. You do not install a component on your page. Obtain a fresh link for each entry; never share a user's link as a public entry point.

Choose the API for an existing chat interface or a client outside the browser. The product quickstart runs a session from your backend. Before exposing it to end users, follow the token guide to isolate each user's access.

Prepare identity and configuration

Signed-in integrations require a service agent, a saved embed policy, and a backend that authenticates host users. Keep the API key on that backend. The browser receives only the current end user's embed token. Changing the interface should not change the stable identifier you assign to that user.

For public question-answering without login, Kit supports anonymous visitors through a publishable key. Kit obtains the anonymous token. Anonymous identity does not replace account authentication or authorize access to orders. See Embed tokens and users.

Embed policies and persistent appearance can currently only be configured in the Dashboard. Kit instance appearance is configured in host code. Complete a real conversation with one integration before adding context, tools, or themes.

Next steps

Embed in a web page

Add PraxisKit to your page so signed-in users can talk to a service agent. This local order-assistant example covers backend issuance, mounting, and user isolation. For other interfaces, see Choose an integration.

You need Bun, an available service agent and runtime environment, and an API key in the same organization. The example connects to Develop. Store PRAXIS_API_KEY only in the backend environment. The browser authenticates with your host; your host verifies identity, requests a token from Praxis, and passes that token to Kit.

1. Prepare your agent

In the Develop Dashboard, open 服务智能体 (Service agents) and select an existing agent. Save its instructions under 接入与配置 → 配置 → 指令. Under 配置 → 安全, select a runtime environment, enable 允许 localhost, and click 保存安全设置. The example runs at http://localhost:4242. Use a dedicated test agent with an empty origin allowlist and localhost enabled. Local requests are rejected when the token inherits a nonempty HTTPS origin list. Add your website's origin when moving to HTTPS hosting.

Embed policies can currently only be configured in the Dashboard. Copy the public agent ID (agent_…). After the saved confirmation appears, continue with actual issuance; saving settings alone does not prove that chat works.

2. Create a token endpoint

Create a directory and save this as server.ts. This loopback-only demo uses HTTP Basic authentication for two test accounts. Set your own test password in DEMO_PASSWORD; it is unrelated to your Praxis API key. The server derives the user ID from the authenticated account and ignores any client-supplied user ID.

import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = "http://localhost:4242";
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (url.origin !== origin) return new Response("Use localhost:4242", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-token") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
        body: JSON.stringify({ agent: agentId, external_user_id: userId }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.token !== "string" || typeof data.expires_at !== "string") {
        throw new Error("Invalid token response");
      }
      return Response.json({ token: data.token, expires_at: data.expires_at }, { headers: noStore });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);

Set the three variables in the server process. Replace PRAXIS_AGENT_ID with the actual ID. Save index.html in the next step before starting.

export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in this server process, then run:
bun server.ts

Expected: the terminal prints the local URL. The host rejects unauthenticated requests. Only authenticated, same-origin requests reach Praxis. Praxis returns HTTP 201 on issuance; the host returns only token and expires_at, with caching disabled.

For an existing application, replace authenticate with its session validation. Mount the route behind login and CSRF protection, and recheck identity on every issuance. Do not use these test accounts as your production login system. See Embed tokens and users for expiration and refresh.

3. Mount the component

Choose one frontend implementation. The API address, CDN, and agent must use the same environment. These examples pin Kit 0.2.8 on Develop.

Save this as index.html in the same directory, then run the startup command above.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Order assistant</title>
<script type="module" src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js" integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU" crossorigin="anonymous"></script>
<praxis-agent style="display:block;height:600px;width:100%"></praxis-agent>
<p id="status" role="status"></p>
<script type="module">
  await customElements.whenDefined("praxis-agent");
  const agent = document.querySelector("praxis-agent");
  agent.addEventListener("praxis.error", () => {
    document.querySelector("#status").textContent = "Chat unavailable. Check your sign-in and configuration.";
  });
  agent.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin", cache: "no-store",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const data = await response.json();
        if (typeof data.token !== "string" || !data.token) throw new Error("Missing token");
        return data.token;
      },
    },
    agent: { name: "Order assistant" },
  });
</script>
</html>

Expected: a chat area appears with a height of 600 pixels. api.url sets the API address, getToken returns a token string, and agent.name sets the display name. Wait for registration before calling setOptions. The praxis.ready event only confirms mounting. A real reply is required to verify the integration.

4. Verify the integration

  1. Open http://localhost:4242. Sign in as customer_123 using your test password. Ask what order questions the assistant can help with and confirm a reply arrives.
  2. In the network panel, check that the host token request succeeds without returning the API key. Session requests should use an embed token. Do not copy credentials into logs or screenshots.
  3. In a separate browser profile or private window, sign in as customer_456. Expect no access to the first account's conversations. Two tabs sharing one login do not test isolation.
  4. Request the local token endpoint without credentials. Expect HTTP 401 from the host. In your application, also verify that signing out prevents further issuance.
curl --silent --output /dev/null --write-out "%{http_code}\n" \
  -X POST http://localhost:4242/api/praxis-token \
  -H "Origin: http://localhost:4242"

Troubleshoot failures

These are defined API failure cases. Check the actual response in your network panel:

  • Issuance HTTP 400: confirm that the embed policy is saved and the runtime environment is available.
  • Issuance HTTP 404: check the agent ID and confirm the API key and service agent belong to the same organization.
  • Chat returns EMBED_ORIGIN_NOT_ALLOWED (HTTP 403): check the page origin. Enable localhost for local testing or save the HTTPS origin for your hosted site.

Next steps

Embed with one line

Add one loader script to display a Praxis-hosted iframe chat interface. Save its appearance in the Dashboard while users stay on your page. “One line” describes frontend installation; your backend still issues tokens for signed-in users.

Prepare an HTTPS host

You need an HTTPS website whose pages and backend you can change, Bun, an API key in the agent's organization, and a service agent with a short number. This example connects to Develop. Your demo HTTPS entry point must forward to 127.0.0.1:4242 and preserve the original Host header. Your host supplies its domain and TLS certificate.

In the Develop Dashboard, open 服务智能体 → your agent → 接入与配置 → 配置 → 安全. Select a runtime environment, add the actual HTTPS origin under 来源白名单, and click 保存安全设置. For example, use https://support.example.com, without a path. Embed policies can currently only be configured in the Dashboard.

Enabling 允许 localhost alone does not authorize iframe embedding. This guide requires a saved HTTPS origin. For HTTP-only local testing, start with the Kit quickstart.

Record two identifiers. Token issuance uses the public ID, agent_…. The script's data-agent uses the short number copied from the Dashboard header: 5–8 letters or digits. Do not put the full ID in the short-number attribute.

Provide a token endpoint

Save this as server.ts behind your HTTPS entry point. It uses two password-protected test accounts. For production, replace authenticate with your host's session validation and retain CSRF protection. Set HOST_ORIGIN to the actual HTTPS origin, without a trailing slash. Configure all environment variables only on the backend.

import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = process.env.HOST_ORIGIN;
if (!origin || new URL(origin).protocol !== "https:") throw new Error("Set HOST_ORIGIN to your HTTPS origin");
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (request.headers.get("Host") !== new URL(origin).host) return new Response("Host rejected", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-token") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
        body: JSON.stringify({ agent: agentId, external_user_id: userId }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.token !== "string" || typeof data.expires_at !== "string") {
        throw new Error("Invalid token response");
      }
      return Response.json({ token: data.token, expires_at: data.expires_at }, { headers: noStore });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);

Start the server after saving index.html in the next section. DEMO_PASSWORD is your chosen test-account password, not an API key. Sign in through the browser as customer_123 or customer_456.

export HOST_ORIGIN="https://support.example.com"
export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in the server process.
bun server.ts

The loader sends a POST request to data-token-endpoint with browser credentials. That endpoint must derive the user ID from validated authentication on every issuance. After Praxis returns HTTP 201, the host returns token and expires_at to the loader. Keep the API key out of HTML and tokens out of page URLs.

Install the loader

Save this as index.html, replacing ABC123 with the actual short number. For an existing page, insert only the script tag where you want chat to appear.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Order assistant</title>
<body>
  <h1>Order support</h1>
  <script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" async></script>
</body>
</html>

The loader creates the iframe and transfers its token. Opening or pasting an iframe address alone does not supply the host user's identity.

Script attributePurpose
data-agentRequired agent short number
data-token-endpointRequired host token endpoint; same-origin in this example
data-layoutOptional: inline, popup, sidebar, or modal
data-launcherOptional: bubble, none, or an existing button's CSS selector

When layout and launcher attributes are absent, the loader uses saved Dashboard appearance. Without a saved layout, it uses popup. Explicit attributes override the corresponding settings. With inline, the script's location determines insertion; reserve enough height in its containing element.

Save appearance

Open 接入与配置 → 接入方式与外观 → iframe. Adjust the name, greeting, and layout, then click 保存. Expect confirmation that changes apply after refreshing the embedded page. Refresh your host page to verify. Persistent appearance can currently only be configured in the Dashboard.

With data-launcher="none", a floating layout has no default opening button. Supply an existing button's CSS selector, or control opening in host code. In this example, the button exists before the loader executes:

<button id="open-support" type="button">Open support</button>
<script src="https://kit-develop.praxisusercontent.com/v1/loader.js" data-agent="ABC123" data-token-endpoint="/api/praxis-token" data-layout="popup" data-launcher="#open-support" async></script>

This replaces the previous script. Keep only one loader script for the same agent on a page.

Verify and troubleshoot

Open your HTTPS page and sign in. Confirm that chat or its launcher appears, send a question, and receive a reply. Use a separate browser to sign in as the second test account. Confirm it cannot see the first account's conversations.

In the network panel, check that loader.js loads, the iframe comes from chat-develop.praxisusercontent.com, and the host token endpoint succeeds. The API key belongs only in server-to-server requests; it must not appear in browser network records.

  • Browser blocks the frame: check that the saved origin matches the page. Your host CSP must allow https://chat-develop.praxisusercontent.com in frame-src and the script CDN in script-src. Do not disable the entire CSP to troubleshoot.
  • Console reports Invalid Praxis loader configuration: check the short number format, script domain, and token endpoint attribute.
  • Host endpoint returns HTTP 401 or 403: restore host authentication or correct the same-origin check. Retrying does not sign the user in.

Test origin rejection by loading the script from another HTTPS site outside the allowlist; expect the browser to block embedding. After signing out of your host, request its token endpoint again and expect rejection. A hidden button does not prove authorization.

Reload the host page after sign-out or account changes so the backend issues for the new identity and removes the old interface. Opening or closing the loader only changes visibility; it does not sign the user out of your host.

Next steps

Open hosted chat

Add a button that takes signed-in users to a separate Praxis Hosted chat page. Your backend authenticates the user, issues a link, and redirects immediately. No chat component is installed on your host page.

Prepare your agent

You need Bun, an API key in the agent's organization, and a service agent with instructions, a runtime environment, and an embed policy. The agent must have a short number. Open it under 服务智能体 (Service agents) in the Develop Dashboard, check the short number in its header, and copy its public agent_… ID.

If the embed policy is not saved, open 接入与配置 → 配置 → 安全, select 运行环境, and click 保存安全设置. Embed policies can currently only be configured in the Dashboard. This example uses the Develop API and a local host at http://localhost:4242; chat opens on the hosted domain.

Hosted is a separate page and cannot be embedded in an iframe. To keep users inside the current page, choose iframe or the Kit component.

Issue a link and redirect

Save this complete server as server.ts. It binds only to loopback, authenticates two test accounts through HTTP Basic, and checks the form request's origin. PRAXIS_API_KEY stays on the backend. external_user_id comes from the authenticated account, never a form field.

import { timingSafeEqual } from "node:crypto";

const apiKey = process.env.PRAXIS_API_KEY;
const agentId = process.env.PRAXIS_AGENT_ID;
const password = process.env.DEMO_PASSWORD;
if (!apiKey || !agentId || !password) throw new Error("Set the three server variables");
const origin = "http://localhost:4242";
const noStore = { "Cache-Control": "no-store" };
function authenticate(request: Request): string | undefined {
  const supplied = Buffer.from(request.headers.get("Authorization") ?? "");
  for (const id of ["customer_123", "customer_456"]) {
    const expected = Buffer.from("Basic " + Buffer.from(`${id}:${password}`).toString("base64"));
    if (supplied.length === expected.length && timingSafeEqual(supplied, expected)) return id;
  }
}
Bun.serve({
  hostname: "127.0.0.1", port: 4242,
  async fetch(request) {
    const url = new URL(request.url);
    if (url.origin !== origin) return new Response("Use localhost:4242", { status: 403 });
    const userId = authenticate(request);
    if (!userId) return new Response("Sign in", {
      status: 401, headers: { ...noStore, "WWW-Authenticate": 'Basic realm="Local demo"' },
    });
    if (request.method === "GET" && url.pathname === "/") {
      return new Response(Bun.file("index.html"), { headers: noStore });
    }
    if (request.method !== "POST" || url.pathname !== "/api/praxis-hosted") {
      return new Response("Not found", { status: 404 });
    }
    if (request.headers.get("Origin") !== origin) {
      return new Response("Origin rejected", { status: 403 });
    }
    try {
      const response = await fetch("https://api-develop.deepraxis.com/agent/v1/embed_tokens", {
        method: "POST",
        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
        body: JSON.stringify({ agent: agentId, external_user_id: userId, hosted: true }),
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) return new Response("Token issuance failed", {
        status: response.status, headers: noStore,
      });
      const data = await response.json();
      if (typeof data.hosted_url !== "string") throw new Error("Missing URL");
      return new Response(null, {
        status: 302, headers: { ...noStore, Location: data.hosted_url },
      });
    } catch {
      return new Response("Token service unavailable", { status: 502, headers: noStore });
    }
  },
});
console.log(origin);

Setting hosted: true adds hosted_url to the issuance response. Successful issuance returns HTTP 201. The host responds with HTTP 302 and puts that URL in Location, taking the browser to chat. It does not return the API key or the issuance response's token to the form page.

Configure the actual agent ID, API key, and your chosen local test password in the server environment. Save the page in the next section, then start from the same directory:

export PRAXIS_AGENT_ID="agent_REPLACE_ME"
# Set PRAXIS_API_KEY and DEMO_PASSWORD in this server process, then run:
bun server.ts

Expected: the terminal shows the local address. Unauthenticated requests and requests from a different origin cannot issue links. In an existing application, replace test-account authentication with session validation and put the route behind login and CSRF protection. Do not use this test-account mechanism for production login.

Add an opening button

Save this as index.html in the same directory. A normal form submission follows the backend redirect. A background fetch request alone does not navigate the browser.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Order support</title>
<form action="/api/praxis-hosted" method="post">
  <button type="submit">Open order assistant</button>
</form>
</html>

Open http://localhost:4242, sign in as customer_123 using your test password, and click the button. Expect navigation to a chat page on chat-develop.praxisusercontent.com. Each click requests a fresh link from your host; do not cache the previous result. After navigation, check that the chat input becomes available.

Understand link lifetime

The #c=… fragment in hosted_url contains a single-use exchange credential, not an embed token. It is valid for 60 seconds after issuance and can be redeemed successfully only once. Do not log, bookmark, or share credential-bearing links. Authenticate each user on your host before entry.

The hosted page reads the credential, removes the fragment, and automatically submits code to POST /agent/v1/embed/exchange. It receives token and expires_at. Your backend must not redeem the credential first, which would consume the browser's link.

The 60-second link lifetime is separate from the embed token's lifetime. After exchange, the page uses that token until expiration. Hosted does not refresh tokens automatically. Expiration or a revocation error replaces chat with a notice to return to the original website. Reloading the hosted page cannot reuse the removed credential; return to the host and click again.

Verify the integration

Use this server-side request to check issuance shape. It prints only the grant ID, expiration, and whether a link exists, without printing credentials. This independent API check does not navigate a browser. Its fixed user ID is for controlled testing; your application must use authenticated identity.

set -o pipefail
curl --fail-with-body --silent --show-error \
  -X POST https://api-develop.deepraxis.com/agent/v1/embed_tokens \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"agent":"agent_REPLACE_ME","external_user_id":"customer_123","hosted":true}' \
  | jq '{id, expires_at, has_hosted_url: (.hosted_url | type == "string")}'

Then exercise the complete button flow. The network panel should show a host POST returning 302, followed by successful exchange on the hosted page. Send an order question and confirm a real reply. A loading interface alone does not establish success.

Enter as customer_456 in a separate browser and confirm it cannot read the first account's conversations. Unauthenticated host requests should return HTTP 401. To test link expiration, create a separate test link and open it after 60 seconds. Expect instructions to return to the host, rather than successful access through repeated retries.

Handle failures and appearance

Exchange returns NOT_FOUND (HTTP 404) for expired, used, and unknown credentials. Restart from the host in all three cases. Missing short numbers or embed policies cause HTTP 400 during issuance; complete those prerequisites in the Dashboard before retrying. For HTTP 429, wait according to Retry-After, then have the host issue a new link.

Save appearance under 接入与配置 → 接入方式与外观 → iframe. Hosted shares settings such as name, theme, and greeting. Persistent appearance can currently only be configured in the Dashboard. Hosted always displays a full-page conversation without a floating launcher. The Dashboard's Hosted → 打开预览 button is a preview and does not replace host issuance for real signed-in users.

Next steps

Configure your agent

Update a service agent's instructions, model and tools, then choose the version offered to end users. Saving configuration and selecting an embed version are separate actions. A pinned embed does not automatically adopt new configuration.

Prepare your agent

You need an existing service agent and an organization account authorized to manage it. Open Service agents → Select agent → Integration and configuration → Configuration in the Dashboard. UI labels below describe the current Chinese Dashboard.

For API access, obtain the public agent_… ID from a create or retrieve response. Set PRAXIS_API_BASE to your environment's API origin and PRAXIS_API_KEY to its organization API Key on your server. Keep the Key out of browser code. The example requires Node.js with native fetch and PRAXIS_AGENT set to the real public ID.

Update and save configuration

  1. In Instructions (指令), define the agent's role, answer scope and response when information is missing.
  2. In Model (模型), select an available model, such as deepseek-flash when listed for your organization.
  3. In Tools and authorization (工具与授权), enable tools and choose automatic execution or confirmation on every call. Configure MCP servers in MCP and credentials (MCP 与凭据). Tools must still authorize users on the server.
  4. Review changed groups in the bottom save bar and click Save (保存).

Expected: The configuration version increases. Reopening Instructions shows the saved text. Leaving with unsaved changes prompts you to handle them.

Save embed settings separately

Embed security policies, vault selection and embed version pinning can currently only be configured in the Dashboard.

In Configuration → Security (安全), select an available environment, add your website origin, and set token lifetime and budgets. Click Save security settings (保存安全设置). Lifetime inputs use minutes and budget inputs use US dollars. Do not paste API seconds or cents into these fields. See Secure your embed for origin rules.

For vaults, select them in MCP and credentials and click Save vaults (保存凭据库). Expected: Both saves display “已保存,不产生新版本” (saved without creating a version). These settings are separate from the configuration draft in the bottom save bar.

Select the embed version

Open Integration and configuration → Versions (版本). Click a version to compare it with its predecessor. Choose Set as embed version (设为嵌入版本) or Roll back to this version (回滚到此版本) and confirm. Check the Embedded (嵌入中) badge. Choose Unpin (取消固定) to follow the latest version again. Save a security policy first if none exists.

An explicit version in token issuance takes precedence over the pinned version. When omitted, issuance uses the pinned version, or the latest when unpinned. Existing tokens retain their version, including after rollback. Issue a new token and start a new session to verify a change.

Verify and continue

In Playground (试用调试), select the saved version and ask a question that distinguishes the new instructions from the old ones. Check the actual reply. Then obtain a new token in your host application, start a session, and confirm its version under Operations → Sessions. A loaded interface alone does not prove the new configuration is in use.

Service agents do not currently support per-message model switching, tool modes, voice input or per-end-user memory. Configuration's appearance section is a placeholder; use Customize appearance.

Next: Test your embed · Agent API · Cookbook

Embed tokens and users

Embed tokens let end users access a specific agent and their own sessions. For signed-in users, your backend issues the token and the browser consumes it. This guide covers identity, expiration, refresh, and single-token revocation.

You need a service agent in your organization, a saved embed policy, and a backend API key. Save the policy under 接入与配置 → 配置 → 安全 in the Dashboard; it can currently only be configured there. Examples use Develop. See Embed in a web page for a complete integration or Hosted for single-use redirects.

Identity and access

Your backend authenticates users through your login system, then supplies a stable external_user_id. The same organization, agent, and identifier refer to the same end user. This is neither an organization member ID nor a display name. Identifiers contain 1–128 characters: letters, digits, underscores, periods, colons, @, and hyphens.

CredentialConsumerPurpose
API keyHost backendIssue, inspect, and revoke tokens; never send it to the browser
Embed tokenEnd-user clientAccess that user's sessions with the specified agent
Publishable keyAnonymous pageObtain anonymous tokens; may appear in page source
visitor_secretThe same visitor's browserPreserve visitor identity; keep it private

Refreshing a token does not change identity. When accounts change, remove the old component and create one for the new identity. UI confirmation buttons do not replace server-side account authorization in your tools.

Issue a token

Production issuance calls POST /agent/v1/embed_tokens from your host backend. These standalone server checks use test user customer_123. Your actual host must derive the ID from a verified login session, never a browser assertion. The Dashboard's 试用调试 → 模拟 → 重新签发 is for identity debugging only.

Set PRAXIS_API_KEY on the server and replace the public agent ID. This command needs cURL and jq.

set -o pipefail
curl --fail-with-body --silent --show-error \
  -X POST https://api-develop.deepraxis.com/agent/v1/embed_tokens \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"agent":"agent_REPLACE_ME","external_user_id":"customer_123"}' \
  | jq '{id, expires_at, has_token: (.token | type == "string")}'

Expected: HTTP 201 with id, token, and expires_at. Use id to revoke this issuance, token as the browser credential, and expires_at as its ISO 8601 UTC expiration. Examples print only redacted check results. Every call creates a new grant; repeated issuance is not idempotent.

Omitted ttl_seconds uses the policy default. The range is 60 seconds through the smaller of the policy maximum and 3600 seconds. budget_cents cannot exceed the policy session-budget maximum. allowed_origins must be a subset of existing policy entries. Version selection uses explicit version, then the policy's pinned version, then latest. The selected version is fixed at issuance. See the API reference for all fields.

Refresh and end access

Kit calls getToken(current) with null initially and the current token on refresh. Use this callback for api.getToken in the quickstart. Your endpoint verifies that the user is still signed in before returning a fresh token.

async function getToken(current) {
  const response = await fetch("/api/praxis-token", {
    method: "POST", credentials: "same-origin", cache: "no-store",
  });
  if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
  const data = await response.json();
  if (typeof data.token !== "string" || !data.token) throw new Error("Missing token");
  return data.token;
}

The callback does not need to send the current token to prove host identity. Kit schedules calls and keeps tokens in memory. Do not hard-code a fixed token. Expected: refreshing preserves access to the same user's sessions.

On sign-out, end the host session and remove the old Kit element. Removal closes its connections but does not revoke issued tokens on the server. Revoke separately when the old credential must stop working immediately. Hosted has no equivalent refresh path; users return to the host after expiration.

Anonymous visitors

For public questions without login, open 接入与配置 → 密钥 → 可公开 key → 新建可公开 key. Configure anonymous limits and budget, and copy the complete value shown only once. Manage allowed origins under 配置 → 安全. These settings can currently only be configured in the Dashboard. A publishable key cannot replace a backend API key.

Save this standalone page as index.html. Replace PRAXIS_PUBLISHABLE_KEY with your actual publishable key and serve it from an allowed origin. It does not need a signed-in host token endpoint. Do not also configure getToken.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Public documentation assistant</title>
<script type="module" src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js" integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU" crossorigin="anonymous"></script>
<praxis-agent style="display:block;height:600px;width:100%"></praxis-agent>
<script type="module">
  await customElements.whenDefined("praxis-agent");
  document.querySelector("praxis-agent").setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      publishableKey: "PRAXIS_PUBLISHABLE_KEY",
    },
    agent: { name: "Documentation assistant" },
  });
</script>
</html>

Kit sends publishable_key to POST /agent/v1/embed_tokens/anonymous, adding an existing visitor_secret on later requests. No API key is required. Success returns HTTP 200 with token and expires_at; only new visitors also receive visitor_secret. Browsers cannot choose external_user_id, budget, instructions, or Hosted options.

Kit saves the visitor credential in local storage, falling back to page memory when storage is unavailable. Do not publish or share it. Expected: the same browser storage preserves visitor identity across refreshes; separate browser storage produces separate identities. Anonymous identity does not authorize access to account data such as orders.

Inspect and revoke

Revoke only the token whose access should end. These paths perform the same task.

Open the agent's 运营与管理 → 签发记录. Enter customer_123 in 终端用户 and click 筛选. Click 吊销 on an active record, verify the user, then click 确认吊销. Expect the record to show revoked status. Failures offer a retry.

Expected: revocation returns a record with revoked_at; repeated revocation returns the original record. Streams using that token receive session.error with EMBED_TOKEN_REVOKED, then close. Other tokens are unaffected. For user-wide revocation, blocking, and unblocking, see Manage end users.

Errors and next steps

Revalidate host login after expiration. Stop the original access after revocation. Correct configuration for disallowed origins; ask an administrator to handle blocked users. Do not repeatedly issue tokens to bypass denial. See Embed limits and errors for codes, HTTP statuses, and recovery conditions.

Pass context

Give the agent the current lesson, selected order, or user preferences so it can answer in context. Use readables for page state and host context for information verified by your backend.

Prepare the integration

Start with a working Kit page, a service agent, and an embed policy. Serve the HTML below from your website. The same-origin POST /api/praxis-token route is your backend route, implemented as described in Embed tokens and users. It checks the login session and returns { token }; it is not a public Praxis endpoint. This example uses the Develop Kit and API. Your page origin must be allowed.

Send current page state

Save this HTML as a page on your website. Changing the selection updates the registered value; Kit takes a snapshot when the user sends a message.

<!doctype html>
<html lang="en">
<meta charset="utf-8" />
<title>Lesson assistant</title>
<label>Current lesson
  <select id="lesson">
    <option value="Lesson 3: Delivery rules">Lesson 3: Delivery rules</option>
    <option value="Lesson 4: Returns">Lesson 4: Returns</option>
  </select>
</label>
<button id="clear">Stop sharing the lesson</button>
<praxis-agent style="display:block;height:600px"></praxis-agent>
<script type="module">
  import "https://kit-develop.praxisusercontent.com/v1/praxis-kit.js";
  await customElements.whenDefined("praxis-agent");
  const chat = document.querySelector("praxis-agent");
  const lesson = document.querySelector("#lesson");
  const update = () => chat.setReadable("lesson", { title: lesson.value });
  update();
  lesson.addEventListener("change", update);
  document.querySelector("#clear").addEventListener("click", () => {
    chat.removeReadable("lesson");
  });
  chat.addEventListener("praxis.log", event => console.warn(event.detail));
  chat.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const { token } = await response.json();
        if (typeof token !== "string" || !token) throw new Error("Missing token");
        return token;
      },
    },
    agent: { name: "Lesson assistant" },
  });
</script>
</html>

Expected result: ask which lesson is open. The agent receives lesson 3. Select lesson 4 and send another message: that message carries the new value. After clicking Stop sharing the lesson, later messages omit the item.

setReadable(name, value) accepts a string or JSON-serializable value. Names must match [A-Za-z0-9_.:-]{1,64}. Setting the same name replaces its value; removeReadable(name) removes it. Invalid names and non-serializable values throw without replacing the previous registration.

Each message can carry at most 32 items and 4096 UTF-8 bytes for the entire context JSON. Kit drops the earliest registered items from that snapshot until it fits. A praxis.log event has level warn, with dropped names in data.names. Registrations remain available; smaller values can be sent again.

Page context is attached to the current user message and marked untrusted. Each message gets a fresh snapshot, but removing a registration does not erase information in earlier messages. Page state cannot establish identity, account balances, or order ownership.

Send backend-verified information

Include system_message when your backend issues a token. Run this request on the server. Replace the public agent ID and derive the user ID from your authenticated login session. Never put PRAXIS_API_KEY or this command in a browser page.

curl --fail-with-body https://api-develop.deepraxis.com/agent/v1/embed_tokens \
  -X POST \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "agent": "agent_example",
    "external_user_id": "customer_123",
    "system_message": [{"type": "text", "text": "The customer prefers English replies."}]
  }'

Expected result: HTTP 201 with token, id, and expires_at. Return only the required short-lived credentials through your host route. Create a new session with that token to include the preference in model context. Keep tokens out of shared logs.

system_message is an array of { type: "text", text } blocks, limited to 8192 UTF-8 bytes of text. Use one block to avoid additional separators increasing the size. This information is visible to the model; it is not secret storage.

For an existing session, an API Key holder can append system.message through Send events. Replacing a token does not rewrite the context of an established session.

Choose another input

InformationWhere to provide it
Shared role and toneDashboard: 配置 → 指令 (Configuration → Instructions)
Replacement instructions for one tokenagent_override.system during issuance; first enable 允许宿主覆盖指令 under 配置 → 安全. null clears instructions
Page data needed only on demandClient tools
Protected or frequently changing order dataServer tools that scope access to the end user

An override without policy permission returns HTTP 400 with EMBED_FIELD_NOT_ALLOWED. Change the policy or remove the field before issuing another token. Repeating the unchanged request cannot resolve it.

Verify the context

In browser Network tools, send a message and inspect praxis_context.items. It should contain name: "lesson" and the current lesson as a string value. Change the lesson, then remove it, sending a message after each action. Check the requests rather than relying only on the model's answer.

For host context, create a new session with the newly issued token. Ask about the preferred reply language and compare it with your issuance request. This checks the experience; identity and authorization still belong to your backend and tools.

Next: Client tools · Secure your integration · Agent API.

Connect client tools

Let the agent read page state on demand or perform a page action. Declare a tool on the agent and implement a matching callback in your Kit page. Kit returns the result to the session so the agent can continue.

Prepare the integration

You need a working Kit page, a service agent's public ID (agent_…), and an API Key on your backend. This example reads a lesson selection without a video, database, or extra dependencies. Your website must provide an authenticated POST /api/praxis-token route returning { token }, as described in Embed tokens and users. This is your route, not a Praxis endpoint.

Use a client tool for on-demand reads. Use page context for small facts needed with every message.

Declare the tool

Save this as register-tool.ts, replace agent_example, and run bun register-tool.ts on a server with PRAXIS_API_KEY set. It reads the agent, then updates tools through the public API. Other tools are preserved, and the current version guards against concurrent changes.

// register-tool.ts — run on the server with: bun register-tool.ts
const key = process.env.PRAXIS_API_KEY;
if (!key) throw new Error("Set PRAXIS_API_KEY on the server");
const url = "https://api-develop.deepraxis.com/agent/v1/agents/agent_example";
const headers = { Authorization: `Bearer ${key}`, "Content-Type": "application/json" };
const currentResponse = await fetch(url, { headers });
if (!currentResponse.ok) throw new Error(`Read failed: ${currentResponse.status}`);
const current = await currentResponse.json();
const tool = {
  type: "custom",
  name: "read_selected_lesson",
  description: "Read the selected lesson when the user asks which lesson is open.",
  input_schema: { type: "object", properties: {}, additionalProperties: false },
};
const tools = [...(current.tools ?? []).filter(
  (item: { type: string; name?: string }) =>
    !(item.type === "custom" && item.name === tool.name),
), tool];
const response = await fetch(url, {
  method: "POST", headers,
  body: JSON.stringify({ version: current.version, tools }),
});
if (!response.ok) throw new Error(`Update failed: ${response.status}`);
const saved = await response.json();
console.log({ id: saved.id, version: saved.version });

Expected result: the terminal prints the agent ID and new version. Issue a token for that version and start a new session. Tokens pinned to an older version and existing sessions do not automatically adopt the new definition. If the embed policy pins a version, update it in the Dashboard or specify the new version during issuance.

Tool names must match ^[a-zA-Z0-9_-]{1,64}$ and must not duplicate custom or built-in tool names. input_schema.type must be object. Describe when the agent should call the tool. A browser function alone does not register a tool with the agent.

Handle the call in your page

Serve this HTML from your website. It uses the Develop Kit and API; the embed policy must allow the page origin.

<!doctype html>
<html lang="en">
<meta charset="utf-8" />
<title>Lesson assistant</title>
<label>Current lesson
  <select id="lesson">
    <option value="Lesson 3: Delivery rules">Lesson 3: Delivery rules</option>
    <option value="Lesson 4: Returns">Lesson 4: Returns</option>
  </select>
</label>
<output id="tool-status" aria-live="polite"></output>
<praxis-agent style="display:block;height:600px"></praxis-agent>
<script type="module">
  import "https://kit-develop.praxisusercontent.com/v1/praxis-kit.js";
  await customElements.whenDefined("praxis-agent");
  const chat = document.querySelector("praxis-agent");
  const lesson = document.querySelector("#lesson");
  chat.onClientTool = async ({ name }) => {
    if (name !== "read_selected_lesson") throw new Error(`Unknown tool: ${name}`);
    return { title: lesson.value };
  };
  chat.addEventListener("praxis.tool.change", event => {
    const { name, status, isError } = event.detail;
    document.querySelector("#tool-status").textContent =
      `${name}: ${status}${isError ? " (error)" : ""}`;
  });
  chat.addEventListener("praxis.log", event => console.warn(event.detail));
  chat.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const { token } = await response.json();
        if (typeof token !== "string" || !token) throw new Error("Missing token");
        return token;
      },
    },
    agent: { name: "Lesson assistant" },
  });
</script>
</html>

Assign onClientTool as a JavaScript property on the element. It is neither an HTML attribute nor a setOptions option. The callback receives { id, name, input }: call ID, tool name, and input. Validate the input in your handler and throw for unknown tool names.

Strings are returned unchanged. Other serializable values become JSON text; undefined becomes an empty string. Exceptions, rejected promises, and serialization failures become error results. Do not include credentials or another user's data.

Verify calls and recovery

Ask the agent to use the tool to identify the open lesson. Expected result: the page reports start, then end, and the agent answers using the returned lesson. Change the selection and ask again. An end event means the server accepted the result, not that the entire response finished.

SymptomAction
No callCheck the new session's agent version and the tool description
not available on this pageAssign onClientTool before a call arrives
Handler failureFix input validation or page logic; the agent receives an error result
Result submission failure in praxis.logThe handler is not automatically rerun. Check connectivity and session state before repeating side effects

The server waits at most 120 seconds, then records an error result and continues. Kit claims call IDs and checks historical results to avoid duplicate execution. This is not a transaction guarantee across browser tabs. Backend authorization and idempotency are required for tools that change business data.

Authorization and next steps

Client custom tools bypass the platform's tool confirmation cards. Implement any page-action confirmation in your own UI. Enforce data permissions on the backend, without trusting browser values or model-supplied identity.

Tool authorization · Component reference · Update agent API.

Configure tool authorization

Choose whether enabled tools run immediately or ask the user to confirm. This guide uses an orders MCP server with query and return tools. Your tool server must still enforce business permissions.

Prepare the tools

Start with a working service agent. Connect an MCP server named orders under 配置 → MCP 与凭据 (Configuration → MCP and credentials). The example requires enabled list_orders and request_return tools. Replace these names with your actual server and tools; do not configure nonexistent example tools.

A confirmation card is not authentication. A public MCP service must authenticate the platform caller before applying end-user data restrictions. See Secure tool access.

Choose an execution policy

PolicyEffect on ordinary enabled tools
always_allowExecutes without asking
always_askPauses for confirmation; allow executes the call, deny prevents it

These are the two supported policies. Do not use auto: even where a type accepts it, the runtime rejects execution. Disable a tool to prevent its use entirely. Asking still lets the user approve a call.

Policies do not bypass mandatory tool interactions, security boundaries, or business checks. Client custom tools execute in your page and do not use these confirmation policies.

Configure tools and defaults

In the current Dashboard, open 服务智能体 → your agent → 接入与配置 → 配置 → 工具与授权 (Service agents → your agent → Integration and configuration → Configuration → Tools and authorization).

  1. Under 授权方式, set orders · 默认授权 to 每次询问 (Ask every time).
  2. Set list_orders to 自动执行 (Run automatically) and request_return to 每次询问. Individual selectors only appear for configured tools. Other tools inherit the default; use the API to add individual settings if needed.
  3. Save. Expected: the configuration version increases. Existing sessions retain their original version.

Resolution follows individual configs[].permission_policy, then default_config.permission_policy, then the tool default. Without an explicit MCP policy, only annotations.readOnlyHint === true defaults to allow. Otherwise, it defaults to ask. This server hint affects only the default; explicit settings take priority.

The built-in toolset defaults to always_allow when no default policy is specified, while mandatory tool interactions still apply. Explicitly require confirmation for refunds, deletion, and external messages instead of relying on server hints.

Verify confirmation

Issue an embed token for the new version and create a new session. If your policy pins a version, update that pin or specify the new version during issuance. Saving an agent does not upgrade existing sessions.

Request an order lookup, then a test return. Expected: the lookup runs immediately; the return shows a Kit confirmation card. Denying must prevent the server-side action. Request it again and allow it to proceed. Verify actual execution in your tool server's records rather than relying only on the model's answer.

Kit supplies the confirmation UI. An embedded user's approval applies to that call and does not save permanent tool permissions. For complete scenarios, see Test your integration.

Next steps

Protect tool access · Client tools · Update agent API.

Customize appearance

Match chat colors, starter text, and the input area to your website. iframe and Hosted share appearance saved in the Dashboard. A Kit component uses options supplied by your page.

Prepare the integration

Start with a working service agent and embed policy. The Kit example requires your website to provide POST /api/praxis-token, check the login session, and return { token }. This is your same-origin route; see Embed tokens and users. The example uses Develop addresses, and your page origin must be allowed.

IntegrationWhere to configure appearance
iframe / HostedSave in the Dashboard; both share it, with changes applied on page refresh
Kit componentPage-side setOptions; the Dashboard Kit editor previews and generates code without saving

Save hosted appearance

Persistent iframe / Hosted appearance can currently only be configured in the Dashboard. Open 服务智能体 → your agent → 接入与配置 → 接入方式与外观 → iframe (Service agents → your agent → Integration and configuration → Integration methods and appearance → iframe).

  1. Under 主题 (Theme), select the color scheme, accent, grayscale, radius, and density.
  2. Under 开场 (Start screen), enter the greeting and prompts. Use 输入区 (Input area) for the placeholder and footer.
  3. Click 保存 (Save). Expected: a message says the changes apply after the embed page refreshes. Refresh the iframe or reopen Hosted to check the same text and colors.

The 配置 → 开场与外观 entry is currently an information page. The public appearance API supports reading only; it has no public save operation.

Configure Kit appearance

Serve this HTML from your website. It sets the theme, start screen, and composer, with a button to verify updates during a conversation.

<!doctype html>
<html lang="en">
<meta charset="utf-8" />
<title>Order assistant</title>
<button id="dark">Switch to dark</button>
<praxis-agent style="display:block;height:600px"></praxis-agent>
<script type="module">
  import "https://kit-develop.praxisusercontent.com/v1/praxis-kit.js";
  await customElements.whenDefined("praxis-agent");
  const chat = document.querySelector("praxis-agent");
  chat.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", {
          method: "POST", credentials: "same-origin",
        });
        if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
        const { token } = await response.json();
        if (typeof token !== "string" || !token) throw new Error("Missing token");
        return token;
      },
    },
    agent: { name: "Order assistant" },
    theme: {
      colorScheme: "light", accent: "#2563eb", grayscale: "cool",
      radius: "lg", density: "normal", fontFamily: "system-ui, sans-serif",
      fontSize: 16,
    },
    header: { title: "Order support", subtitle: "Delivery and returns" },
    startScreen: {
      greeting: "What would you like to know?",
      prompts: [{ label: "Delivery rules", prompt: "Explain the delivery rules." }],
    },
    composer: { placeholder: "Ask about an order", attachments: { enabled: false } },
    history: { enabled: true },
    footer: "Confirm order details with the merchant.",
  });
  document.querySelector("#dark").addEventListener("click", () => {
    chat.setOptions({ theme: { colorScheme: "dark" } });
  });
</script>
</html>

Expected result: a blue accent, greeting, and Delivery rules button appear. The composer has no attachment button. Click Switch to dark to change the colors while keeping the current session and messages.

Subsequent setOptions calls merge nested groups and replace arrays. Updating theme.colorScheme does not require resending the token callback. To change starter prompts, supply the complete startScreen.prompts array.

Adjust colors and text

OptionValues or purpose
theme.colorSchemelight, dark, system; omitted means light, while system follows the system scheme
theme.accent / theme.surfaceAccent / background; use explicit opaque CSS colors such as #2563eb, not var(...) or transparent colors
theme.grayscaleneutral, warm, cool
theme.radiusnone, sm, md, lg, full
theme.densitycompact, normal, spacious
theme.fontFamily / theme.fontSizeFont stack / positive pixel size; load fonts in your own page
startScreen.greeting / startScreen.promptsGreeting / at most 4 visible prompts; label is button text, clicking sends prompt
composer.placeholderInput placeholder
composer.attachments.enabledKit attachment button, enabled by default; unavailable in saved iframe / Hosted appearance
localezh or en; if omitted, Kit uses the host page language, then the browser language

Saved appearance JSON is limited to 8192 UTF-8 bytes. Greetings allow 200 characters; at most 4 prompts each allow 40 characters for label and 200 for prompt. Input placeholders allow 80 characters. Avatar and header images require HTTPS URLs. See the component reference for component-specific boundaries.

Verify and continue

Check starter text in a new session. Click a prompt and confirm the sent text matches prompt. Switch an ongoing conversation to dark and check that messages remain. Open another page to verify saved Dashboard appearance instead of mistaking local Kit options for saved settings.

Appearance changes do not change agent instructions or tool authorization. If saving fails, follow the field error: shorten text, reduce prompt count, or correct image URLs before retrying.

Component reference · Secure your integration · Agent API.

Secure your embed integration

Use your host login session to establish identity, an origin allowlist to restrict browser access, and server-side checks to protect tools. This page configures those boundaries. See Test your integration for scenarios and the go-live checklist for release verification.

Preparation and responsibilities

You need Dashboard access to manage the service agent, a host backend, and permission to change website response headers. Configure the embed policy and token issuance before reviewing these responsibilities.

Platform boundaryHost responsibility
Embedded users access only their own sessions under the bound agentDerive external_user_id from a verified login session, not a browser-supplied identity
Cross-user and cross-agent resource access returns 404Keep API Keys server-side; authenticate issuance requests and protect against cross-site request forgery
Browser origin checks and iframe restrictionsRegister actual website origins and restrict scripts loaded by the host page

Protect identity and credentials

Your backend uses PRAXIS_API_KEY to issue a short-lived embed token. Send only the required token to the browser. Revalidate the host login session for issuance and renewal. Never let a request body's external_user_id establish identity. Keep API Keys and embed tokens out of URLs, HTML, and logs.

Kit's transport holds its token in memory. Anonymous access also uses a visitor_secret, which Kit attempts to save in localStorage to retain visitor identity. It falls back to page memory when storage is unavailable. A publishableKey is public; a visitor_secret is private. Anonymous identity is not a logged-in account on your website.

On logout, stop issuance, remove the chat component, and revoke the relevant credentials from your backend. Clearing the page is not revocation. Blocking an end user prevents new issuance and revokes that user's existing tokens. Kit can obtain a token before expiry; Hosted does not automatically renew. See Embed tokens and users.

The model may repeat instructions, system_message, page context, or tool results. Include only information the current user may access. Never pass secrets or another user's data as context.

Restrict page origins

The origin allowlist can currently only be configured in the Dashboard. Open 服务智能体 → your agent → 接入与配置 → 配置 → 安全 (Service agents → your agent → Integration and configuration → Configuration → Security). Click 添加来源 (Add origin), enter an origin such as https://shop.example.com, then 保存安全设置 (Save security settings). Expected: the saved message confirms no new agent version was created.

allowed_origins permits at most 50 entries. Enter origins without paths, queries, or fragments. Regular origins require HTTPS. https://*.example.com matches one subdomain level, not the root or deeper subdomains, and does not support ports. 允许 localhost is for local HTTP development and defaults to off.

During issuance, use allowed_origins to select a nonempty subset of policy entries, matching them exactly. Do not use an empty array to mean “deny all”: token validation falls back to the policy allowlist. Changing a policy does not revoke issued tokens. Revoke credentials when access must be withdrawn immediately.

A disallowed origin returns HTTP 403 with EMBED_ORIGIN_NOT_ALLOWED. Correct the configuration before reconnecting. Origin checks constrain browsers; non-browser requests without Origin bypass this check. It does not replace token authentication.

Protect tool access

Your MCP server must authenticate the platform caller before treating X-Praxis-External-User-Id as the end-user identity. The platform also sends X-Praxis-Agent-Id and X-Praxis-Session-Id. Sessions without an end user do not carry these identity headers.

Reject requests missing required identity. Scope every query to that user, and verify ownership and business state before changing an order. Do not trust a model-supplied user ID or a public request merely because it contains identity headers. Authenticate the caller using MCP-configured headers before trusting platform-supplied identity.

Confirmation cards check intent; clicking allow does not grant data access. Client tools and page context come from the browser and cannot enforce authorization.

Protect the host page

This Develop response-header example assumes initialization scripts live in same-origin JavaScript files and Kit loads from its CDN. Narrow image and other sources to your site's needs. If initialization remains inline, add a CSP nonce or hash for that script.

Content-Security-Policy: default-src 'self'; script-src 'self' https://kit-develop.praxisusercontent.com; style-src 'self' 'unsafe-inline' https://kit-develop.praxisusercontent.com; connect-src 'self' https://api-develop.deepraxis.com https://kit-develop.praxisusercontent.com; frame-src https://chat-develop.praxisusercontent.com; font-src 'self' https://kit-develop.praxisusercontent.com data:; img-src 'self' https: data: blob:; frame-ancestors 'none'; base-uri 'self';

Kit themes use inline styles, which this policy allows. Connection sources must match api.url. frame-src permits loading the hosted iframe; frame-ancestors 'none' prevents other sites from embedding your host page.

Platform iframe ancestors come from the allowlist; an empty list prevents framing. Hosted cannot be framed. Kit's open Shadow DOM isolates styles, but scripts on the same page can read it. Avoid sharing the chat page with untrusted scripts.

For CDN integrity checks, copy the actual fixed-version URL and integrity generated by the Dashboard, retaining crossorigin="anonymous". Do not attach a fixed hash to the floating /v1/ URL or use an illustrative hash as a release value.

Verify and continue

Reload from an allowed website and send a message. Inspect the host response's CSP in browser Network tools. Console should show no CSP failures loading Kit or connecting to the API. Confirm the issuance route returns only short-lived credentials and that tool access is scoped to the logged-in user.

Test your integration · Go-live checklist · Embed limits and errors.

Manage end users

Find end users by their host application ID and stop or restore embedded access when needed. Each action applies to that user within the current organization and agent.

Prepare and choose an action

You need a service agent with an embed policy, organization management access, and the external_user_id used at issuance. Verify actions with an isolated test user first. Revocation and blocking interrupt that user's ongoing conversations.

GoalActionSubsequent access
End access through existing credentialsRevoke tokensRevokes all existing tokens for this user; the host can issue new ones
Continue refusing accessBlockRevokes existing tokens and rejects new issuance
Allow issuance againUnblockAllows new issuance; previously revoked tokens remain revoked

To revoke just one token, use token management instead of user-wide revocation.

Find and manage a user

  1. Open Service agents → Select agent → Operations → End users (终端用户). Labels describe the current Chinese Dashboard.
  2. Enter an ID in Search by end-user ID prefix (按终端用户 ID 前缀搜索). The table shows first seen, last active, session count, cost and status.
  3. Click the user ID to open details. Check Recent sessions (最近会话) to confirm the intended user.
  4. Click Revoke tokens (吊销令牌), Block (封禁) or Unblock (解封) in the row or details. Verify the ID in the dialog and confirm the action.

Expected: Revocation reports “已吊销 N 张” (N tokens revoked). Blocking changes the status to “已封禁”; unblocking changes it to “正常”. These actions do not delete the user's session or cost records.

Verify the access boundary

After revocation, access with an old token must fail. The host can issue a new token after checking the user's login again. After blocking, issuance must return EMBED_USER_BLOCKED (HTTP 403). After unblocking, issue a new token and send a message. A revoked token must not become valid again.

Check that another user's access to this agent remains unaffected. Derive identity from the host's authenticated server session, not an arbitrary ID submitted by the browser.

Handle failures

If a user cannot be found, verify the organization, public agent ID and complete ID used at issuance. Dashboard Playground data is excluded from customer user listings. Similar ID prefixes do not establish identity.

If blocking fails, read the state again before retrying. The blocked state may already be saved while token revocation remains incomplete. Do not report the operation as complete in that case. Record the HTTP status and request ID, then use Limits and errors.

Next steps

Inspect sessions and usage · Test your embed · Agent API · Cookbook

Inspect sessions and usage

Use sessions, users and spending to assess your embed, then inspect an individual conversation. This page is for service agents already receiving traffic from a host application.

Prepare the records

With an authorized organization account, open Service agents → Select agent → Operations in the Dashboard. Labels below describe the current Chinese Dashboard. Have the time, end-user ID and integration type ready.

Send a recognizable test message from your host website and receive a reply before checking operations data. Dashboard Playground records are excluded from customer operations statistics. They cannot prove customer traffic or spending was recorded.

Inspect the overview

Open Overview (概览) and choose 24 hours, 7 days or 30 days. Aggregate overview statistics are currently Dashboard-only; there is no corresponding public API.

InformationQuestion it answers
Sessions, active end users, cost, cost per sessionHas traffic or the cost of serving a conversation changed?
Positive feedback rateWhat proportion of rated responses received positive feedback?
P50 first-token latency, error rateHas the initial response slowed, or have runs failed?
Source and version distributionDoes traffic come from Kit, iframe, Hosted, API or automation? Is the new version in use?
Attention counters and top 5 toolsAre origins blocked, budgets reached or tool calls unusual?

Expected: Session trends and version distribution cover the selected period. A metric without samples displays “—”, not zero. The 24-hour trend uses hourly buckets; other periods use daily buckets. Run errors, origin blocks and tool counts aggregate the intersecting UTC calendar days, including whole boundary days.

Find and replay sessions

  1. Open Sessions (会话), enter the full end-user ID, and click Filter (筛选). Choose a Source (来源) if needed.
  2. Open the session and check its user, source, version and creation time in the replay header.
  3. Inspect messages and tool calls, locate your test message and reply, then use Close replay (关闭回放).

Expected: Replay is read-only; you cannot send messages. To prevent further messages, click Archive (归档) and confirm. Records remain readable after archiving, but the session cannot receive more messages.

Inspect spending and budgets

Open Usage and budgets (用量与预算) to see monthly spending, the monthly cap and its action. Choose 7, 30 or 90 days for daily spending. Below are budget and rate settings, monthly source breakdown and the top 20 end users by spending. Monthly totals use the UTC calendar month; displayed amounts are US dollars.

These aggregate reports and budget settings are currently Dashboard-only. Change cap (修改上限) opens Configuration → Security. Save and return to verify. Stop new sessions rejects session creation when the cap is reached; Alert only does not block it. Cost composition (花费构成) is not available yet; its placeholder is not a breakdown.

Verify and act

Match the host message, replay and source using the same user, session and period. For an unexpected version, check the version used at token issuance. Pinning or rolling back does not alter existing tokens. For missing data, check the environment, organization and period before creating more test sessions.

For origin or budget failures, record the response's X-Request-Id, time, HTTP status and error code. Exclude API Keys and tokens. See Manage end users for user-level actions.

Next: Embed limits and errors · Agent API · Cookbook

Test your embed

Before going live, verify conversations and refusal paths in your actual host website. These are acceptance scenarios to execute. A visible component or praxis.ready proves mounting, not successful authentication or a model reply.

Prepare the test environment

Use an isolated service agent, available model and environment, saved embed policy, and an integrated host page. Allow that page's actual origin and set an affordable budget. Prepare host test accounts A and B with distinct external_user_id values derived from authenticated server sessions.

First select a version and send a question in Service agents → Select agent → Integration and configuration → Playground (试用调试). This checks configuration. Then test the host below. Dashboard simulation does not test host login, domains or token endpoints, and does not enter customer operations statistics.

Run acceptance scenarios

Input or actionExpected result and verification
A logs in and asks about a courseAn actual reply appears; Operations → Sessions contains the same message, user and source
Call your host token endpoint while logged outThe host refuses issuance and returns no embed token
Load from an origin outside the allowlistAccess fails; an API response can report EMBED_ORIGIN_NOT_ALLOWED (403). Browser framing policy can block an iframe first
Issue with ttl_seconds: 59INVALID_PARAMS (400), with no usable token
Submit agent_override while host overrides are disabledEMBED_FIELD_NOT_ALLOWED (400); browser input cannot change trusted instructions
Issue for a blocked userEMBED_USER_BLOCKED (403)
Create a session at the active-session limitEMBED_RATE_LIMITED (429); wait for runs to finish rather than deleting idle history
Create at the monthly cap with “Stop new sessions”EMBED_MONTHLY_CAP_REACHED (402); do not manufacture this condition on an agent serving real users
Invoke a declared client toolThe intended page effect and result occur; unknown tools, invalid arguments or missing business permissions fail. A confirmation card alone is insufficient

Use this standalone script to check the TTL minimum. On your server, set PRAXIS_API_BASE (API origin), PRAXIS_API_KEY (organization API Key), and PRAXIS_AGENT (real agent_… ID). Save it as test-token-limit.mjs and run node test-token-limit.mjs with Node.js supporting native fetch. Configure the policy first; otherwise missing configuration fails before TTL validation.

import assert from 'node:assert/strict';

const { PRAXIS_API_BASE, PRAXIS_API_KEY, PRAXIS_AGENT } = process.env;
assert(PRAXIS_API_BASE && PRAXIS_API_KEY && PRAXIS_AGENT, 'Set the three environment variables');
assert(PRAXIS_AGENT.startsWith('agent_'), 'Use the public agent ID');
const response = await fetch(`${PRAXIS_API_BASE.replace(/\/$/, '')}/agent/v1/embed_tokens`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${PRAXIS_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ agent: PRAXIS_AGENT, external_user_id: 'docs_test_a', ttl_seconds: 59 }),
});
assert.equal(response.status, 400, 'Invalid TTL must be rejected');
console.log({ status: response.status, requestId: response.headers.get('X-Request-Id') });

Expected: Exit successfully with status: 400. This checks refusal status only; separately inspect the error body for INVALID_PARAMS and the TTL field message. The script prints no credentials and does not replace successful issuance and reply tests.

Verify identity and lifecycle

  1. After A starts a session, log out and destroy A's component. Log in as B with a new component. A's sessions must be absent from history, and reading A's session with B's token must fail. Clearing the interface alone does not switch identity.
  2. Keep A logged in through token refresh. Confirm getToken(current) rechecks host identity and subsequent messages still belong to A. Separately, an expired-token request must return EMBED_TOKEN_EXPIRED (401).
  3. Revoke A's current token under Operations → Issuance records (签发记录). Old access must fail without automatic refresh bypassing revocation. Test user-wide revocation, blocking and unblocking with Manage end users.
  4. For anonymous access, use separate browser storage for two visitors. Reload one visitor's page to check continuity. Never share visitor_secret between visitors.
  5. For Hosted, test exchange after 60 seconds, repeated exchange, and token expiry in an open page. The first two fail. The open page does not refresh its token automatically; obtain a new link from the host.

Diagnose and clean up

Record time, environment, component version, expected and actual results, plus failed responses' X-Request-Id, status and error code. An error event may contain only message; inspect browser network responses for details. Exclude Keys, tokens and visitor credentials.

Revoke test tokens, archive unused test sessions, and restore temporary limits and blocked states. Archiving retains records. Run one successful conversation with normal configuration afterward to confirm cleanup left no access problems.

Next: Go-live checklist · Limits and errors · Agent API

Go-live checklist

Use this checklist before first launch or after changing domains, identity integration or component versions. Complete Test your embed, save verifiable results for each item, then open access.

Confirm launch requirements

  • Configuration and versions match. Dashboard Integration and configuration → Versions shows the intended embed version. A newly issued token creates a session with that version. Check that an explicit issuance version does not override your selection.
  • Identity comes from host authentication. Logged-out requests cannot obtain tokens from your host. Test users A and B cannot see each other's sessions. The API Key stays on the server; browser source and responses contain no real PRAXIS_API_KEY value.
  • Anonymous credentials are handled correctly. If enabled, expose only the publishable key and configure its limits. Visitors with separate browser storage remain isolated. Do not share visitor_secret.
  • Target origins work. Complete a conversation on the actual HTTPS domain; an unlisted origin fails. The iframe renders in its intended page. Hosted opens independently and cannot be framed. Disable localhost access when no longer needed.
  • Component delivery is reproducible. Record and verify the Kit version, script URL and applicable integrity value. Load from an empty cache and receive a real reply, not just praxis.ready.
  • Refusal paths recover correctly. Test expiry, revocation, blocking and origin refusal. Destroy the old component on host logout. After Hosted expiry, obtain a new link rather than looping over an invalid one.
  • Tools enforce permissions. Test valid input, invalid input and users without business permissions. Confirmation on every call does not replace server-side authorization.
  • Budgets and monitoring work. Verify US-dollar budgets, lifetime, request limits and active-session limits. Confirm the monthly cap action. Find the host test session, source and spending under Operations.

Prepare rollback and support

Keep a verified host deployment and component version. For configuration issues, select an older version and click Roll back to this version (回滚到此版本). Verify after issuing a new token; existing tokens do not change. To stop a specific user's access immediately, block or revoke all tokens and confirm old-token access fails.

For support, retain the time, environment, public agent ID, session ID, HTTP status, error code and X-Request-Id. Exclude tokens and API Keys. Follow the recovery action in Limits and errors, then verify a conversation from the host.

Related: Configure your agent · Component reference · Agent API

Component reference

<praxis-agent> renders an agent conversation on your website. Initialize it with element.setOptions(options). This reference covers properties, methods and events. For integration steps, see Embed in a web page.

Initialize

Initial options require api.url, agent.name, and either api.getToken or api.publishableKey. Missing values throw TypeError. Choose getToken for authenticated users or publishableKey for anonymous visitors. Supply one; when both are supplied, the publishable key takes precedence and getToken is not called.

This anonymous example targets Develop. Create a publishable key there, save an embed policy, and allow your host origin. Replace PRAXIS_PUBLISHABLE_KEY and serve this HTML from that origin. A publishable key is not a server API Key. The script URL, version and integrity value must belong to the same release.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Course assistant</title>
<script type="module"
  src="https://kit-develop.praxisusercontent.com/v0.2.8/praxis-kit.js"
  integrity="sha384-vey+VZZcFIWJq9Fmv/nmLxspKtXRiL8XiDOfg2g2QFvwZi5MA+tkiQvPBtoMS8eU"
  crossorigin="anonymous"></script>
<praxis-agent style="display:block;height:600px;width:100%"></praxis-agent>
<p id="status" role="status"></p>
<script type="module">
  await customElements.whenDefined('praxis-agent');
  const agent = document.querySelector('praxis-agent');
  const status = document.querySelector('#status');
  agent.addEventListener('praxis.ready', () => { status.textContent = 'Interface mounted'; });
  agent.addEventListener('praxis.error', () => { status.textContent = 'Check the failed network request'; });
  agent.setOptions({
    api: {
      url: 'https://api-develop.deepraxis.com',
      publishableKey: 'PRAXIS_PUBLISHABLE_KEY',
    },
    agent: { name: 'Course assistant' },
    locale: 'en',
  });
</script>
</html>

Expected: A chat interface appears and sending a question produces a real reply. Interface mounted only confirms mounting; also verify replies and user isolation.

Properties

api and agent

PropertyTypeMeaning
api.urlstringRequired API origin or base ending in /agent/v1; immutable after initialization
api.getToken(current: string | null) => Promise<string>Returns an embed token. Initially current is null; refresh receives the current token. Recheck host login on your backend every time
api.publishableKeystringAnonymous token entry point instead of getToken; immutable after initialization. A privately stored visitor_secret preserves visitor identity
agent.namestringRequired display name; does not select an agent
agent.avatarstringAvatar URL; credentials determine the actual agent accessed
onClientTool(call: { id, name, input }) => unknown | Promise<unknown>Element property, outside setOptions. Handles client tools; throw for unknown tools

Layout and content

PropertyType / defaultEffect
layoutinline, sidebar, popup, modal; default inlineConversation layout
widthnumber; 400 CSS pixelsSidebar width
launcher.typenone, bubble, mascotRequired when supplying launcher; defaults to none for inline, bubble otherwise
launcher.image, launcher.labelstringLauncher image and text
launcher.positionbottom-right, bottom-leftLauncher position; defaults to bottom-right
header.title, header.subtitlestringTitle and subtitle; title defaults to agent.name
header.iconstringSupported Lucide icon name or image URL
header.actions{ id: string; icon: string; label: string }[]Custom header buttons emitting click events
history.enabledboolean; trueHistory entry; false also disables showHistory()
startScreen.greetingstringEmpty-conversation greeting
startScreen.prompts{ label: string; prompt: string }[]Displays the first 4; clicking sends prompt
composer.placeholderstringDefaults to the localized input hint
composer.attachments.enabledboolean; trueAllows file selection, pasting and dropping
disclaimer, footerstringConversation notice and footer text
autoScrollbooleanWhether new content scrolls automatically
localezh, enFalls back to host page language, then browser language. Tags starting with zh select Chinese; otherwise English. No language information defaults to English

theme

PropertyValues / constraints
colorSchemelight, dark, system; light when omitted, system follows the browser
accent, surfaceExplicit opaque CSS colors; var(), currentcolor and transparency throw TypeError
grayscaleneutral, warm, cool
radiusnone, sm, md, lg, full; base radii of 0, 4, 10, 18, 9999 CSS pixels
densitycompact, normal, spacious; base spacing of 3, 4, 5 CSS pixels
fontFamilyNonempty font-family string without ; { } < >; otherwise TypeError
fontSizePositive finite number in CSS pixels; otherwise RangeError

Default accent colors are #167A43 in light mode and #30a46c in dark mode. Foreground colors are selected for contrast.

React props

React's PraxisAgent uses these props. The current React binding still requires a top-level getToken, even though options.api includes publishableKey in its type. For anonymous access, use the Web Component above directly.

Prop / return objectMeaning
getTokenRequired; same signature as api.getToken
optionsOptions above without api.getToken; omission renders an unconfigured element
layout, launcherOverride options.layout and options.launcher.type; launcher accepts none/bubble/mascot
className, style, childrenHost element styling and React children
onClientToolClient tool callback, matching the element property
ref / usePraxisAgent()ref exposes a control handle; the hook returns stable { ref, control }

Methods

MethodReturn / behavior
setOptions(partial)Initializes or merges options
sendUserMessage(text)Promise<"accepted" | "rejected" | "unknown">; can reject empty or oversized text, pending approvals or unavailable submission. accepted does not prove a successful reply; inspect the session before resending an unknown result
setComposerValue(text)Replaces the draft without sending
focusComposer()Focuses the composer; no effect while history is open
setThreadId(id)Switches sessions and closes history; null opens an empty conversation, not a different user identity
showHistory(), hideHistory()Opens or closes history
open(), close(), toggle()Opens, closes or toggles the panel; no effect for inline
setReadable(name, value)Registers page context. Name: 1–64 characters, letters, digits or _ . : -. Value: string or JSON-serializable value
removeReadable(name)Removes the named page value

The React control handle includes the methods from sendUserMessage through toggle above. Calls before onReady or after unmount throw a not-ready error rather than queueing. For React page context and tools, use usePraxisReadable and usePraxisTool, with PraxisProvider when sharing a registry scope. See the guides below.

Events

Events are bubbling, composed CustomEvent objects that cross Shadow DOM. React callbacks receive the same event object; read its event.detail.

Event / React callbackDetail and timing
praxis.ready / onReadyNone; interface mounted, not proof of authentication or network success
praxis.error / onErrorKitError with code, status, retryable, message, or only { message }; check fields before using them
praxis.thread.change / onThreadChange{ threadId: string | null }; current session changed
praxis.response.start / onResponseStartNone; processing became busy
praxis.response.end / onResponseEndNone; processing stopped being busy, not a success assertion
praxis.open, praxis.close / onOpen, onCloseNone; panel opened or closed
praxis.history.open, praxis.history.close / onHistoryOpen, onHistoryCloseNone; history opened or closed
praxis.header.action / onHeaderAction{ id: string }; custom header button clicked
praxis.tool.change / onToolChange{ id, name, status: "start" | "end", isError? }; client tool execution changed
praxis.log / onLog{ level: "info" | "warn" | "error", message, data? }; context trimming or tool diagnostics
praxis.frame.closeNone; requests that the host close an iframe; no corresponding React callback prop

Defaults and lifecycle

setOptions merges objects recursively and replaces arrays. Explicit undefined resets a field. Changing api.url or api.publishableKey after initialization throws TypeError. Destroy and recreate the element when changing backends or user identity. Removing an element releases connections and refresh timers, but does not revoke issued tokens.

Page context snapshots allow 32 items and 4096 UTF-8 bytes. Excess entries are omitted starting with the earliest registration, with a log event. These runtime options are separate from Dashboard-persisted iframe/Hosted appearance, which has its own fields and size limits.

Usage guides

Embed tokens and users · Pass context · Add client tools · Customize appearance

Embed limits and errors

This page covers embed tokens, Kit, iframe and Hosted. The source column distinguishes fixed validation, Dashboard policy and component behavior. Policy values come from the configuration saved for your agent.

Limits

ItemValue and unitSource
external_user_id1–128 characters; ASCII letters, digits or _ . : @ -Issuance validation
ttl_seconds60–3600 seconds, capped by policy ttl_max_seconds; omission uses policy defaultIssuance validation and policy
system_messageText across all text blocks ≤8192 UTF-8 bytesIssuance validation
metadata≤16 keys; keys 1–64 characters, string values ≤512 charactersIssuance validation
budget_cents0 through policy session maximum, in US cents; omission uses policy defaultIssuance validation and policy
Issuance allowed_originsSubset of policy entries, matched exactlyIssuance validation
Policy origins≤50 HTTPS origins; wildcards match one subdomain level without a portEmbed policy
allow_localhostfalse by default; allows HTTP localhost / 127.0.0.1, with optional port, when enabledEmbed policy
max_active_sessions_per_user1–100 active sessions per user; idle history is excludedEmbed policy
per_user_rate_per_min1–600 requests/minute/userEmbed policy
blocked_end_users≤1000 user IDsEmbed policy
monthly_cap_centsNonnegative integer US cents; null means uncapped; UTC calendar monthPolicy and monthly accounting
Page context≤32 items; complete JSON ≤4096 UTF-8 bytesAPI validation; Kit trims oldest entries
Session title / initial eventsTitle ≤500 characters; ≤50 initial plain-text user messagesEmbedded session validation
Attachment picker≤5 files/message; each ≤10 MiB (10,485,760 bytes); PNG, JPEG, GIF, WebP, PDF, TXT, MDKit input validation; the first message of a new session still accepts only plain text
startScreen.promptsFirst 4 displayedKit component
Hosted exchange credential60 seconds, usable onceHosted exchange validation

The monthly action stop_new_sessions rejects new sessions; alert_only does not block them. Dashboard monetary inputs use US dollars; API budget fields use cents. An exhausted session budget stops new work while retaining tool confirmation and interruption.

Server errors

These statuses apply to ordinary HTTP failures. Once a stream opens, errors arrive inside the stream and cannot change its HTTP status. Retryability refers to the original request, before changing configuration or credentials.

EMBED_TOKEN_EXPIRED

401 · Access · No direct retry. The token expired. Refreshable Kit access obtains another token and replays an expiry-failed request at most once. Hosted requires a new host-issued link.

EMBED_TOKEN_REVOKED

401 · Access/stream · Not retryable. Kit stops refreshing. End old access and have the host recheck eligibility. Do not automatically issue tokens to bypass revocation.

EMBED_ORIGIN_NOT_ALLOWED

403 · Origin check · Not retryable. Check scheme, host and port against the policy and any narrower issuance allowlist. After correction, issue again and verify.

EMBED_USER_BLOCKED

403 · Issuance/session creation · Not retryable. An administrator must review the user under Operations → End users. If access should resume, unblock and issue a new token.

EMBED_RATE_LIMITED

429 · Requests/session creation/anonymous issuance · Retry later. Request frequency, active sessions or daily anonymous sessions reached their limit. Wait for the rate window, running sessions or next UTC day respectively.

EMBED_BUDGET_EXHAUSTED

402 · Session work/anonymous issuance · Not retryable. Session budget or daily anonymous spending is exhausted. Review the budget; continue with a new token and session only when authorized, or wait for daily recovery. Do not loop issuance to bypass limits.

EMBED_MONTHLY_CAP_REACHED

402 · Session creation · Not retryable. The monthly cap requires stopping new sessions. An administrator decides whether to change the cap or action, or wait for the next UTC month.

EMBED_FIELD_NOT_ALLOWED

400 · Issuance/session creation · Not retryable. Remove unauthorized agent_override. Embedded session creation accepts only title, time_zone and initial_events. Initial events are plain-text user.message events with optional page context.

Other refusals

CodeHTTP / stageRecoveryRetryable
INVALID_PARAMS400 / validationCorrect the reported type, range or sizeNo
UNAUTHENTICATED401 / authenticationCheck missing/invalid credentials or a disabled publishable keyNo
NOT_FOUND404 / resource or Hosted exchangeVerify organization, public ID and policy; obtain a fresh Hosted link if expired or consumedNo
UPSTREAM_UNAVAILABLE502 / upstream callRetry later; check management writes for partial completion firstYes

For session permissions, archiving and event-content errors, see Agent limits and errors.

Kit errors and retries

praxis.error detail can be a KitError with code, status, retryable and message, or only { message }. Status 0 means no HTTP response.

CodeStatus / stageRecovery and retryability
network_error, stream_disconnected0 / network or streamRetry after restoring connectivity
http_errorResponse status / HTTPInspect response; without explicit retryable, 429 and 5xx permit retry
invalid_token0 / token retrievalgetToken returned an empty value; correct it, retryable
invalid_responseResponse status / JSON or historyCheck format and pagination progress; no automatic retry
invalid_streamResponse status / streamCheck SSE content and JSON; no automatic retry
event_cursor_not_found200 / history recoveryReload history to locate the session; no automatic retry
callback_error0 / callbackFix the host exception; not retryable
aborted, disposed0 / lifecycleCancelled request or destroyed component; do not retry the old instance

Non-Hosted token retrieval retries retryable failures up to 3 times at 1, 2 and 4 seconds. Revocation or budget refusal stops that retry loop. Transport-owned stream recovery reconnects at most 5 times; the interface may own recovery instead. Do not add an unlimited retry loop.

Diagnose failures

Retain time, environment, public agent_… ID, HTTP status, error code and X-Request-Id without credentials. Check token status under Operations → Issuance records, then replay the session. Repeat embed tests after recovery. See the Agent API for field definitions.

Limits and errors

Use this page to investigate agent configuration and session API failures. See Embed limits and errors for tokens, origins, anonymous quotas, and components. Organization balance and quotas belong to Usage and billing.

Request and execution limits

ItemValueUnit and meaning
Agent name1–256characters
Instructions system100,000maximum characters
Agent / session metadata16 / 64 / 512pairs / key characters / value characters
Session title500maximum characters
Initial initial_events50maximum events
Session and event list limit1–100; 20range; default items
Session budget2000default US cents (USD 20)
Session time_zoneUTCdefault; accepts IANA names
Custom tools32 / 16 KiBmaximum count / UTF-8 JSON bytes per input_schema

A budget uses type: "limit", max_list_cost.currency: "USD", and a positive integer string in max_list_cost.amount, measured in US cents. null removes the ceiling. The budget controls subsequent model requests; it is not a cost forecast. String limits use JavaScript string length, so not every character occupies one unit. Tool schema size is measured in UTF-8 bytes.

Handle errors by code

Native API errors contain error.type, error.message, and request_id. Classified service failures can also include error.code and error.retryable; request parsing failures may have only a type and message. Use the actual response: equal HTTP statuses do not imply equal causes.

INVALID_PARAMS

HTTP 400. A request field, format, or unsupported option is invalid. Correct the request from the error message; unchanged retries will not help.

UNAUTHENTICATED

HTTP 401. Credentials are missing or invalid. Check the Key for this environment and organization, then authenticate again.

SESSION_FORBIDDEN

HTTP 403. The caller cannot access this session. Check its user and organization. Do not retry automatically or probe other users by changing IDs.

AGENT_NOT_FOUND

HTTP 404. The agent or version is missing or inaccessible. Use the ID from creation and verify its version.

SESSION_NOT_FOUND

HTTP 404. The session is missing or inaccessible. Check the returned ID and caller identity; repeating a creation request is not recovery.

MODEL_NOT_ALLOWED

HTTP 400. The model is not allowed by the pinned version. Select an allowed model or update the configuration and create a new session.

SESSION_CONFLICT

HTTP 409. The session state conflicts with the operation. Read the session or pending tool before submitting again; do not resubmit a completed tool result.

SESSION_ARCHIVED

HTTP 409. The session is archived and cannot accept messages. Retain its history for inspection and create a new session for another conversation.

INVALID_STATE

HTTP 409. The state does not permit this operation, such as deleting a running session. Interrupt it or wait for idle or termination, then check the state again.

INVALID_PAGE_TOKEN

HTTP 400. The cursor is invalid or mismatched to the query. Restart at the first page, then pass next_page unchanged with the same filters.

UPSTREAM_UNAVAILABLE

HTTP 502. An upstream is temporarily unavailable. Retry after backing off; check existing results first when creating resources or sending messages.

SANDBOX_UNAVAILABLE

HTTP 503. The execution environment is temporarily unavailable. Retry after backing off. Record the request identifier and contact support if failures persist.

When to retry

Resolve the cause first when error.retryable is false. For HTTP 429, read Retry-After and avoid tight retry loops. Temporary upstream or environment failures can be retried after backing off. A broken network connection does not prove the request was never executed. Check known sessions and events before repeating creation or message requests.

After an event stream starts, execution failures can arrive as events. A successful connection status does not establish task success. Inspect replies, pending tool confirmations, and session status; accepting a message is separate from completing its task.

Capture diagnostic information

Record the time, environment, agent and session IDs, HTTP status, error type and code, and X-Request-Id. If only request-id or request_id is available, record that value too. Keep Keys, embed tokens, and visitor credentials out of diagnostic records.

Inspect sessions and usage, then compare your request with the API reference. To narrow a reproduction, start from the minimal request in the quickstart.

Automation

Automation combines platform capabilities into scripts and workflows that you run on demand or start with a trigger.

ObjectPurpose
ScriptTypeScript that executes from start to finish and returns one result.
WorkflowA YAML process with step checkpoints, waiting and suspension.
ActionA reusable capability with input and output schemas, called from a script or workflow.
RunOne execution of a script or workflow, including its input, output, status and calls.
TriggerStarts a published script or workflow on a schedule or when a webhook arrives.

Choose an entry point

Use a script for a calculation, batch job or sequence of API calls. Use a workflow when work must wait across time or resume from checkpoints. Package a shared capability as an action. Actions are called inside scripts or workflows; they have no standalone run endpoint.

Rerunning a failed script executes it from the beginning. Effects that already occurred are not rolled back. Add a trigger to a published target when it should run automatically.

Run once

Create a develop API Key in Console and set PRAXIS_API_KEY. In Bash with curl and jq installed, this sends a Bearer-authenticated request and adds one without calling a model:

export PRAXIS_API_URL="https://api-develop.deepraxis.ai"
: "${PRAXIS_API_KEY:?Set PRAXIS_API_KEY to your develop API Key}"
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"source":"export default async (input: { n: number }) => ({ answer: input.n + 1 });","language":"ts","input":{"n":41},"wait":60}' \
  | jq '{id,status,output,error}'

Expected: status: "completed" and output: {"answer":42}. If still pending or running, save the returned id as RUN_ID and read the result. The quickstart also shows how to save and publish this script.

Next steps

API reference → · Cookbook →

Quickstart

Use develop with Bash, curl and jq. Run the commands in order in one Bash session. No SDK installation is needed. This script adds two numbers without calling a billable capability.

1. Create an API Key

Open Develop Console → API Keys, create a key and store it in PRAXIS_API_KEY. See Account management for key management.

export PRAXIS_API_URL="https://api-develop.deepraxis.ai"
: "${PRAXIS_API_KEY:?Set PRAXIS_API_KEY to your develop API Key}"
command -v curl
command -v jq
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs?limit=1" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" | jq -e '.data | type == "array"'

Expected: both commands are available. The request prints true and exits with code 0, confirming access to the automation run list.

2. Run an ad hoc script

Create a run accepts source code and input. wait is the maximum number of seconds this HTTP request waits for a terminal run: 0–60, default 0. wait: 60 returns sooner if the run finishes. Reaching this limit does not cancel the run. HTTP 200 means the run is terminal, which can include failure; HTTP 202 means it is still pending or executing. Check status in both cases.

SOURCE='export default async (input: { n: number }) => ({ answer: input.n + 1 });'
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg source "$SOURCE" \
    '{source:$source,language:"ts",input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
jq -e '.id | startswith("run_")' <<< "$RUN"

Expected: true, with the new run ID saved in RUN_ID. The script takes n: 41 and returns answer: 42.

An HTTP 202 response is still a Run object. A subset of its fields looks like this (use the actual id returned by your request):

{
  "id": "run_...",
  "status": "running",
  "output": null,
  "error": null
}

Keep RUN_ID and use the next section to GET /automation/v1/runs/$RUN_ID until completed, faulted or cancelled. Do not create a duplicate run just because the wait expired.

3. Read the result

Retrieve the run until it reaches a terminal state. This helper polls for roughly three minutes. A failed run prints its actual error and returns a nonzero exit code.

read_result() {
  local result
  for attempt in $(seq 1 90); do
    result=$(curl --fail-with-body -sS \
      "$PRAXIS_API_URL/automation/v1/runs/$1" \
      -H "Authorization: Bearer $PRAXIS_API_KEY") || return 1
    case "$(jq -r '.status' <<< "$result")" in
      completed) jq -e '.output.answer == 42' <<< "$result"; return $? ;;
      faulted|cancelled) jq '{status,error}' <<< "$result"; return 1 ;;
    esac
    sleep 2
  done
  echo 'Still pending: retrieve the run again using its ID' >&2
  return 1
}
read_result "$RUN_ID"

Expected: true and exit code 0. The assertion checks both completion and the value 42. If the run is still pending, keep its ID and retrieve it again instead of creating another run.

4. Save and publish

Save the same source as a script draft. Read its current draft.revision, then publish a version. Each tutorial execution uses a fresh name.

Choose your own namespace, such as your project name. docs-samples below is only an example; change NAMESPACE once and the following requests reuse it.

NAMESPACE="docs-samples"
SCRIPT_NAME="hello-$(date +%s)-$RANDOM"
SCRIPT=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/scripts" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg namespace "$NAMESPACE" --arg name "$SCRIPT_NAME" --arg source "$SOURCE" \
    '{namespace:$namespace,name:$name,draft:$source}')")
jq -e '.id | startswith("scr_")' <<< "$SCRIPT"
DRAFT=$(curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/scripts/$NAMESPACE/$SCRIPT_NAME" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" | jq -er '.draft.revision')
curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/scripts/$NAMESPACE/$SCRIPT_NAME/versions" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg revision "$DRAFT" \
    '{version:"1.0.0",expected_draft_revision:$revision}')" \
  | jq -e '.version == "1.0.0"'

Expected: both checks print true; version 1.0.0 is published. If PRECONDITION_FAILED reports “草稿已修改”, read the draft again, review its content and publish with the current revision.

5. Run the published version

TARGET="$NAMESPACE/$SCRIPT_NAME@1.0.0"
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" \
    '{target:$target,input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
read_result "$RUN_ID"

Expected: true again, confirming the output is 42. Keep TARGET to add a trigger. When finished, archive the script; its name and history remain reserved.

API reference → · Cookbook →

Concepts

Scripts and workflows define the work. A run records one execution, a trigger determines when it starts, and an action provides a reusable capability during execution.

Definitions, versions and tags

Scripts and workflows share namespace/name; a name belongs to only one kind. Drafts are editable. Published versions are immutable and version numbers cannot be reused. Publishing identical content under the same version returns that version. Deprecation preserves history rather than deleting it.

References follow npm version and dist-tag conventions: docs-samples/hello@1.0.0 pins a version; @^1.2.0, @~1.2 and @1.x select ranges; @latest follows a tag. Omitting @ means @latest. Each run records the version it actually resolved.

Stable releases update latest according to version selection rules; publishing an older release does not automatically move it backwards. A prerelease requires an explicit tag such as beta. Tags such as stable and beta can move. A tag cannot be a version or range, and latest cannot be deleted. Moving a tag affects subsequent resolution without modifying published content.

Publish a version to preserve content and set a tag to point to an existing version. Tag writes do not require If-Match. Draft updates require the current ETag; publishing a draft requires expected_draft_revision.

Actions have revisions rather than semantic versions. Compatible changes take effect immediately; call records identify the revision used. Use a new action name for an incompatible schema. Published workflows lock script and child-workflow dependencies to exact versions; actions are not locked.

Run lifecycle

StatusMeaning
pendingAccepted and waiting to start.
runningExecuting.
waitingA workflow is waiting for an event, timer or child run.
suspendedA workflow has been suspended.
completedSucceeded; read output.
faultedFailed; read error.
cancelledCancellation completed.

Scripts do not enter waiting or suspended. During cancellation, a run may remain running with cancel_requested_at set. A timeout is faulted with error type timeout, not a separate status. Terminal runs cannot resume. Rerun creates a new root run whose rerun_of points to the original.

HTTP success means the request was handled, not that execution succeeded. Check status, output and error. Inspect script calls and logs, or workflow tasks and waits.

Child runs and origin

A script can start a child through praxis.automation.runs.create. The child's initiator remains the original human, API Key or trigger. parent_run_id expresses the parent relationship. Filter by channel=run for runs started by another run, or by parent_run_id for a particular parent's children. Public run details expose a parent summary and root_run_id.

Cancellation cascades to children; normal parent completion allows children to continue. A child's deadline cannot exceed the parent's remaining time. Repeating a target in the call chain is rejected as a cycle. See limits for depth and fan-out bounds.

Idempotency and retries

Create a run accepts Idempotency-Key, scoped to the owner, HTTP method, path and key for 24 hours. The request fingerprint excludes wait. Reuse the key and body when retrying a network request. A replay returns the current run with Idempotent-Replayed: true. Use a fresh key when deliberately starting another execution.

A different request with the same key returns IDEMPOTENCY_KEY_MISMATCH; a request still in progress returns IDEMPOTENCY_IN_FLIGHT. See the create-run error table. Idempotency does not roll back effects from calls completed before a failure.

Cost attribution

Runs do not carry a separate execution charge. Model, search and other capabilities called during a run are charged at their regular prices and attributed to the run and trigger. A calculation that calls no such capability incurs no model or search charge.

API reference → · Cookbook →

Write a script

A script exports a default function that receives input. Its return value becomes the run's output.

Before running this page, complete quickstart steps 1–3 in the same Bash session: set PRAXIS_API_URL and PRAXIS_API_KEY, define SOURCE and run it, then define and run read_result. The helper checks for answer 42, as expected by the arithmetic examples here.

Entry point and schemas

Save this as script.ts:

export default async (input: { n: number }): Promise<{ answer: number }> => {
  console.log("Adding one", input.n);
  return { answer: input.n + 1 };
};

The input schema comes from parameter types. The output schema comes from an explicit return type, with Promise unwrapped. Supported forms include primitives, objects, arrays, unions and resolvable local type aliases. Unsupported types produce an empty schema and a warning. An empty schema does not validate your fields. Do not rely on a return expression to infer output constraints when the return annotation is missing.

Validate the definition to inspect the generated schemas before publishing:

curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/validate" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --rawfile source script.ts '{kind:"script",source:$source}')" \
  | jq -e '.valid == true and .input_schema.properties.n.type == "number" and .output_schema.properties.answer.type == "number"'

Expected: true and exit code 0. Inspect errors and warnings in the full validation response when a type cannot be inferred.

Execution and logs

RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --rawfile source script.ts \
    '{source:$source,language:"ts",input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
read_result "$RUN_ID"
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs/$RUN_ID/logs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

Expected: the result assertion prints true; the log response includes Adding one 41. console.log/info/debug produce info entries; console.warn and console.error retain their levels. Logs are separate from output: use return for data the caller needs. Read logs, or subscribe with Accept: text/event-stream.

The global praxis object

The runtime supplies praxis. No import, API Key or package installation is required inside a script. Its SDK entry points are listed below. Methods use the reference pages' path, query and body options, for example praxis.automation.runs.create({body:{target,input}}). The initiator's permissions still govern access.

Entry pointReference
praxis.agentAgent
praxis.modelModel
praxis.fileFile
praxis.sandboxSandbox
praxis.searchSearch
praxis.databaseDatabase
praxis.automationAutomation
praxis.organizationOrganization
praxis.metaMeta

Scripts cannot access external networks directly. Use platform capabilities and await calls to receive their results and handle failures.

Call a built-in action

call("namespace/name", input) takes an Action reference and an input object and returns the Action's output. It is one step of the current run: it has its own input and output and takes one row in the call ledger; it does not create a child run. praxis is the platform namespace, and a bare name resolves only to platform Actions. A call always uses the Action's current revision; @version is not supported yet, and the namespace::name form is no longer accepted.

A failed call throws an error carrying code, message, hint, the call-ledger position seq and request_id. Several calls can run concurrently with Promise.all. praxis.* is the platform SDK only: call(...) and praxis.ocr(...) were removed and fail at runtime with a hint to use call.

Save this as chat.ts. It calls praxis/chat. The model is built into the action and maintained by its author; callers do not select a model. The same applies to praxis/ocr, praxis/image and praxis/embed. The legacy model field is deprecated and ignored. This call incurs model usage charges:

export default async (input: { prompt: string }) => {
  return await call("praxis/chat", {
    messages: [{ role: "user", content: input.prompt }],
    stream: false,
  });
};
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --rawfile source chat.ts \
    '{source:$source,language:"ts",input:{prompt:"Say hello in one sentence."},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs/$RUN_ID" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" | jq '{id,status,output,error}'

Expected: terminal status completed, model output in output, and null error. If still pending or running, repeat the final GET until terminal. For faulted, inspect the error; HTTP success alone does not prove a successful model call. Retrieve the action at praxis/chat for its complete input and output contract.

Call your own action

This example creates an action, then calls its full name. The generated script contains a literal reference so it can be checked before publication.

ACTION_NAME="add-one-$(date +%s)-$RANDOM"
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/actions" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg name "$ACTION_NAME" '{
    namespace:"docs-samples",name:$name,
    input_schema:{type:"object",properties:{n:{type:"number"}},required:["n"],additionalProperties:false},
    output_schema:{type:"object",properties:{answer:{type:"number"}},required:["answer"],additionalProperties:false},
    implementation:{kind:"code",language:"ts",source:"export default async (input: { n: number }) => ({ answer: input.n + 1 });"}
  }')" | jq -e '.id | startswith("act_")'
SOURCE="export default async (input: { n: number }) => await call(\"docs-samples/$ACTION_NAME\", input);"
RUN=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/runs" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg source "$SOURCE" \
    '{source:$source,language:"ts",input:{n:41},wait:60}')")
RUN_ID=$(jq -er '.id' <<< "$RUN")
read_result "$RUN_ID"

Expected: both creation and result checks print true, with answer 42. Archive the action when finished. Actions are reusable capabilities, but cannot orchestrate other actions, scripts or workflows from inside their implementation.

Call another script or workflow

invoke("namespace/name@version", input) starts another published script or workflow as a child run, waits for it to finish and returns the child's output; leave out @version for the latest. Unlike call, which is one step inside the current run and creates no child run, invoke creates a child run with its own ID, status and logs, and the call record points to it through child_run_id.

export default async (input: { urls: string[] }) => {
  // Use Promise.all to run several invokes at once; child runs start through a queue.
  const pages = await Promise.all(input.urls.map(url => invoke("team/fetch-page", { url })));
  return { pages };
};
  • The wait is capped: invoke waits synchronously, bounded by the parent run's deadline. At the cap it throws INVOKE_TIMEOUT with the child run ID in the message; the child is not cancelled, so you can read its result separately. For long waits use a workflow run: step, which consumes no compute while it waits.
  • Failures: a child that fails, is cancelled or cannot be created makes invoke throw; code is the child's error code and message carries the child run ID. A missing or archived target fails immediately and nothing is queued.
  • Cancel and timeout: when the parent run is cancelled or fails, children it is still waiting on are cancelled with it.
  • Finding child runs: list runs filtered by parent_run_id for each child's ID, target, status and duration.
  • Limits: at most 100 child runs per run, 20 executing at once (the rest queue), nesting up to 10 levels, and a run cannot invoke itself.

A script cannot start a detached child with invoke; to start a run without waiting, call create run.

Output and time budgets

Return JSON-serializable data. Do not return undefined, BigInt or circular references. UTF-8 encoded JSON output must fit within 1 MiB; larger output faults the run. Store large results in a file or other storage and return a reference.

The default script wall-clock limit is five minutes. CPU time is capped at 30 seconds and further bounded by the remaining deadline. Waiting for a platform call differs from executing pure computation; increasing wait time does not fix an infinite loop. See limits and errors for the full limits.

Diagnosing errors

CodeOriginal messageAction
SCRIPT_ERROR脚本执行出错Inspect the original exception and user-code locations in detail, fix the script and start a new run.
CPU_LIMIT_EXCEEDED脚本 CPU 时间超出上限Remove infinite loops and reduce computation per batch.
PAYLOAD_TOO_LARGEPayload exceeds 1 MiBReduce JSON output and return a storage reference.
INPUT_INVALIDValue must be JSON serializableReturn an explicit JSON value such as an object or null.

Execution failures appear in Run.error. Rejected create requests return an HTTP problem response. Handle these separately.

API reference → · Cookbook →

Triggers

Triggers separate when work runs from its definition.

Before running this page, complete quickstart steps 1–3 in the same Bash session: set PRAXIS_API_URL and PRAXIS_API_KEY, define SOURCE and run it, then define and run read_result. The helper checks for answer 42, as expected by the arithmetic examples here. Also save and publish the script and set TARGET in steps 4–5. TARGET must reference that published script, which accepts n and returns answer.

Scheduled execution

Choose exactly one schedule form:

FormUse
cron: "0 9 * * *"Daily at 09:00 local time; five fields, minute precision, with L, W and # support.
every: "PT5M"Every five elapsed minutes; the minimum is one minute.
atAn absolute RFC 3339 timestamp with offset or Z, between now and one year from now.

timezone accepts an IANA name and defaults to UTC. For local cron schedules, nonexistent spring DST times are skipped and repeated autumn times execute once. every uses elapsed time and at uses an absolute instant; neither uses the cron DST rule.

Preview the next occurrences:

curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers/preview" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"schedule":{"cron":"0 9 * * *"},"timezone":"Asia/Shanghai"}' \
  | jq -e '{next_fire_times} | select(.next_fire_times | length == 5)'

Expected: five future timestamps, for example:

{
  "next_fire_times": [
    "2026-10-13T01:00:00.000Z",
    "2026-10-14T01:00:00.000Z",
    "2026-10-15T01:00:00.000Z",
    "2026-10-16T01:00:00.000Z",
    "2026-10-17T01:00:00.000Z"
  ]
}

Dates depend on when you preview. Z means UTC. For example, 2026-10-13T01:00:00.000Z is October 13 at 09:00 in Shanghai (UTC+8). The timezone controls the cron schedule; returned timestamps are always expressed in UTC.

Create the trigger, then fire it once to check the target:

TRIGGER=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" '{
    name:"daily-hello",target:$target,kind:"schedule",input:{n:41},
    schedule:{cron:"0 9 * * *"},timezone:"Asia/Shanghai",
    overlap:"skip",catchup_window_seconds:300
  }')")
SCHEDULE_ID=$(jq -er '.id' <<< "$TRIGGER")
jq -e '.state == "enabled" and (.next_fire_times | length == 5)' <<< "$TRIGGER"
FIRING=$(curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/fire" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}')
jq -e '.outcome == "started"' <<< "$FIRING"
read_result "$(jq -er '.run_id' <<< "$FIRING")"

Expected: all three assertions print true. started confirms launch; read_result uses the returned run_id to retrieve the run, check completion and confirm answer 42.

Pin target with @1.0.0 or follow a tag with @latest. Every firing records the resolved version. Static input is validated on creation and update, then checked again when firing in case the target's schema changed.

Overlap, delays and active windows

The default overlap: "skip" records skipped_overlap if the preceding run is unfinished. allow starts overlapping runs subject to concurrency limits. Buffering, cancelling previous runs and historical backfill are not available.

catchup_window_seconds defaults to 300. A scheduling delay beyond this window produces missed without starting a run. This does not promise to replay every occurrence after resuming. Paused triggers do not execute; resuming continues future scheduling.

Set RFC 3339 start_at and end_at timestamps to bound the active window. A one-shot at expires after firing; reaching the end time also makes the trigger expired. 5 consecutive firing failures pause it automatically. Use pause and resume to control future firings.

Pause the trigger created above, then resume it:

curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/pause" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}' \
  | jq -e '.state == "paused" and (.next_fire_times | length == 0)'
curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/resume" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}' \
  | jq -e '.state == "enabled" and (.next_fire_times | length == 5)'

Expected: both requests print true. Resuming this daily schedule returns its next five occurrences.

A workflow YAML cron/every schedule manages a source=definition trigger when published as latest. Change it by republishing the definition, not by PATCHing the trigger. You can pause or resume it. schedule.after and schedule.on are not supported yet.

Webhooks

With input_mode: "body", incoming JSON becomes run input; non-JSON bodies become strings. This sends n:41 to the quickstart script:

HOOK=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" '{
    name:"webhook-hello",target:$target,kind:"webhook",webhook:{input_mode:"body"}
  }')")
WEBHOOK_ID=$(jq -er '.id' <<< "$HOOK")
WEBHOOK_URL=$(jq -er '.webhook_url' <<< "$HOOK")
curl --fail-with-body -sS "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -H "webhook-id: hello-$(date +%s)-$RANDOM" \
  --data '{"n":41}' | jq -e '.id | startswith("frg_")'

Expected: HTTP 202 and true, with a firing ID in the response. Check firing history for the asynchronous launch result.

The full URL is returned only on creation or rotation. Its token is the credential: no API Key is needed, and the URL must stay private. Rotation invalidates the old URL. Invalid tokens, paused triggers and nonexistent triggers return 404. Bodies are limited to 1 MiB. The same webhook-id is deduplicated for 24 hours.

Signature headers and envelopes

The endpoint authenticates the secret URL. It does not automatically verify Standard Webhooks, GitHub or Stripe HMAC signatures. Supplying webhook-signature does not mean it was verified.

To read headers, create the trigger with webhook: {input_mode:"envelope"}. The target schema must accept the following shape rather than the quickstart's n input:

{
  "body": { "n": 41 },
  "headers": { "content-type": "application/json", "webhook-id": "delivery-1" },
  "webhook_id": "delivery-1",
  "received_at": "2026-10-11T12:00:00.000Z"
}

An absent webhook_id becomes null. Only these headers are forwarded: content-type, user-agent, webhook-id, webhook-timestamp, webhook-signature, x-github-event, x-github-delivery, x-hub-signature-256 and stripe-signature. Authorization and Cookie are excluded.

JSON bodies are parsed before delivery; the envelope has no separate original-byte field. For protocols that sign raw bytes, JSON.stringify(body) cannot substitute for the original payload. Verify such signatures in your receiving service before forwarding to the secret URL. There is currently no generic verification example that can safely be applied directly to this JSON envelope.

Inspect firings and clean up

curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/triggers/$WEBHOOK_ID/firings" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  | jq -e '.data | length > 0'
curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/triggers/$WEBHOOK_ID/firings" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  | jq '.data[] | {outcome,run_id,resolved_version,error}'

Expected: the first request prints true; the second lists records. Read again shortly if asynchronous processing is still underway. started means launched; inspect error for failed. missed exceeds the catchup window, skipped_overlap has an unfinished prior run, and deduped matched a prior delivery. Follow run_id to inspect terminal execution status. Firing records are retained for 30 days.

Delete both tutorial triggers when finished to stop future scheduled execution:

for id in "$SCHEDULE_ID" "$WEBHOOK_ID"; do
  curl --fail-with-body -sS -X DELETE \
    "$PRAXIS_API_URL/automation/v1/triggers/$id" \
    -H "Authorization: Bearer $PRAXIS_API_KEY"
done

Expected: both curl requests exit 0. Deleting a trigger does not affect runs already started.

API reference → · Cookbook →

Limits and errors

These defaults bound a run and its descendants. A shorter caller deadline also limits child execution. Concurrency limits queue work; they do not increase execution capacity.

Execution and data

LimitValue
Nesting depth10
Children per run / simultaneously active children100 / 20
Nodes per run tree1,000
Concurrent runs per owner / definition100 / 20
Action / sandbox action timeout30 s / 10 min
Script wall-clock timeout5 min
Workflow default / maximum timeout24 h / 7 days
Maximum create-run wait60 s
Payload soft threshold / hard limit256 KiB / 1 MiB
Call payload / checkpoint inline threshold32 KiB / 256 KiB
Task executions per run, including loops and retries4,000
Durable execution step budget per run9,000
Branches per fork / nested concurrent effects10 / 25
Idempotency retention24 h
Run log retention30 days
Records per page100
Label count / key length / value length50 / 40 / 500 characters
Description length1,000 characters
SSE heartbeat interval20 s

The payload soft threshold is not the rejection boundary. The hard limit is 1 MiB, measured as encoded UTF-8 JSON bytes. Inline thresholds determine storage handling; they do not permit oversized script output.

Script runtime

LimitValue
CPU timeAt most 30 seconds, further bounded by the remaining deadline
Connector requests per script1,000, including logs and progress
Concurrent connector operations20
One log message4,096 UTF-8 bytes; longer messages are truncated
Call preview512 characters
Script error detail1,000 characters
User stack frames retainedUp to 5

Script output must be JSON-serializable. Failed output encoding faults the run. Scripts have no checkpoints or transactional rollback. See Write a script for tested error messages and fixes.

Triggers

LimitValue
Enabled triggers per owner100
Minimum every interval1 minute
cron precision5 fields, one minute
One-shot at windowNow through one year from now
Default catchup window300 seconds
Future occurrences in a previewUp to 5
Consecutive firing failures before pause5
Consecutive firing failures before automatic pause5
Webhook body1 MiB
webhook-id deduplication24 hours
Firing history30 days

Find the right error

An HTTP error rejects the request. A faulted run records an execution error in error, even when retrieving that run returns HTTP 200. Keep the run ID and X-Request-Id when investigating.

For a concrete schedule failure, INTERVAL_TOO_SHORT reports every must be at least one minute; use PT1M or longer. Do not retry the unchanged invalid schedule.

API reference → · Cookbook →

Models

A unified interface to LLM, image, audio and embedding models; OpenAI- and Anthropic-protocol compatible so existing SDKs work unchanged.

Quickstart

Call deepseek-flash with a cloud API key, read the amount charged, and find the record for that call. This example incurs a small charge.

1. Prepare your key and terminal

Open Console → API Keys, click “创建 Key” (Create Key), enter a name, and create the key. Securely inject it into the current terminal as PRAXIS_API_KEY. Never put it in code or logs. You can use an existing key.

Install Python 3.10+ and uv. Run in a macOS / Linux terminal:

test -n "${PRAXIS_API_KEY:-}"

Expected: exit code 0. Exit code 1 means the variable is missing; set it before continuing. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Send a request and read its cost

Save as quickstart.py. From that directory, run uv run -q --no-project --with openai --with anthropic --with google-genai python quickstart.py.

import json
import os
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url=os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com") + "/openai/v1",
    timeout=90.0,
    max_retries=0,
)
raw = client.chat.completions.with_raw_response.create(
    model="deepseek-flash",
    messages=[{"role": "user", "content": "Reply with exactly: Hello Praxis."}],
    max_tokens=256,
)
reply = raw.parse()
usage = reply.usage.model_dump()
request_id = raw.headers.get("x-request-id")
generation_id = raw.headers.get("x-praxis-generation-id")
assert raw.status_code == 200 and reply.choices[0].message.content
assert request_id and generation_id
assert isinstance(usage.get("cost"), (int, float)) and usage["cost"] >= 0
print(reply.choices[0].message.content)
print("usage:", usage)
print("X-Request-Id:", request_id)
print("X-Praxis-Generation-Id:", generation_id)
Path("quickstart-result.json").write_text(json.dumps({
    "request_id": request_id, "generation_id": generation_id, "cost": usage["cost"],
}))

Expected: exit code 0, a nonempty reply (usually Hello Praxis.), usage, two IDs, and a new quickstart-result.json. This file contains only the call IDs and cost, never the key.

usage includes input and output token counts. usage.cost is the amount actually charged in USD, not a price per million tokens. A missing cost does not mean zero. See Chat Completions API for field definitions.

3. Find this call

Open Console → 模型 (Models) → 调用日志 (Call logs). Paste the previous step’s X-Request-Id into “按 X-Request-Id / Generation ID 查找” and press Enter. These Console labels were checked on 2026-10-13.

Expected: the deepseek-flash call appears with its status, usage, and cost. Records can take a few seconds to appear. Use the key’s account and environment, with a time range covering the call.

For equivalent verification without a browser, save the following as verify-generation.py. In the same directory, run uv run -q --no-project --with openai --with anthropic --with google-genai python verify-generation.py.

import json
import os
import time
from decimal import Decimal
from pathlib import Path
import httpx

saved = json.loads(Path("quickstart-result.json").read_text())
base = os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com")
with httpx.Client(timeout=60.0) as client:
    for attempt in range(10):
        response = client.get(
            base + "/model/v1/generation",
            params={"id": saved["generation_id"]},
            headers={"Authorization": "Bearer " + os.environ["PRAXIS_API_KEY"]},
        )
        if response.status_code != 404:
            break
        time.sleep(3)
    response.raise_for_status()
    record = response.json()
    assert record["id"] == saved["generation_id"]
    assert record["model"] == "deepseek-flash"
    assert abs(Decimal(str(record["cost"])) - Decimal(str(saved["cost"]))) <= Decimal("0.0000005")
    print("Verified generation:", record["id"])
    print("cost:", record["cost"])
    print("X-Request-Id:", response.headers.get("x-request-id"))

Expected: exit code 0, Verified generation, and the cost recorded to six decimal places in USD; the script allows for that rounding. This query uses X-Praxis-Generation-Id, not X-Request-Id or the response body’s id. See the generation record API.

Troubleshooting

  • 404 when querying a record: the script retries every 3 seconds, up to 10 attempts, while the record becomes available. If it still fails, keep both IDs, check the key, environment, and generation ID belong to this call, then query again.
  • Model "gpt-4o" does not exist.: model IDs are not automatically mapped. Use deepseek-flash or an ID from the catalog.

Next, try streaming or tool calling.

API reference → · Cookbook →

Model catalog

Models and prices change. Read the Console → Model catalog or live API instead of relying on a static list.

1. Prepare the query

Install Python 3.10+ and uv. The native GET /model/v1/models catalog needs no key. This example also queries the OpenAI-compatible catalog, so set PRAXIS_API_KEY using the quickstart.

Expected: the key environment variable is set. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Check the model and protocol

Save as catalog.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python catalog.py.

import os
import httpx

base = os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com")
with httpx.Client(timeout=60.0) as client:
    response = client.get(base + "/model/v1/models")
    response.raise_for_status()
    models = response.json()["data"]
    model = next(item for item in models if item["id"] == "deepseek-flash")
    assert "openai.chat.completions" in model["supported_apis"]
    for field in ("id", "type", "supported_apis", "context_window", "pricing"):
        print(field + ":", model[field])
    print("X-Request-Id:", response.headers.get("x-request-id"))

    filtered = client.get(
        base + "/openai/v1/models",
        params={"api": "responses"},
        headers={"Authorization": "Bearer " + os.environ["PRAXIS_API_KEY"]},
    )
    filtered.raise_for_status()
    ids = [item["id"] for item in filtered.json()["data"]]
    assert "deepseek-flash" in ids
    print("Responses models:", ids)
    print("X-Request-Id:", filtered.headers.get("x-request-id"))

Expected: exit code 0, the type, protocols, context, and prices for deepseek-flash, and a Responses list containing that ID. Checked on 2026-10-13; current query results take precedence. If an assertion fails, do not send a request to that protocol. Select a supporting model from the latest list first.

3. Choose from the results

Start with type for the task category, then confirm the intended protocol in supported_apis. Model names do not establish protocol support. The native list also accepts supported_api=openai.responses; api=chat,embeddings on the OpenAI-compatible list matches either API.

Check context and output limits, then prices for input, output, cache reads, or non-token units. Models and the catalog explains billing units and availability. List models API defines the fields.

Expected: the selected ID supports your protocol, its limits fit your request, and its billing unit fits the task. Then use the quickstart to make a real call and check its actual cost.

API reference → · Cookbook →

Concepts

A model ID selects the model; a protocol defines the request and response format. The platform completes the call through an eligible route.

Start with the quickstart for your first call, or the catalog to select a model.

API reference → · Cookbook →

Models and the catalog

The catalog describes your choices; a call record describes actual usage and charges. They serve different purposes.

Model IDs and capabilities

Use the complete catalog id as model, for example deepseek-flash. Display names are not IDs. A stable ID does not promise an unchanging version; check the model description for rolling releases.

type distinguishes language, image, speech, video, embedding, and other model categories. It does not imply support for every related endpoint. supported_apis lists enabled protocols; check Chat, Responses, and Messages separately. Input and output modalities describe content capabilities, not native support for every file format.

The context window limits the content available for input and generation, not the guaranteed output length. The native catalog uses context_window; the OpenAI-compatible catalog uses context_length. Check output limits separately in the model description and API reference.

Listed prices and actual charges

pricing amounts are in USD, with units determined by each price item. Token prices are per token; multiply by one million for a per-million display. Input and output have separate prices. input_cache_read prices cached input reads; it does not guarantee cache hits.

Not every model bills by token: video can bill by second, images by image, OCR by page, and transcription by audio minute. A missing price does not mean free. Do not multiply every price by one million. See List models API for price items and field definitions.

Chat’s usage.cost is the actual USD charge. Multiplying catalog prices by token counts is only an estimate and may miss caching or other units. Verify real calls with the generation lookup in the quickstart.

Availability

Availability measures whether the platform and upstream can complete valid requests. Platform and upstream failures count; caller-caused rejections, such as invalid parameters, do not. It does not mean arbitrary parameters will work or guarantee the next request’s success.

The catalog guide shows how to retrieve current entries. This page does not maintain a static model or price list.

API reference → · Cookbook →

Routing

You choose a model. The platform selects a route supporting that model and your request protocol. A model can have multiple routes, whose number and health can change.

From request to route

The platform checks the model, protocol, and parameter requirements, then selects an available route using health and priority. Alternatives must support the same protocol; failover does not silently translate between Chat, Responses, and Messages.

Session affinity can influence eligible routes within the same priority. Priority does not permanently bind a supplier or guarantee the cheapest route on every call.

Failover

After a retryable upstream failure, upstream rate limit, or unavailable route, the platform can try another suitable route. If no alternative exists or attempts still fail, the request returns an error. Retries are not unlimited, and an already-started stream is not guaranteed to resume seamlessly.

Failover cannot repair invalid caller parameters. Applications must still check completion and handle failures using errors and limits.

Parameter requirements and provider selection

provider.require_parameters requires routes to declare support for relevant tool and sampling parameters in the request. It narrows eligible routes; it does not add model capabilities or guarantee a tool call or particular answer. Requests can be rejected when no route meets the requirements. See the Chat API reference for parameter support.

Provider-selection options order, only, and ignore within provider are BYOK-only. Capability constraints such as require_parameters are not provider selection. The platform chooses routes for platform models. BYOK is coming soon; do not depend on it to select platform suppliers.

API reference → · Cookbook →

Protocol compatibility

Protocol compatibility lets you keep your client’s request format. It does not mean every model supports every protocol or every official feature. Check the model’s supported_apis and endpoint support notes.

One address, https://api.deepraxis.com, with one entry per protocol family:

EntryProtocols
/openaiOpenAI protocols
/anthropicAnthropic protocols
/googleGoogle Gemini API

OpenAI Chat Completions

POST /openai/v1/chat/completions. A common chat protocol; streaming (SSE), tool calls, and multimodal input depend on the model. See Chat API for field support and session affinity for session identifiers.

OpenAI Responses

POST /openai/v1/responses plus retrieve / delete / cancel / input_items. OpenAI's stateful next-generation protocol; agents like Codex use it. See Responses API for support. Response compaction and input-token counting endpoints are not implemented and return 501.

OpenAI Files

/openai/v1/files: upload user_data files for later requests; list, retrieve metadata, download, delete.

OpenAI Images

POST /openai/v1/images/generations: use a model that declares image-generation support in the catalog.

Anthropic Messages

POST /anthropic/v1/messages. Claude Code and the Anthropic SDK use it. See Messages API for support.

Anthropic Files

/anthropic/v1/files: upload, list, retrieve metadata, delete; uploads are not downloadable (same as upstream).

Google Gemini API (generateContent)

POST /google/v1beta/models/{model}:generateContent. Currently image generation and editing with the native Gemini protocol.

Model catalog

Use GET /openai/v1/models?api=responses to find Responses models; api=chat,embeddings matches either API. The native catalog GET /model/v1/models and its model detail endpoint expose each model’s supported_apis; filter the native list with ?supported_api=openai.responses.

GET /openai/v1/models: the models this key can call, OpenAI-shaped; clients such as Kilo Code discover models through it.

Anthropic clients use GET /anthropic/v1/models; Google clients use GET /google/v1beta/models. Each returns the corresponding protocol’s model list.

Standardized paths

Every entry also has a standardized long path /v1/router/<vendor>/... with identical semantics — see the API reference.

→ Go deeper: OpenAI-compatible API · Anthropic-compatible API

API reference → · Cookbook →

Session affinity

Session affinity lets later requests in a session prefer a previously successful route when conditions allow. It is a routing preference, not a supplier lock or storage for your conversation history.

Why it helps

Multi-turn conversations often repeat system prompts, tool definitions, and earlier messages. Keeping repeated prefixes on a cache-capable route improves the chance of upstream cache hits, reducing repeated-input cost and latency. The same session ID does not guarantee a hit: content changes, cache expiry, or failover can prevent it.

Identifying a session

Clients can identify a session with body session_id or header x-session-id; the body takes precedence when both exist. Without an explicit session ID, prompt_cache_key or a derived conversation-prefix identity may be used. Content changes affect derived identities. Keep an explicit ID stable for a long conversation, and use different IDs for different conversations.

When it applies

Cache affinity requires a route supporting the model and protocol, preserved input, and a cache-read price below the ordinary input price. Only successfully completed calls update affinity. Later calls must still pass current health and capability checks.

Affinity reorders routes only within the same priority; it cannot bypass cooldowns or parameter requirements. Normal routing applies after 10 idle minutes since the last success, when the previous route becomes ineligible, or when the affinity record is unavailable. Applications cannot rely on permanent upstream state placement.

Chat requests must still include the required message history. Stateful Responses continuation is a separate protocol capability; a session ID does not replace previous_response_id. See Chat API and Responses API for parameters.

API reference → · Cookbook →

BYOK: bring your own keys

BYOK (your own model-provider credentials) is coming soon; configuration is not yet available in Console. For now, use a Praxis cloud API key with platform models from the catalog. The platform manages routes and billing. Follow the quickstart for your first call.

API reference → · Cookbook →

Guides

OpenAI-compatible API

Use the OpenAI SDK at https://api.deepraxis.com/openai/v1. Requests are forwarded using the same protocol; compatibility does not imply that every official endpoint, field, or model is available.

Choose the right address

ClientBase URLModel catalog
OpenAI SDKhttps://api.deepraxis.com/openai/v1/openai/v1/models
Anthropic SDKhttps://api.deepraxis.com/anthropic/anthropic/v1/models
Google GenAI SDKhttps://api.deepraxis.com/google/google/v1beta/models
Praxis native APIhttps://api.deepraxis.com/model/v1See the native API, including providerOptions

Each compatible catalog lists models that offer its protocol. Membership in the OpenAI catalog does not imply both Chat Completions and Responses support. An undeclared operation returns 404 model_protocol_not_offered.

Field differences: where to check

Your application usesCheck before sending
messages, tools, sampling, reasoning, or provider preferencesChat Completions field support
input, stored responses, or response toolsResponses field support
usage.cost or generation IDsCost handling in the migration guide and generation records
Uploads and file purposesUpload a file
An endpoint beyond chatOperation support and unsupported alternatives

Unknown top-level fields can be forwarded to the same-protocol upstream; acceptance does not prove that they took effect. Declared fields still undergo validation. Consult the endpoint's field support before relying on an option.

1. Prepare

You need Python 3.10+, uv, and a Praxis key. Create one in Console → API Keys and securely inject it into the current terminal as PRAXIS_API_KEY. Never put the key in code or logs. Keep your original service configuration and key for rollback. Run the commands below in Bash on macOS or Linux.

test -n "${PRAXIS_API_KEY:-}"

Expected: exit code 0. Exit code 1 means the variable is missing; set it before continuing.

2. Verify Responses

This example uses deepseek-flash. As of October 12, 2026, glm-5.3-flash also offers Responses; gpt-5.6-sol offers Chat Completions but not Responses. Confirm the operation in the model catalog.

unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
uv run -q --no-project --with openai --with anthropic python - <<'PY'
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.deepraxis.com/openai/v1",
    api_key=os.environ["PRAXIS_API_KEY"],
    timeout=90.0,
    max_retries=0,
)
models = client.models.list()
assert "deepseek-flash" in {m.id for m in models.data}
raw = client.responses.with_raw_response.create(
    model="deepseek-flash",
    input="Reply OK.",
    max_output_tokens=1024,
)
reply = raw.parse()
text = reply.output_text
print("HTTP", raw.status_code)
print("X-Request-Id:", raw.headers.get("x-request-id"))
print("X-Praxis-Generation-Id:", raw.headers.get("x-praxis-generation-id"))
print(text)
assert raw.status_code == 200 and text and text.strip()
assert raw.headers.get("x-request-id")
PY

Expected: exit code 0, HTTP 200, nonempty text (usually OK.), and an X-Request-Id. Assertions check both model availability and a real reply, not just connectivity. Keep the request ID for troubleshooting, never the key. The example incurs a small usage charge.

Expected: output_text contains the final answer; reasoning output is not treated as user-facing text. If the request returns model_protocol_not_offered, select a Responses-capable model. If it says does not exist, copy an exact catalog ID. To roll back an existing application, restore the original base URL, key, and model together, then rerun its original smoke request.

API reference → · Cookbook: migrate from OpenAI →

Anthropic-compatible API

Use the Anthropic SDK at https://api.deepraxis.com/anthropic. The SDK adds /v1/messages; do not append /v1 to its base URL. Requests are forwarded using the same protocol, subject to the model's declared capabilities.

What compatibility covers

/anthropic/v1/models lists Anthropic-compatible models. Do not assume an ID from another protocol's catalog works here. An undeclared operation returns 404 with anthropic.messages is not offered for …; switching the URL does not translate unsupported model capabilities.

Field differences: where to check

Your application usesDifference to check
thinking and output_config.effortModel-dependent reasoning options; some models cannot disable thinking. See Messages
Cache controls, tools, metadata, beta or workspace headersMessages field support; do not assume every official extension is available
count_tokensA calibrated estimate, marked by x-praxis-token-count: estimated; see Count tokens
Model limits and capabilitiesList models; unknown limits can be null
File uploads and expirationUpload a file
CostsMessages cost handling and generation records

Unknown top-level fields can reach the same-protocol upstream, which decides whether to accept them. Declared fields are validated; a successful response alone does not establish that every option was applied. See the operation list for support and migration alternatives for missing operations.

1. Prepare

You need Python 3.10+, uv, and a Praxis key. Create one in Console → API Keys and securely inject it into the current terminal as PRAXIS_API_KEY. Never put the key in code or logs. Keep your original service configuration and key for rollback. Run the commands below in Bash on macOS or Linux.

test -n "${PRAXIS_API_KEY:-}"

Expected: exit code 0. Exit code 1 means the variable is missing; set it before continuing.

2. Verify Messages and token counting

unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
uv run -q --no-project --with openai --with anthropic python - <<'PY'
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.deepraxis.com/anthropic",
    api_key=os.environ["PRAXIS_API_KEY"],
    timeout=90.0,
    max_retries=0,
)
models = client.models.list()
assert "deepseek-flash" in {m.id for m in models.data}
raw = client.messages.with_raw_response.create(
    model="deepseek-flash",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Reply OK."}],
)
reply = raw.parse()
text = "".join(block.text for block in reply.content if block.type == "text")
print("HTTP", raw.status_code)
print("X-Request-Id:", raw.headers.get("x-request-id"))
print("X-Praxis-Generation-Id:", raw.headers.get("x-praxis-generation-id"))
print(text)
assert raw.status_code == 200 and text and text.strip()
assert raw.headers.get("x-request-id")
count = client.messages.with_raw_response.count_tokens(
    model="deepseek-flash",
    messages=[{"role": "user", "content": "Reply OK."}],
)
print("count_tokens:", count.parse().input_tokens)
print("X-Request-Id:", count.headers.get("x-request-id"))
print("x-praxis-token-count:", count.headers.get("x-praxis-token-count"))
assert count.status_code == 200
assert count.headers.get("x-praxis-token-count") == "estimated"
PY

Expected: exit code 0, HTTP 200, nonempty text (usually OK.), and an X-Request-Id. Assertions check both model availability and a real reply, not just connectivity. Keep the request ID for troubleshooting, never the key. The example incurs a small usage charge.

Expected: text is extracted from type == "text" blocks, even if a thinking block comes first. The second call prints a token estimate and estimated; do not use that estimate as billed usage.

If you receive 400 with cannot disable thinking, remove the request to disable thinking or choose a model that permits it. For anthropic.messages is not offered for …, choose an Anthropic-compatible model offering the operation. To roll back an existing application, restore its original URL, key, model, and thinking options, then rerun the original Messages request.

API reference → · Cookbook: migrate from Anthropic →

Streaming responses

Streaming splits one answer into multiple events. After displaying text, read the terminal status and usage to confirm completion.

This page uses deepseek-flash with POST /openai/v1/chat/completions and claude-opus-5 with POST /anthropic/v1/messages. Both examples were verified on 2026-10-12.

1. Prepare your terminal

Use a Praxis cloud API key in PRAXIS_API_KEY; see API keys and authentication. Do not put the key in source code. Install Python and uv; TypeScript examples use Bun.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. The examples default to https://api.deepraxis.com. To use another environment, set PRAXIS_API_BASE to its API origin, without a trailing slash or API path.

Save each Python block using its indicated filename. Run it with uv run -q --no-project --with openai --with anthropic python <filename>. Each block is independent.

2. Read text and final usage with Python

Save as streaming.py. Run uv run -q --no-project --with openai --with anthropic python streaming.py.

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
text, finish, usage = '', None, None
with client.chat.completions.with_streaming_response.create(
    model='deepseek-flash', messages=[{'role':'user','content':'Reply with exactly: Hello streaming.'}],
    max_tokens=256, stream=True, stream_options={'include_usage':True},
) as response:
    print('X-Request-Id:', response.headers.get('x-request-id'))
    for chunk in response.parse():
        if chunk.usage is not None:
            usage = chunk.usage
        for choice in chunk.choices:
            piece = choice.delta.content or ''
            text += piece
            print(piece, end='', flush=True)
            if choice.finish_reason is not None:
                finish = choice.finish_reason
print('\nfinish_reason:', finish)
assert text.strip() and finish == 'stop', 'Incomplete text response'
assert usage is not None, 'No final usage received'
print('usage:', usage.model_dump())

Expected: incremental Hello streaming., then finish_reason: stop and usage containing input and output token counts. Exit code: 0.

Read Chat Completions in this order:

  1. Collect choices[].delta.content. Role, thinking, or empty deltas may contain no answer text; do not display them as answer text.
  2. Record finish_reason. This example ends with stop; tool_calls requires the tool-calling flow.
  3. With stream_options.include_usage=true, continue reading the final usage chunk. Its choices may be empty; do not access choices[0] unconditionally.
  4. The HTTP stream ends with data: [DONE]. The SDK handles this marker and finishes iteration.

Do not break on finish_reason: usage may arrive afterward. A disconnected stream does not establish completion. Without terminal status or final usage, treat text as partial and usage as unknown.

3. Inspect the events with cURL

Run in the same terminal:

curl --fail-with-body --no-buffer --silent --show-error --include \
  "${PRAXIS_API_BASE:-https://api.deepraxis.com}/openai/v1/chat/completions" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"deepseek-flash","messages":[{"role":"user","content":"Reply with exactly: Hello streaming."}],"max_tokens":256,"stream":true,"stream_options":{"include_usage":true}}'

Expected: HTTP 200 and text/event-stream. Text deltas precede finish_reason: "stop", a usage chunk, and finally data: [DONE]. cURL exits 0. --no-buffer prevents display buffering. Exit code 0 alone does not prove answer completion; inspect the terminal status too.

4. Use Anthropic Messages

Messages sends message_start, content block content_block_start / content_block_delta / content_block_stop, then message_delta and message_stop. A ping may occur between events. Do not parse these as Chat Completions choices. Input usage appears in the starting message; the SDK aggregates later updates. get_final_message() returns the complete result.

Save as streaming-anthropic.py. Run uv run -q --no-project --with openai --with anthropic python streaming-anthropic.py.

import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
with client.messages.stream(model='claude-opus-5', max_tokens=256,
    messages=[{'role':'user','content':'Reply with exactly: Hello streaming.'}]) as stream:
    print('X-Request-Id:', stream.response.headers.get('x-request-id'))
    for text in stream.text_stream:
        print(text, end='', flush=True)
    message = stream.get_final_message()
assert message.stop_reason == 'end_turn'
assert any(block.type == 'text' and block.text.strip() for block in message.content)
print('\nusage:', message.usage.model_dump())

Expected: the same text and final usage, with stop_reason=end_turn. Exit code: 0.

Observed failure and fix

Using text-embedding-3-small in this chat request returned HTTP 404:

openai.chat.completions is not offered for text-embedding-3-small

The model does not offer this chat endpoint. Waiting for more events cannot resolve it. Using the verified deepseek-flash example returned text, terminal status, and final usage successfully.

Protocol details:OpenAI · Anthropic。

API reference → · Cookbook →

Tool calling

The model returns a tool name and arguments. Your application executes the tool and sends its result back. A tool call is not the final answer.

This example reads local demonstration data: Paris is always 22°C. It does not call a weather service. GET /openai/v1/models lists tools and tool_choice in deepseek-flash's supported_parameters. The two-turn flow below was verified on 2026-10-12.

1. Prepare your terminal

Use a Praxis cloud API key in PRAXIS_API_KEY; see API keys and authentication. Do not put the key in source code. Install Python and uv; TypeScript examples use Bun.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. The examples default to https://api.deepraxis.com. To use another environment, set PRAXIS_API_BASE to its API origin, without a trailing slash or API path.

Save each Python block using its indicated filename. Run it with uv run -q --no-project --with openai --with anthropic python <filename>. Each block is independent.

2. Define a tool and complete both turns

Save as tool-calling.py. Run uv run -q --no-project --with openai --with anthropic python tool-calling.py.

Define the name, description, and parameters of get_temperature, then request a call. Validate arguments, run the local lookup, and return the result with its original tool_call_id. Finally, request a text answer. Both turns set reasoning_effort="none".

import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
tools = [{'type':'function','function':{
    'name':'get_temperature', 'description':'Read the demo temperature for Paris.',
    'parameters':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
        'required':['city'],'additionalProperties':False}}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
first = client.chat.completions.create(model='deepseek-flash', messages=messages,
    tools=tools, reasoning_effort='none', tool_choice={'type':'function','function':{'name':'get_temperature'}}, max_tokens=1024)
print('X-Request-Id:', first._request_id)
message = first.choices[0].message
assert first.choices[0].finish_reason == 'tool_calls' and message.tool_calls
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
    assert call.function.name == 'get_temperature'
    args = json.loads(call.function.arguments)
    assert args == {'city':'Paris'}, args
    result = {'city':args['city'], 'temperature_c':22}
    messages.append({'role':'tool','tool_call_id':call.id,'content':json.dumps(result)})
second = client.chat.completions.create(model='deepseek-flash', messages=messages,
    tools=tools, reasoning_effort='none', tool_choice='none', max_tokens=1024)
print('X-Request-Id:', second._request_id)
answer = second.choices[0].message.content or ''
assert second.choices[0].finish_reason == 'stop' and '22' in answer, answer
print(answer)

Expected: the first turn returns finish_reason=tool_calls and arguments {"city":"Paris"}. The second answer contains 22 with finish_reason=stop. Exit code: 0.

Preserve the complete first assistant message. Return one role="tool" message per call. Do not invent call IDs or send results as ordinary user text. Arguments are JSON strings: parse and validate them before execution. Never execute arbitrary model-provided function names. This example ends the tool phase with tool_choice="none". Applications using auto should handle repeated tool turns with a maximum turn limit.

3. Run the same flow with TypeScript

Save as tool-calling.ts. Run bun tool-calling.ts. It uses built-in fetch; no SDK installation is needed.

import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.com';
async function chat(body: object) {
  const response = await fetch(`${base}/openai/v1/chat/completions`, {
    method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
    body: JSON.stringify({model: 'deepseek-flash', max_tokens: 1024, reasoning_effort: 'none', ...body}),
  });
  console.log('X-Request-Id:', response.headers.get('x-request-id'));
  const data = await response.json();
  assert(response.ok, JSON.stringify(data));
  return data;
}
const tools = [{type: 'function', function: {name: 'get_temperature',
  description: 'Read the demo temperature for Paris.',
  parameters: {type: 'object', properties: {city: {type: 'string', enum: ['Paris']}},
    required: ['city'], additionalProperties: false}}}];
const messages: any[] = [{role: 'user', content: 'Use get_temperature for Paris, then tell me the temperature in Celsius.'}];
const first = await chat({messages, tools, tool_choice: {type: 'function', function: {name: 'get_temperature'}}});
const choice = first.choices[0];
assert.equal(choice.finish_reason, 'tool_calls');
assert(choice.message.tool_calls?.length);
messages.push(choice.message);
for (const call of choice.message.tool_calls) {
  assert.equal(call.function.name, 'get_temperature');
  const args = JSON.parse(call.function.arguments);
  assert.deepEqual(args, {city: 'Paris'});
  messages.push({role: 'tool', tool_call_id: call.id,
    content: JSON.stringify({city: args.city, temperature_c: 22})});
}
const second = await chat({messages, tools, tool_choice: 'none'});
assert.equal(second.choices[0].finish_reason, 'stop');
assert(second.choices[0].message.content.includes('22'));
console.log(second.choices[0].message.content);

Expected: the same tool call and result submission, followed by an answer containing 22. Exit code: 0.

4. Use Anthropic Messages

Messages defines parameters with input_schema. It returns tool_use content blocks and stop_reason="tool_use". Preserve the complete assistant content, then send tool_result in the next role="user" message. Match the original call using tool_use_id. This example was verified with claude-opus-5.

Save as tool-calling-anthropic.py. Run uv run -q --no-project --with openai --with anthropic python tool-calling-anthropic.py.

import json
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
tools = [{'name':'get_temperature','description':'Read the demo temperature for Paris.',
    'input_schema':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
        'required':['city'],'additionalProperties':False}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    tools=tools, tool_choice={'type':'tool','name':'get_temperature'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
first = raw.parse()
assert first.stop_reason == 'tool_use'
messages.append({'role':'assistant','content':[block.model_dump(exclude_none=True) for block in first.content]})
results = []
for block in first.content:
    if block.type == 'tool_use':
        assert block.name == 'get_temperature' and block.input == {'city':'Paris'}
        results.append({'type':'tool_result','tool_use_id':block.id,
            'content':json.dumps({'city':'Paris','temperature_c':22})})
assert results
messages.append({'role':'user','content':results})
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    tools=tools, tool_choice={'type':'none'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
second = raw.parse()
answer = ''.join(block.text for block in second.content if block.type == 'text')
assert second.stop_reason == 'end_turn' and '22' in answer, answer
print(answer)

Expected: a tool request, then an answer containing 22 with stop_reason=end_turn. Exit code: 0.

Observed failures and fixes

Forcing a named function without disabling thinking returned HTTP 400 from deepseek-flash:

tool_choice a named function is not supported while thinking is enabled on this model. Set reasoning effort to "none" or use tool_choice "auto".

The verified fix used here is explicit reasoning_effort="none". Setting it only on the first turn also produced HTTP 400 on the second turn:

The selected thinking setting is not supported on this model route.

Keeping reasoning_effort="none" on both turns passed. Preserve this parameter when retrying.

Protocol details:OpenAI · Anthropic。

API reference → · Cookbook →

Structured outputs

Structured outputs constrain an answer to a JSON structure. Your application must still check completion, parse JSON, and validate business fields.

Choose a supported model

Check supported_parameters in GET /openai/v1/models: candidates should list response_format and structured_outputs. On 2026-10-12, Chat Completions json_schema with strict passed for gpt-5.6-sol. Messages output_config passed for claude-opus-5. The examples use these models respectively.

Although deepseek-flash lists these parameters, its json_schema request returned 400 during testing. It is therefore not used here for schema enforcement. Catalog declarations do not replace response validation. json_object (JSON mode) requests JSON formatting, not schema enforcement. Use json_schema when fields and types must be constrained.

1. Prepare your terminal

Use a Praxis cloud API key in PRAXIS_API_KEY; see API keys and authentication. Do not put the key in source code. Install Python and uv; TypeScript examples use Bun.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. The examples default to https://api.deepraxis.com. To use another environment, set PRAXIS_API_BASE to its API origin, without a trailing slash or API path.

Save each Python block using its indicated filename. Run it with uv run -q --no-project --with openai --with anthropic python <filename>. Each block is independent.

2. Use json_schema and strict

Save as structured-output.py. Run uv run -q --no-project --with openai --with anthropic python structured-output.py.

Set name, strict: true, and schema inside response_format.json_schema. This schema makes every property required and rejects extra fields with additionalProperties: false.

import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
schema = {'type':'object', 'properties':{
    'city':{'type':'string'}, 'temperature':{'type':'integer'}},
    'required':['city','temperature'], 'additionalProperties':False}
response = client.chat.completions.create(model='gpt-5.6-sol', max_tokens=1024,
    messages=[{'role':'user','content':'Return JSON for Paris with temperature 22.'}],
    response_format={'type':'json_schema','json_schema':{
        'name':'weather','strict':True,'schema':schema}})
print('X-Request-Id:', response._request_id)
choice = response.choices[0]
assert choice.finish_reason == 'stop' and not choice.message.refusal
value = json.loads(choice.message.content)
assert set(value) == {'city','temperature'}
assert isinstance(value['city'], str) and type(value['temperature']) is int
assert value == {'city':'Paris','temperature':22}, value
print(value)

Expected: {'city': 'Paris', 'temperature': 22}. Exit code: 0. The code does not strip Markdown fences. Only directly parseable JSON with the expected fields and types passes.

Check finish_reason and refusal before parsing. Do not treat truncated or refused answers as valid business results. For streaming, collect the complete text before parsing.

3. Validate the same result with TypeScript

Save as structured-output.ts. Run bun structured-output.ts; no SDK installation is needed.

import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.com';
const schema = {type: 'object', properties: {
  city: {type: 'string'}, temperature: {type: 'integer'}},
  required: ['city', 'temperature'], additionalProperties: false};
const response = await fetch(`${base}/openai/v1/chat/completions`, {
  method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
  body: JSON.stringify({model: 'gpt-5.6-sol', max_tokens: 1024,
    messages: [{role: 'user', content: 'Return JSON for Paris with temperature 22.'}],
    response_format: {type: 'json_schema', json_schema: {name: 'weather', strict: true, schema}}}),
});
console.log('X-Request-Id:', response.headers.get('x-request-id'));
const data = await response.json();
assert(response.ok, JSON.stringify(data));
const choice = data.choices[0];
assert.equal(choice.finish_reason, 'stop');
assert(!choice.message.refusal);
const value = JSON.parse(choice.message.content);
assert.deepEqual(value, {city: 'Paris', temperature: 22});
console.log(value);

Expected: city=Paris, integer temperature=22, and no extra fields. Exit code: 0.

4. Use output_config with Messages

Messages places the schema inside output_config.format, with type=json_schema. Do not copy the Chat Completions response_format or json_schema.name wrapper. Do not add OpenAI's strict field here.

Save as structured-output-anthropic.py. Run uv run -q --no-project --with openai --with anthropic python structured-output-anthropic.py.

import json
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
schema = {'type':'object', 'properties':{
    'city':{'type':'string'}, 'temperature':{'type':'integer'}},
    'required':['city','temperature'], 'additionalProperties':False}
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    messages=[{'role':'user','content':'Return JSON for Paris with temperature 22.'}],
    output_config={'format':{'type':'json_schema','schema':schema}})
print('X-Request-Id:', raw.headers.get('x-request-id'))
message = raw.parse()
assert message.stop_reason == 'end_turn'
value = json.loads(''.join(block.text for block in message.content if block.type == 'text'))
assert set(value) == {'city','temperature'}
assert isinstance(value['city'], str) and type(value['temperature']) is int
assert value == {'city':'Paris','temperature':22}, value
print(value)

Expected: stop_reason=end_turn, successful JSON validation, and the same object. Exit code: 0.

Observed failure and fix

The schema request to deepseek-flash returned HTTP 400. The fixed portion of the error message was:

This response_format type is unavailable now

The message also includes that request's identifier. Switching to the verified gpt-5.6-sol example passed. HTTP 200, JSON inside Markdown, or parseable ordinary JSON alone does not establish schema enforcement. Keep the complete validation shown above.

Protocol details:OpenAI · Anthropic。

API reference → · Cookbook →

Adjust reasoning effort

The same “low” setting uses different fields in Chat, Messages, and Google. Choose a supported protocol and level, then inspect usage. Effort is a hint; actual consumption comes from usage.

Choose a protocol and level

Open Console → Models, select a model, and find “协议与思考档位” (Protocols and reasoning levels). Expected: supported APIs and thinking options are visible. If disabling is not offered, do not send a setting that disables thinking.

Confirm protocols with supported_apis from GET /model/v1/models. For programmatic Chat checks, use reasoning.supported_efforts and reasoning.mandatory from GET /openai/v1/models?api=chat. On October 13, 2026, the Chat catalog marked glm-5.3, glm-5.3-flash, and kimi-k3 as mandatory=true: thinking cannot be disabled. Recheck the catalog before calling. Not every model supports every level. The usage fields below were returned by the models used here; other models may omit them.

APIThinking controlUsage read in this guide
Chat Completionsreasoning_effort; also accepts a reasoning objectusage.completion_tokens_details.reasoning_tokens
Anthropic Messagesthinking selects the mode; output_config.effort selects the levelusage.output_tokens_details.thinking_tokens
Google generateContentthinkingBudget or thinkingLevel inside generationConfig.thinkingConfigusageMetadata.thoughtsTokenCount

Chat levels range from none to max, subject to model support. none disables thinking; it does not mean “low.” Messages uses enabled, adaptive, and disabled for budgeted, adaptive, and disabled thinking. The effort field is not thinking.effort. Google budget and level controls depend on the model; do not send both together.

See the field definitions for Chat, Messages, and Google.

1. Prepare your terminal

Use Python 3.10+ and uv. Put your Praxis cloud API Key in PRAXIS_API_KEY; do not put it in source code or logs.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. Exit code 1 means the variable is missing. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Chat: select low and read usage

deepseek-flash supports Chat and low.

Save as reasoning-chat.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python reasoning-chat.py.

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
response = client.chat.completions.create(
    model='deepseek-flash', reasoning_effort='low', max_tokens=2048,
    messages=[{'role': 'user', 'content': 'What is 17 times 23? Answer briefly.'}])
assert response.choices[0].finish_reason == 'stop'
assert '391' in (response.choices[0].message.content or '')
details = response.usage.completion_tokens_details
assert details is not None and details.reasoning_tokens is not None, 'Reasoning usage was not reported'
print('X-Request-Id:', response._request_id)
print('answer:', response.choices[0].message.content)
print('reasoning_tokens:', details.reasoning_tokens)

Expected: the answer contains 391, the finish reason is stop, and the script prints actual reasoning_tokens. Exit code 0. Counts vary between calls.

3. Messages: select low and read usage

This example uses claude-opus-5 because it returned thinking token details during verification. The Messages response from deepseek-flash omitted them. Missing is not zero; output_tokens is not a substitute.

Save as reasoning-messages.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python reasoning-messages.py.

import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
raw = client.messages.with_raw_response.create(
    model='claude-opus-5', max_tokens=2048,
    thinking={'type': 'adaptive'}, output_config={'effort': 'low'},
    messages=[{'role': 'user', 'content': 'What is 17 times 23? Answer briefly.'}])
response = raw.parse()
answer = ''.join(block.text for block in response.content if block.type == 'text')
assert response.stop_reason == 'end_turn' and '391' in answer
usage = response.usage.model_dump()
details = usage.get('output_tokens_details') or {}
assert details.get('thinking_tokens') is not None, 'Thinking usage was not reported'
print('X-Request-Id:', raw.headers.get('x-request-id'))
print('answer:', answer)
print('thinking_tokens:', details['thinking_tokens'])

Expected: the answer contains 391, the stop reason is end_turn, and the script prints actual thinking_tokens. Exit code 0.

4. Google: select a budget or level by model

This calls two models: gemini-2.5-flash with a budget, then gemini-3.1-flash-lite-preview with low effort. HTTP exposes Google's field names directly.

Save as reasoning-google.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python reasoning-google.py.

import json
import os
import httpx

base = os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com')
with httpx.Client(timeout=120) as client:
    for model, thinking in [
        ('gemini-2.5-flash', {'thinkingBudget': 1024}),
        ('gemini-3.1-flash-lite-preview', {'thinkingLevel': 'low'}),
    ]:
        response = client.post(base + '/google/v1beta/models/' + model + ':generateContent',
            headers={'Authorization': 'Bearer ' + os.environ['PRAXIS_API_KEY']},
            json={'contents': [{'role': 'user', 'parts': [{'text': 'What is 17 times 23? Answer briefly.'}]}],
                  'generationConfig': {'maxOutputTokens': 2048, 'thinkingConfig': thinking}})
        print('X-Request-Id:', response.headers.get('x-request-id'))
        response.raise_for_status()
        data = response.json()
        candidate = data['candidates'][0]
        answer = ''.join(part.get('text', '') for part in candidate['content']['parts'])
        assert candidate['finishReason'] == 'STOP' and '391' in answer
        assert 'thoughtsTokenCount' in data['usageMetadata'], 'Thinking usage was not reported'
        print(model, answer, json.dumps(data['usageMetadata']))

Expected: both answers contain 391, both finish reasons are STOP, and each prints thoughtsTokenCount. Exit code 0.

Compare results

Keep the model and question constant. Sample multiple calls and compare actual thinking tokens, complete answers, latency, and quality. A budget is not a universal hard cap across models. “Low” does not guarantee fewer tokens than “high” on every call or faster responses. Do not infer a monotonic relationship from one response. Preserve missing usage as “not reported,” never zero.

Observed failure and recovery

Setting reasoning_effort="none" on glm-5.3 returned HTTP 400 with code thinking_disable_unsupported:

glm-5.3 cannot disable thinking; omit the thinking setting or choose a level.

Omit the disabling setting or choose a supported level, then retry. Changing to another protocol's disabling field does not remove this restriction.

API reference → · Cookbook →

Responses API

Use Responses only with a model that declares openai.responses. Supporting Chat Completions does not imply Responses support. Query GET /openai/v1/models?api=responses; on October 13, 2026 it returned deepseek-flash and glm-5.3-flash.

1. Prepare your terminal

Use Python 3.10+ and uv. Put your Praxis cloud API Key in PRAXIS_API_KEY; do not put it in source code or logs.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. Exit code 1 means the variable is missing. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Continue from the previous response

The platform expands the conversation history referenced by previous_response_id. The second call sends only the new question and the first response ID. Both calls use the same Key and model; the script checks that the second answer recalls a code absent from its new input.

Save as responses.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python responses.py.

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
models = client.models.list(extra_query={'api': 'responses'})
print('X-Request-Id:', models._request_id)
assert 'deepseek-flash' in [model.id for model in models]
first = client.responses.create(model='deepseek-flash',
    input='Remember this code: LIME-742. Reply OK.',
    reasoning={'effort': 'none'}, max_output_tokens=512)
print('X-Request-Id:', first._request_id)
assert first.status == 'completed' and first.id
second = client.responses.create(model='deepseek-flash',
    previous_response_id=first.id, input='What was the code? Reply with only the code.',
    reasoning={'effort': 'none'}, max_output_tokens=512)
print('X-Request-Id:', second._request_id)
assert second.status == 'completed' and 'LIME-742' in second.output_text
assert second.usage is not None
print('answer:', second.output_text)
print('usage:', second.usage.model_dump())

Expected: both responses have status completed; the second answer contains LIME-742 and prints usage. Exit code 0. Reusing history is not free input: inspect usage for each turn.

Limits and observed failures

POST /openai/v1/responses/compact and POST /openai/v1/responses/input_tokens currently return HTTP 501 with not_implemented. Do not depend on them for conversation compaction or token preflight. Manage conversation length in your application and read actual usage after completion.

A request using gpt-5.6-sol returned HTTP 404 with model_protocol_not_offered. Choose a model from the Responses-filtered catalog. If the catalog assertion fails, recheck that catalog before retrying. Response IDs from another gateway do not move with your base URL; rebuild the necessary history.

See create a response for parameters and the OpenAI API reference for each operation's support status.

API reference → · Cookbook →

Read images, PDFs, and text files

Choose a model that supports both your protocol and input type. Check supported_apis, architecture.input_modalities, and x_praxis.input_modalities in GET /model/v1/models. native means native input, converted means conversion, and unsupported means unavailable. An image-capable model is not automatically an audio- or video-capable model.

This example uses deepseek-flash for an inline image and qwen3.8-flash for PDF and text file_id references. The latter declares native PDF input. File upload and lifecycle details belong to the file service guide.

1. Prepare your terminal

Use Python 3.10+ and uv. Put your Praxis cloud API Key in PRAXIS_API_KEY; do not put it in source code or logs.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. Exit code 1 means the variable is missing. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Send known content and verify the answer

The script creates a red PNG, a one-page PDF, and a text file locally. It verifies the color, then uploads both documents with purpose=user_data and asks for their verification code. Each uploaded file is deleted in finally, including when a content check fails. No existing files are required.

Save as multimodal.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python multimodal.py.

import base64
import os
import struct
import zlib
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')

# Build a 128 x 128 red PNG using only the Python standard library.
def chunk(kind, data):
    return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', zlib.crc32(kind + data))
image = (b'\x89PNG\r\n\x1a\n'
    + chunk(b'IHDR', struct.pack('>IIBBBBB', 128, 128, 8, 2, 0, 0, 0))
    + chunk(b'IDAT', zlib.compress((b'\x00' + b'\xff\x00\x00' * 128) * 128))
    + chunk(b'IEND', b''))
response = client.chat.completions.create(model='deepseek-flash', reasoning_effort='none', max_tokens=256,
    messages=[{'role': 'user', 'content': [
        {'type': 'text', 'text': 'Name the main color in this image in English.'},
        {'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,' + base64.b64encode(image).decode()}},
    ]}])
print('X-Request-Id:', response._request_id)
answer = response.choices[0].message.content or ''
assert response.choices[0].finish_reason == 'stop' and 'red' in answer.lower(), answer
print('image:', answer)

# Build a one-page PDF with selectable text.
text = b'The verification code is LIME-742.'
stream = b'BT /F1 18 Tf 50 100 Td (' + text + b') Tj ET'
objects = [b'<< /Type /Catalog /Pages 2 0 R >>',
    b'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
    b'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 200] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
    b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
    b'<< /Length ' + str(len(stream)).encode() + b' >>\nstream\n' + stream + b'\nendstream']
pdf, offsets = b'%PDF-1.4\n', [0]
for index, obj in enumerate(objects, 1):
    offsets.append(len(pdf))
    pdf += f'{index} 0 obj\n'.encode() + obj + b'\nendobj\n'
xref = len(pdf)
pdf += b'xref\n0 6\n0000000000 65535 f \n'
pdf += b''.join(f'{offset:010d} 00000 n \n'.encode() for offset in offsets[1:])
pdf += f'trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n'.encode()

for name, data, mime in [('code.pdf', pdf, 'application/pdf'), ('code.txt', text, 'text/plain')]:
    file = client.files.create(file=(name, data, mime), purpose='user_data')
    print('upload X-Request-Id:', file._request_id)
    try:
        response = client.chat.completions.create(model='qwen3.8-flash', reasoning_effort='none', max_tokens=256,
            messages=[{'role': 'user', 'content': [
                {'type': 'text', 'text': 'What is the verification code in the file? Reply with only the code.'},
                {'type': 'file', 'file': {'file_id': file.id}},
            ]}])
        print('chat X-Request-Id:', response._request_id)
        answer = response.choices[0].message.content or ''
        assert response.choices[0].finish_reason == 'stop' and 'LIME-742' in answer, answer
        print(name, answer)
    finally:
        deleted = client.files.delete(file.id)
        print('delete X-Request-Id:', deleted._request_id)
        assert deleted.deleted

Expected: the image answer contains red; both code.pdf and code.txt return LIME-742; both deletions succeed. Exit code 0. HTTP success alone is insufficient: the content assertions must pass.

Observed failure and recovery

With the same PDF, deepseek-flash returned HTTP 200 but said:

I wasn't able to read the attached PDF

Its catalog entry advertises converted PDF input; that did not establish successful reading in this test. Switching the document calls to native-PDF qwen3.8-flash passed the content checks. If a model cannot read the document, use a verified model or supply the relevant text directly. Do not accept a plausible answer without checking known content. Keep the file available until the request finishes.

For this tested path, file_id accepts PDF and text files, not just images. This does not imply every model supports every file type. Parameters: Chat Completions.

API reference → · Cookbook →

Image generation and editing

Use gpt-image-2 through the OpenAI Images API for both generation and editing. Check GET /openai/v1/models?api=images before calling; the native catalog declares openai.images.generations and openai.images.edits separately.

1. Prepare your terminal

Use Python 3.10+ and uv. Put your Praxis cloud API Key in PRAXIS_API_KEY; do not put it in source code or logs.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. Exit code 1 means the variable is missing. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Generate and edit

Run in a writable directory. This example makes two billable image requests: one generation and one edit. It saves circle.png and circle-edited.png; use a fresh directory to avoid overwriting existing files. Automatic retries are disabled.

Save as images.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python images.py.

import base64
import os
from pathlib import Path
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1',
    timeout=180, max_retries=0)
models = client.models.list(extra_query={'api': 'images'})
print('X-Request-Id:', models._request_id)
assert 'gpt-image-2' in [model.id for model in models]

def save(response, filename):
    print('X-Request-Id:', response._request_id)
    assert len(response.data) == 1 and response.data[0].b64_json
    image = base64.b64decode(response.data[0].b64_json, validate=True)
    assert image.startswith(b'\x89PNG\r\n\x1a\n'), 'Expected PNG output'
    Path(filename).write_bytes(image)
    assert response.usage is not None, 'Usage was not reported'
    print(filename, len(image), 'bytes; usage:', response.usage.model_dump())

save(client.images.generate(model='gpt-image-2',
    prompt='A single blue circle centered on a plain white background.',
    n=1, size='1024x1024', quality='low'), 'circle.png')
with open('circle.png', 'rb') as image:
    save(client.images.edit(model='gpt-image-2', image=image,
        prompt='Change the blue circle to a red circle. Keep the white background.',
        n=1, size='1024x1024', quality='low'), 'circle-edited.png')

Expected: both files are nonempty PNGs; each response contains one image and token usage. Exit code 0. Open the files: the first should show a blue circle, the second a red circle on white. The signature check proves file format, not visual accuracy.

3. Read image costs

gpt-image-2 is billed by tokens, not a fixed price per image. Text input, image input, and image output have separate rates. Editing includes input image tokens. Read the returned usage and the current prices in the model catalog; see usage and billing for account totals.

If the catalog assertion fails, stop before generating and choose a currently available image model. If an output assertion fails, retain the request ID and response error; do not repeatedly regenerate to diagnose a file problem.

Operation details: generate and edit.

API reference → · Cookbook →

Files in conversations

See the file service for uploads, expiry, downloads and deletion.

Use file_id to reference images, text files or PDFs uploaded with purpose=user_data. Pass {"type":"file","file":{"file_id":"ID returned by upload"}} in message content. Acceptance does not guarantee that the selected model can read the content. Files uploaded for vision or assistants are not used as image input here.

Verified on Develop on 2026-10-13: qwen3.8-flash extracted the requested phrase from both text and PDF files. For the same PDF, deepseek-flash returned HTTP 200 but reported conversion failure (provider_rejected) and stated that the file was not read. Switch to a verified model or place extracted text directly in the message. HTTP 200 alone does not prove the file was read.

Image input still requires an image-capable model; this example uses qwen3-vl-plus.

Install openai and set PRAXIS_API_KEY. For Develop, set PRAXIS_API_BASE=https://api-develop.deepraxis.ai. The example uploads a tiny PNG, asks for a description and deletes the file.

import base64
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url=os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com") + "/openai/v1",
)
image = base64.b64decode(
    "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABWklEQVR4nO3OQQ0AMBAEofVv+iqDxzRBALvtg/wgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwg7gEgaMOyrMtNTwAAAABJRU5ErkJggg=="
)
file = client.files.create(file=("pixel.png", image, "image/png"), purpose="user_data")
try:
    response = client.chat.completions.create(
        model="qwen3-vl-plus",
        max_tokens=80,
        messages=[{"role": "user", "content": [
            {"type": "text", "text": "Describe this image in one sentence."},
            {"type": "file", "file": {"file_id": file.id}},
        ]}],
    )
    print(response.choices[0].message.content)
finally:
    client.files.delete(file.id)

Expected: a brief image description; the file is deleted when the request finishes.

Plugins

Plugins process requests or responses around a model call so models can use converted inputs. They do not change a model's native capabilities. file-parser is the first plugin: when needed, it converts documents or images to text before sending them to your chosen model.

The same plugins parameter works on create requests to /model/v1/chat/completions, /model/v1/messages, and /model/v1/responses, with or without streaming. The platform consumes it; it is not forwarded as an upstream model parameter.

Configuration and defaults

Add a plugins array to the request body. Each entry identifies a plugin by id:

{
  "plugins": [
    { "id": "file-parser", "enabled": true }
  ]
}

Omitting plugins or sending plugins: [] inherits the defaults; neither disables plugins. File parsing is enabled by default. PDFs and images pass through when the model supports them natively and they meet its limits. Otherwise, text PDFs and Office documents use local text extraction, scanned pages use OCR, and images use OCR plus a visual description. Extracted text consumes the main model's context window.

To disable conversion:

{
  "plugins": [
    { "id": "file-parser", "enabled": false }
  ]
}

Native inputs remain available when conversion is disabled; unsupported inputs are rejected. Plugin IDs, fields, and values are validated, so unknown configuration is not silently ignored.

PDF processing

pdf.engineBehavior
auto (default mode)Prefer native input. When conversion is needed, extract text locally and use OCR for scanned pages or pages with insufficient text.
nativeRequire native PDF input. Reject unsupported inputs or inputs beyond native limits, without falling back to text conversion.
ocrForce text conversion even when the model supports native PDFs. Pages with usable text still use local extraction; only pages needing recognition invoke OCR.

Omitting pdf.engine inherits the effective defaults. Without a tenant or platform override, this selects automatic mode. The request parameter currently accepts only explicit native or ocr; the string "auto" cannot override an existing default. You can set the tenant PDF default to automatic in the console.

{
  "plugins": [
    { "id": "file-parser", "pdf": { "engine": "ocr" } }
  ]
}

pdf.engine only affects PDFs. Set "image": { "describe": false } to disable conversion of non-native image inputs; this leaves native vision and document processing available.

Configuration precedence

From highest to lowest: platform policy > locked tenant settings > request > tenant defaults > plugin defaults.

Platform policy cannot be overridden. A request that changes a locked tenant value returns PLUGIN_SETTING_LOCKED. Unlocked settings can be overridden for one request without changing tenant defaults. Platform limits, including file size and page budgets, cannot be changed through request parameters.

Billing

  • Local extraction generates no additional model usage. The resulting text is still billed as input to the main model.
  • OCR is measured by pages actually processed. Image descriptions are measured by the description model's input, cached input, and output usage. Prices follow the models used.
  • OCR, image descriptions, and the main model have separate usage records. A main-model failure does not erase conversion usage already incurred.
  • A parsing cache hit makes no new conversion child call and adds no usage for that work. The next main-model call still incurs its actual input and output usage.

Annotations and replay

Responses after conversion carry file annotations. Preserve the complete object, including its hash and parsed content:

{
  "type": "file",
  "file": {
    "hash": "<preserve the returned hash>",
    "name": "notes.pdf",
    "content": [{ "type": "text", "text": "Extracted text" }]
  }
}
APINon-streaming locationStreaming location
Chat Completionschoices[].message.annotationschoices[].delta.annotations, once per choice
Anthropic MessagesTop-level message annotationsmessage_start.message.annotations
Responsesoutput_text.annotations inside a message's content[] in output[]Corresponding text snapshots in response.content_part.done, response.output_item.done, and terminal response events

Existing citation annotations and their indices are preserved. Responses adds file annotations to completed snapshots, leaving unfinished snapshots unchanged. The Messages top-level field and Responses type: "file" annotation are platform extensions; do not turn file annotations into Anthropic content blocks. Only actual conversion produces these annotations; native passthrough does not. If Responses returns only tool calls, refusal blocks, or empty output, the platform does not fabricate a text message to carry annotations.

On the next turn, keep the original user input and previous assistant content, including annotations:

// Chat Completions
messages.push(response.choices[0].message);

// Anthropic Messages: preserve the top-level extension
messages.push({
  role: "assistant",
  content: response.content,
  annotations: response.annotations,
});

// Responses: preserve output_text annotations
input.push(...response.output);

Then append the new user question. Streaming clients should preserve annotations in the accumulated response. If you assemble messages yourself, collect annotations from the events listed above. If an SDK's types omit extension fields, use its extra-field mechanism or read the raw JSON.

When the returned content matches the server's parsed artifact, original file, configuration, and model capabilities, parsing is skipped. Modified content, changed configuration, or unavailable cache entries can prevent reuse; client annotations alone do not prove that parsing occurred. If you no longer include the original file, annotation text still becomes ordinary conversation context. Sending only previous_response_id is not equivalent to explicitly returning file annotations.

Discovering input support

GET /model/v1/models exposes OpenRouter-shaped architecture.input_modalities containing only native modalities. Native PDF support appears as file; enabling a plugin never adds it to that array.

The extension x_praxis.input_modalities reports native, converted, or unsupported by input type:

{
  "architecture": { "input_modalities": ["text"] },
  "x_praxis": {
    "input_modalities": {
      "text": "native",
      "image": "converted",
      "pdf": "converted",
      "docx": "converted",
      "pptx": "converted",
      "xlsx": "converted",
      "audio": "unsupported",
      "video": "unsupported"
    }
  }
}

This example represents a chat model with native text input only. Support is derived from model capabilities and registered plugins. converted means a conversion path exists; effective tenant settings, request configuration, and limits still apply. Non-conversation calls such as embeddings and OCR do not gain conversation plugins. Audio and video currently have no conversion plugin and require native model support.

Limits

  • Inputs requiring conversion must provide inline content or an authorized file reference supported by the selected API. The plugin does not download arbitrary external URLs.
  • Conversion supports PDF, DOCX, PPTX, XLSX, and images. Office extraction reads document bodies, slide text, and cell values, not embedded objects or images within documents.
  • The limits are 10 MiB per file and 20 MiB per request; up to 100 parsed PDF pages and 20 paid OCR pages, with at most 10 pages per batch. Each request allows up to 4 paid child calls and 2 concurrent calls.
  • Conversion has a 90-second deadline. OCR child calls allow up to 45 seconds and image descriptions up to 30 seconds. During streaming conversion, : PRAXIS PROCESSING comments may arrive before model output; they are keepalives, not generated text.
  • Extraction is bounded to 1,000,000 characters and 1,000 spreadsheet rows per sheet. Model context and input limits still apply.
  • Responses converts newly submitted input only during create. Retrieve, cancel, and other object operations do not rerun conversion. A background: true request requiring conversion returns PLUGIN_BACKGROUND_UNSUPPORTED.
  • Concurrent cache misses can each incur conversion usage. If the main model fails, the current API does not guarantee that parsed file annotations will be returned.

Session affinity

Use the same x-session-id for requests in one conversation. A new conversation gets a new ID. Session affinity helps eligible requests reuse a provider, but does not guarantee a fixed provider or a cache hit.

The ID does not store Chat Completions history for you. Send the messages needed for each turn; use Responses if you want to continue by response ID.

1. Prepare your terminal

Use Python 3.10+ and uv. Put your Praxis cloud API Key in PRAXIS_API_KEY; do not put it in source code or logs.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. Exit code 1 means the variable is missing. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Reuse an ID across two turns

This example generates a UUID and sends it on both requests. It also includes the first exchange in the second request.

Save as session-affinity.py and run uv run -q --no-project --with openai --with anthropic --with google-genai python session-affinity.py.

import os
import uuid
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
session_id = str(uuid.uuid4())
messages = [{'role': 'user', 'content': 'Remember this code: LIME-742. Reply OK.'}]
for turn in range(2):
    response = client.chat.completions.create(model='deepseek-flash',
        messages=messages, reasoning_effort='none', max_tokens=256,
        extra_headers={'x-session-id': session_id})
    print('X-Request-Id:', response._request_id)
    assert response.choices[0].finish_reason == 'stop'
    answer = response.choices[0].message.content or ''
    assert answer
    print('turn', turn + 1, answer)
    if turn == 0:
        messages.append({'role': 'assistant', 'content': answer})
        messages.append({'role': 'user', 'content': 'What was the code? Reply with only the code.'})
    else:
        assert 'LIME-742' in answer, answer

Expected: two completed responses, two request IDs, and LIME-742 in the second answer. Exit code 0. This verifies the requests and conversation history; it does not prove a particular provider or cache hit. Verify cache usage separately with the prompt cache guide.

Observed failure and recovery

An empty body session_id returned HTTP 400: session_id must not be empty. Use a nonempty conversation ID. When the body also contains session_id, it takes precedence over the header; avoid keeping conflicting values in two places. Do not reuse one global ID for unrelated conversations.

Field details: Chat Completions.

API reference → · Cookbook →

Prompt caching and cost

Prompt caching reuses computation for repeated input prefixes, not a previous answer. Verify hits using cached token counts, not matching answers or lower latency.

The OpenAI example uses deepseek-flash without cache-control parameters. On 2026-10-12, its second request reported 25,856 cached tokens from 26,016 input tokens. These are observed values, not a guarantee for every request.

1. Prepare your terminal

Use a Praxis cloud API key in PRAXIS_API_KEY; see API keys and authentication. Do not put the key in source code. Install Python and uv; TypeScript examples use Bun.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. The examples default to https://api.deepraxis.com. To use another environment, set PRAXIS_API_BASE to its API origin, without a trailing slash or API path.

Save each Python block using its indicated filename. Run it with uv run -q --no-project --with openai --with anthropic python <filename>. Each block is independent.

2. Request the same prefix twice

Put stable instructions and reference material first, with the current question last. Keep the model, prefix content, and ordering unchanged. This example generates long demonstration material and makes two billable model calls.

Save as prompt-cache.py. Run uv run -q --no-project --with openai --with anthropic python prompt-cache.py.

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
prefix = '\n'.join(f'Record {i}: Paris is in France. The demo temperature is 22 Celsius.' for i in range(1500))
hits = []
for turn in range(2):
    response = client.chat.completions.create(model='deepseek-flash',
        messages=[{'role':'system','content':prefix},
            {'role':'user','content':'What country is Paris in? Reply with one word.'}],
        reasoning_effort='none', max_tokens=64)
    print('X-Request-Id:', response._request_id)
    assert response.choices[0].finish_reason == 'stop'
    usage = response.usage
    assert usage and usage.prompt_tokens_details is not None
    cached = usage.prompt_tokens_details.cached_tokens
    assert cached is not None
    hits.append(cached)
    print(turn + 1, 'prompt_tokens:', usage.prompt_tokens, 'cached_tokens:', cached)
assert hits[1] > 0, 'No cache hit observed on the second request'

Expected: input and cached token counts for both requests, with cached_tokens > 0 on the second. Exit code: 0. The first request may also hit an existing cache; it need not report zero.

usage.prompt_tokens_details.cached_tokens is a subset of prompt_tokens; do not add it again. Caching reduces eligible input costs, not output costs. Calculate costs using the model's cached-input, ordinary-input, and output rates, rather than a fixed discount. Minimum prefix length and retention depend on the model.

3. Check a hit with TypeScript

Save as prompt-cache.ts. Run bun prompt-cache.ts; no SDK installation is needed.

import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.com';
const prefix = Array.from({length: 1500}, (_, i) =>
  `Record ${i}: Paris is in France. The demo temperature is 22 Celsius.`).join('\n');
const hits: number[] = [];
for (let turn = 0; turn < 2; turn++) {
  const response = await fetch(`${base}/openai/v1/chat/completions`, {
    method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
    body: JSON.stringify({model: 'deepseek-flash', max_tokens: 64, reasoning_effort: 'none',
      messages: [{role: 'system', content: prefix},
        {role: 'user', content: 'What country is Paris in? Reply with one word.'}]}),
  });
  console.log('X-Request-Id:', response.headers.get('x-request-id'));
  const data = await response.json();
  assert(response.ok, JSON.stringify(data));
  assert.equal(data.choices[0].finish_reason, 'stop');
  const cached = data.usage.prompt_tokens_details?.cached_tokens;
  assert.equal(typeof cached, 'number');
  hits.push(cached);
  console.log(turn + 1, 'prompt_tokens:', data.usage.prompt_tokens, 'cached_tokens:', cached);
}
assert(hits[1] > 0, 'No cache hit observed on the second request');

Expected: positive cached_tokens on the second request. Exit code: 0.

4. Observe Anthropic cache_control

Messages accepts cache_control: {type: "ephemeral"} on a stable text block. This example uses claude-opus-5 and reads cache_creation_input_tokens and cache_read_input_tokens after each call. Messages input_tokens excludes cache reads and writes; total input is the sum of all three fields.

During testing, both requests were accepted but reported zero cache reads and writes. This example observes cache status; it does not promise a hit. supported_parameters in GET /openai/v1/models exposes parameter declarations, not proof of individual cache hits. Use the returned usage fields as evidence.

Save as prompt-cache-anthropic.py. Run uv run -q --no-project --with openai --with anthropic python prompt-cache-anthropic.py.

import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
prefix = '\n'.join(f'Record {i}: Paris is in France. The demo temperature is 22 Celsius.' for i in range(1500))
hits = []
for turn in range(2):
    raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=64,
        system=[{'type':'text','text':prefix,'cache_control':{'type':'ephemeral'}}],
        messages=[{'role':'user','content':'What country is Paris in? Reply with one word.'}])
    print('X-Request-Id:', raw.headers.get('x-request-id'))
    message = raw.parse()
    assert message.stop_reason == 'end_turn'
    usage = message.usage
    assert usage.cache_read_input_tokens is not None
    assert usage.cache_creation_input_tokens is not None
    hits.append(usage.cache_read_input_tokens)
    print(turn + 1, 'input:', usage.input_tokens, 'write:', usage.cache_creation_input_tokens,
        'read:', usage.cache_read_input_tokens)
print('HIT' if hits[1] > 0 else 'MISS: no cached input reported')

Expected: two completed requests, read/write statistics, and HIT or MISS: no cached input reported. Exit code: 0. Only a positive read count establishes a hit. Exit code 0 proves only successful requests and usage inspection.

Handle the observed miss

The Messages example returned cache_creation_input_tokens=0 and cache_read_input_tokens=0, without an HTTP error. Request acceptance does not establish caching. To reproduce a verified hit, run the deepseek-flash OpenAI example above and check for positive cached_tokens.

To keep prefixes reusable, use the same model and complete prefix for both calls. Avoid timestamps or changing identifiers inside the prefix. Keep usage and X-Request-Id for comparison. Matching answers are not cache evidence.

Protocol details:OpenAI · Anthropic。

API reference → · Cookbook →

BYOK: use your own provider key

BYOK is coming soon: Console → Models → BYOK is currently disabled and marked “即将推出” (Coming soon). You cannot import your own provider key there yet. For now, use a Praxis cloud API Key and a model in the catalog, following the model quickstart. Do not use a provider key as PRAXIS_API_KEY.

API reference → · Cookbook →

Errors & limits

This page is what to do: error codes, retry policy, actual limits. The why is in Rate limits & errors; your account's quota in Quotas.

Sandbox

快速开始

沙箱(Sandbox)提供安全隔离的代码执行环境——上传代码、运行、获取结果,全程在云端隔离实例中进行。

前提

一个 Praxis API Key。到 控制台 → API Keys 创建。

1. 查看可用环境

沙箱支持多种执行环境(内置环境只读,租户可自建):

curl https://api.deepraxis.ai/sandbox/v1/environments \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

2. 创建沙箱实例

通过 API 创建实例,指定环境和初始资源。

3. 执行代码

在沙箱中运行代码并获取输出。

4. 释放实例

使用完后释放,避免持续计费。

下一步

概念

概念一句话
环境(Environment)代码运行的隔离实例,包含预装工具和运行时
实例(Instance)一个正在运行的沙箱环境
生命周期创建 → 使用 → 释放,释放后不可恢复
网络限制部分环境限制出站网络(limited networking)

指南

你想做什么去哪
第一次使用沙箱快速开始
处理错误和限制限制与错误

限制与错误

通用错误码和重试策略见模型的错误与限流——同一套 HTTP 语义适用于所有服务。

沙箱特有的注意事项:

事项说明
实例数上限每个账户空间同时运行的沙箱实例有上限
执行超时单次代码执行有超时限制
网络限制部分环境限制出站网络(limited networking)
包安装失败会终止会话,错误码 ENVIRONMENT_PACKAGE_INSTALL_FAILED

遇到沙箱特有的错误,参考错误码文档并联系我们。

Search

Roadmap

This service is not yet available. This page explains what it is, the problem it solves and what integration will look like.

What it is

(TODO)

How it relates to other services

(TODO)

快速开始

搜索(Search)为 AI 应用提供网络搜索能力——返回结构化结果,可直接用于 RAG 或智能体工具调用。

前提

一个 Praxis API Key。到 控制台 → API Keys 创建。

1. 执行搜索

通过 Search API 执行一次搜索,获取结构化结果。

2. 在智能体中使用

搜索可以作为智能体的工具——智能体自主决定何时搜索、如何整合结果。

3. 构建知识库问答

搜索与模型结合,构建基于网络信息的问答系统。

下一步

概念

概念一句话
查询(Query)你传入的搜索关键词或问题
结果(Result)结构化的搜索返回,包含标题、摘要、链接
RAG检索增强生成——先搜索再让模型基于结果回答

指南

你想做什么去哪
第一次使用搜索快速开始
处理错误和限制限制与错误

限制与错误

通用错误码和重试策略见模型的错误与限流。

搜索特有的注意事项:

事项说明
搜索频率受账户空间速率限制约束
结果数量每次搜索返回的结果条数有上限
内容时效搜索结果反映当前网络内容,不保证时效性

遇到搜索特有的错误,参考错误码文档并联系我们。

Cloud Browser

Cloud Browser is Chrome running in the cloud: connect directly with Playwright or Puppeteer, or watch and take over in Console.

Three ways to connect

Run these examples with Node.js. Set PRAXIS_API_KEY, save the code as an .mjs file, then run it with node. Install the client packages; no local Chrome download is needed.

export PRAXIS_API_KEY="your API Key"
npm install playwright @browserbasehq/sdk

One-step connection

Connect to wss://api-develop.deepraxis.ai/browser/v1/connect to create a browser. Use ?session=<id> to attach to an existing session, or ?name=<name> to attach if that name exists and create otherwise. Names accept 1–128 letters, digits, or . _ : -. Do not send both session and name.

import { chromium } from "playwright";

const headers = { Authorization: `Bearer ${process.env.PRAXIS_API_KEY}` };
const browser = await chromium.connectOverCDP(
  "wss://api-develop.deepraxis.ai/browser/v1/connect",
  { headers },
);
const cdp = await browser.newBrowserCDPSession();
const session = await cdp.send("Praxis.getSession");
console.log(session.sessionId, session.liveViewUrl, session.consoleUrl);
try {
  const page = await browser.contexts()[0].newPage();
  await page.goto("https://api-develop.deepraxis.ai/meta/v1/health");
  console.log(await page.textContent("body"));
} finally {
  await browser.close();
  // Release immediately when finished to stop billing.
  const closed = await fetch(
    `https://api-develop.deepraxis.ai/browser/v1/sessions/${session.sessionId}`,
    { method: "POST", headers: { ...headers, "Content-Type": "application/json" },
      body: JSON.stringify({ status: "REQUEST_RELEASE" }) },
  );
  if (!closed.ok) throw new Error(await closed.text());
}

When creating a session, you may also pass context, persist, timeout (seconds), idle (milliseconds), labels and viewport. Encode the last two with JSON.stringify and URLSearchParams. Creation options are ignored when attaching to an existing session. A returned connectUrl carries credentials, so connecting needs no extra API key. Keep this URL private.

Official Browserbase SDK

Use your Praxis API key and set the official SDK's baseURL to https://api-develop.deepraxis.ai/browserbase. The SDK sends X-BB-API-Key automatically. You do not need a Browserbase project; projectId can be omitted.

import Browserbase from "@browserbasehq/sdk";
import { chromium } from "playwright";

const bb = new Browserbase({
  apiKey: process.env.PRAXIS_API_KEY,
  baseURL: "https://api-develop.deepraxis.ai/browserbase",
});
const session = await bb.sessions.create({ timeout: 300, keepAlive: true });
try {
  const browser = await chromium.connectOverCDP(session.connectUrl);
  try {
    const page = await browser.contexts()[0].newPage();
    await page.goto("https://api-develop.deepraxis.ai/meta/v1/health");
    console.log(await page.textContent("body"));
  } finally {
    await browser.close();
  }
} finally {
  await bb.sessions.update(session.id, { status: "REQUEST_RELEASE" });
}

See the Browserbase-compatible API for available endpoints. Proxies, stealth options, extensions and Selenium are unavailable. region and solveCaptchas are currently accepted but ignored: they do not select a region or solve CAPTCHAs.

Create with REST, then connect

POST /browser/v1/sessions returns a connectUrl with credentials valid for the session's lifetime. Pass it directly to Playwright or Puppeteer without an additional API key.

import { chromium } from "playwright";

const base = "https://api-develop.deepraxis.ai/browser/v1";
const headers = {
  Authorization: `Bearer ${process.env.PRAXIS_API_KEY}`,
  "Content-Type": "application/json",
};
const response = await fetch(`${base}/sessions`, {
  method: "POST", headers, body: JSON.stringify({ timeout: 300, keepAlive: true }),
});
if (!response.ok) throw new Error(await response.text());
console.log("X-Request-Id:", response.headers.get("X-Request-Id"));
const session = await response.json();
try {
  const browser = await chromium.connectOverCDP(session.connectUrl);
  try {
    const page = await browser.contexts()[0].newPage();
    await page.goto("https://api-develop.deepraxis.ai/meta/v1/health");
    console.log(await page.textContent("body"));
  } finally {
    await browser.close();
  }
} finally {
  const closed = await fetch(`${base}/sessions/${session.id}`, {
    method: "POST", headers, body: JSON.stringify({ status: "REQUEST_RELEASE" }),
  });
  if (!closed.ok) throw new Error(await closed.text());
}

See Cloud Browser API for the full parameter reference. With Puppeteer, connect to the same URL using puppeteer.connect({ browserWSEndpoint: session.connectUrl }).

Watch and take over

Open Browser › Sessions in Console, then choose a session to watch or take over. Send the CDP command Praxis.getSession to get sessionId, liveViewUrl and consoleUrl. To ask a user to log in or confirm an action, request human takeover and complete it when finished.

Session statuses are PENDING, RUNNING, ERROR, TIMED_OUT and COMPLETED. API sessions created with keepAlive=false end after clients disconnect and no viewer remains. Sessions opened through one-step connection or Console are released by default after five minutes without a client or viewer. The maximum lifetime is six hours. There is no pause or hibernation; use a Context to retain login state for next time.

Login state

Create a Context, then pass browserSettings.context when creating a session. persist: true saves cookies and localStorage when the session closes; IndexedDB is not saved. A concurrent writer using the same context may receive 409 CONTEXT_LOCKED.

// Reuse the REST setup above; contextId is the id returned by context creation.
const response = await fetch(`${base}/sessions`, {
  method: "POST", headers,
  body: JSON.stringify({
    timeout: 300,
    browserSettings: { context: { id: contextId, persist: true } },
  }),
});

Pricing and limits

  • $0.12 per browser hour, billed per second with a one-minute minimum per session. Failures caused by the platform are billed only for actual seconds. Billing stops after release.
  • By default, each account can run 10 concurrent sessions, with a maximum of six hours per session.
  • Proxies, CAPTCHA solving, stealth options, region selection and extensions are unavailable. Unsupported proxy, stealth and extension settings return 501; region and solveCaptchas are currently accepted but ignored.
  • Some websites, including Google and Baidu, may identify cloud browsers as bots.
  • Keep the X-Request-Id response header when troubleshooting a request.

Application

An application publishes a site (static files, Worker code, or both) to the platform and gives it a reachable address. Every publish creates a version, and a deployed version becomes the live one.

Where to store data

Use the application's own d1, kv or r2 components for runtime data such as orders and sessions, with business rules enforced by application code. Use Database for structured content, File for files and Knowledge for searchable knowledge: these are shared content stores that applications can read through public APIs, not their private runtime storage.

Runtime capabilities

The tables below list the current runtime capabilities of an application.

Publishing and deployment

CapabilityStatusNotes
Worker → version → deploymentSupportedCreating a version does not put it live; a deployment does. Set deploy: true when creating the version to do both in one step
RollbackSupportedCreate a deployment for an older version again; no files are re-uploaded
Whole-site replacementSupportedEach version's file manifest must list every file of the site; only files whose content changed are uploaded
Gradual rolloutNot supportedA deployment puts exactly one version at 100%
Retained versionsLimitThe latest 100 versions
Script sizeLimit10 MiB compressed
Deployment timeoutLimitA deployment that does not finish within 10 minutes fails

Static assets

CapabilityStatusNotes
Content-addressed deduplicationSupportedIdentical content is stored once; republishing does not upload it again
Paths and sizeLimitUp to 100000 files per version, 25 MiB per file
html_handlingSupportedauto-trailing-slash, force-trailing-slash, drop-trailing-slash or none
not_found_handlingSupportednone, 404-page or single-page-application
_headersSupportedApplies only to static asset responses; up to 100 rules, 2000 characters per line
_redirectsSupportedApplies only to static asset responses; up to 2000 static and 100 dynamic rules, 1000 characters per line

_headers and _redirects do not apply to responses your Worker code generates. For cross-origin access, set the CORS headers in code:

const CORS = {
  "access-control-allow-origin": "*",
  "access-control-allow-methods": "GET, POST, OPTIONS",
  "access-control-allow-headers": "content-type",
};

export default {
  async fetch(request) {
    if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS });
    return Response.json({ ok: true }, { headers: CORS });
  },
};

Outbound requests

CapabilityStatusNotes
Public addressesSupportedfetch() in your code can request any public address
Other applications on the platformSupportedRequesting another application's address returns that application's response

Bindings

TypeStatusNotes
d1SupportedDeclared when creating a version
kv_namespaceSupportedDeclared when creating a version
r2_bucketSupportedDeclared when creating a version
secret_textSupportedDeclared when creating a version; the value is written through the secrets endpoint
plain_textSupportedDeclared when creating a version
jsonSupportedDeclared when creating a version
praxis_tokenSupportedEnable platform API access as the owner organization; no token is injected
praxis_vault_refUnavailableCannot be used at the moment
Other types (such as service, queue, durable_object_namespace, ai)Not supportedCreating a version returns UNSUPPORTED

Secrets

CapabilityStatusNotes
Write a secretSupportedPUT /application/v1/workers/{id}/secrets/{name}. The write derives a new version from the current one and deploys it
Read a secret value backNot supportedValues are write-only; no endpoint returns them

Compatibility flags

CapabilityStatusNotes
compatibility_flagsSupportedPass them when creating a version; they take effect as given
global_fetch_strictly_publicAlways onAdded to every version and cannot be turned off; passing global_fetch_private_origin or no_global_fetch_strictly_public returns 400

Calling platform APIs

Declare this binding in the version you deploy:

{ "type": "praxis_token", "name": "PRAXIS_API_TOKEN" }

Your application can call the Develop API without an API key:

export default {
  async fetch() {
    return fetch("https://api-develop.deepraxis.com/application/v1/workers");
  }
};

The application has the same organization access as an API key of its owner organization. Resources of other organizations return not found. No token or secret is exposed to application code, and env.PRAXIS_API_TOKEN is not created. Without the binding, protected platform endpoints return 401 unless you supply your own valid credential. Other destination hosts are unchanged.

The Praxis SDK uses the same outbound path: omit apiKey and use the Develop API as its baseUrl. No SDK change is required. Existing applications must deploy a version with the binding to enable access. Removing the binding takes effect after the deployment becomes active and the routing cache refreshes (up to 10 seconds).

Files

Files store the original bytes referenced by media libraries, knowledge stores and conversations. Upload a file to receive a file_id, then pass that ID to the service that needs it.

A media library organizes assets and their understanding results. A knowledge store extracts and indexes document content. Uploading a file alone does not add it to either library or make it searchable.

Quickstart · Concepts · Limits and errors

API reference → · Cookbook →

Quickstart

Install the SDK and set PRAXIS_API_KEY to your API key. Save the Python code as files.py.

For Develop, also set PRAXIS_API_BASE=https://api-develop.deepraxis.ai. Without this variable, the example uses the production API. Use a key for the selected environment.

python -m pip install openai
python files.py
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url=os.environ.get("PRAXIS_API_BASE", "https://api.deepraxis.com") + "/openai/v1",
)
file = client.files.create(
    file=("hello.txt", b"Hello, Praxis!\n", "text/plain"),
    purpose="user_data",
    expires_after={"anchor": "created_at", "seconds": 3600},
)
try:
    print("uploaded:", file.id)
    files = client.files.list(limit=100)
    print("listed:", any(item.id == file.id for item in files))
    content = client.files.content(file.id).read()
    assert content == b"Hello, Praxis!\n"
    print("downloaded:", content.decode().strip())
finally:
    deleted = client.files.delete(file.id)
    print("deleted:", deleted.deleted)

Expected: an uploaded ID, listed: True, downloaded: Hello, Praxis! and deleted: True. The example deletes its file even if listing or downloading fails.

API reference → · Cookbook →

Concepts

Purpose

purpose records how you intend to use an uploaded file. Uploads accept three values:

PurposeUse and limitation
user_dataGeneral input files. Required when referencing an image in a conversation.
assistantsFiles intended for assistant or knowledge-store workflows. Uploading does not attach a file to an agent or store. Conversation image references reject this purpose.
visionAccepts only PNG, JPEG, GIF or WebP images (detected from content); other content returns 400 FILE_TYPE_UNSUPPORTED. Conversation image references reject this purpose.

batch, fine-tune and evals are not accepted upload purposes. Anthropic uploads are stored as user_data.

Reuse a file ID

/file/v1/files, /openai/v1/files and /anthropic/v1/files share file IDs for the same account. Metadata and deletion work across these APIs. Download uploaded files through the native or OpenAI API; Anthropic downloads only support session output files.

  • Conversations: reference a user_data image using type: "file" and file.file_id. Do not place the ID in image_url.url. Text and PDF file IDs are not currently accepted as conversation image input. See the runnable image example.
  • Media libraries: import the ID as file_id to create an asset. See media files.
  • Knowledge stores: add file_id to a store to extract and index its content. See knowledge concepts.
  • Agents: attach a file to a session resource using type: "file", file_id and mount_path. Setting purpose=assistants alone does not attach it. See session resources.

Session output metadata can include scope, relative_path and revision. List a session's files with scope_id; Anthropic requires anthropic-beta: managed-agents-2026-04-01 for this filter.

Expiry

Without an expiry setting, an uploaded file persists until deleted. expires_after measures seconds from created_at; the returned expires_at is the expiry timestamp. Expired files cannot be read or reused. See the supported ranges.

Download safety

Downloaded content identified as HTML, SVG, XML or JavaScript is served as an attachment. Responses use X-Content-Type-Options: nosniff. Detection uses content rather than the filename: uploaded HTML or SVG may be identified and returned as plain text. Downloading does not execute that content.

Range downloads

Content downloads accept one Range interval: bytes=0-1023, bytes=1024- (to the end) or bytes=-1024 (the last 1024 bytes). A satisfiable range returns 206 with Content-Range; a range beyond the file size returns 416. Every download response carries Accept-Ranges: bytes, so downloads can resume and audio or video can seek.

API reference → · Cookbook →

Limits and errors

Limits

ItemLimit
Single file100 MiB (104,857,600 bytes); empty files are rejected
File storage quota20 GiB (21,474,836,480 bytes), shared by files in the account
OpenAI or native expires_afteranchor: "created_at", 3,600–2,592,000 seconds (1 hour–30 days)
Native praxis[expires_in_seconds]3,600–7,776,000 seconds (1 hour–90 days)
Anthropic form expires_in_seconds3,600–7,776,000 seconds (1 hour–90 days)

Choose one expiry setting. Anthropic expires_in_seconds belongs in the form body; as a query parameter it returns 400. See upload parameters.

Unknown fields

The compatible APIs reject unknown arguments the way their vendors do instead of ignoring them:

APIAccepted argumentsAnything else
Anthropic uploadForm file, expires_in_seconds; query beta only400 invalid_request_error, e.g. purpose: Extra inputs are not permitted
OpenAI uploadForm file, purpose, expires_after[anchor], expires_after[seconds]; no query parameters400 invalid_request_error, e.g. Unrecognized request argument supplied: foo, with param naming the field
OpenAI listQuery after, order, limit, purposeSame as above

Extensions such as praxis[expires_in_seconds] and scope_id are available only on the native file API.

Content detection accepts PDF, DOCX/XLSX/PPTX, PNG/JPEG/GIF/WebP, supported audio and video, and UTF-8 text. A filename extension does not make arbitrary binary data acceptable.

Errors

ErrorWhat to do
PURPOSE_UNSUPPORTEDSupply user_data, assistants or vision.
FILE_EMPTYUpload a nonempty file.
FILE_TOO_LARGEKeep the file within 100 MiB.
FILE_TYPE_UNSUPPORTEDUse a supported content type; renaming the extension is insufficient. purpose=vision accepts only images, and a corrupt Office document or ZIP archive also returns this error.
VALIDATION_ERRORCheck the filename and expiry parameters.
QUOTA_EXHAUSTEDDelete unneeded files before uploading more.
HTTP 404Check the ID, account and whether the file has expired or been deleted.
File is not an eligible imageFor conversation input, upload a supported image with purpose=user_data.

Oversized requests can be rejected by the edge with HTTP 413 and an HTML body before the file API returns JSON. Handle the HTTP status before decoding an error body.

API reference → · Cookbook →

Media Library

Media Library stores, understands, and retrieves images, videos, audio, and documents for agents. Each library isolates its assets, folders, and searches. Official Cloudinary SDKs work directly.

Quickstart · Concepts · Limits and errors

API reference →

Quickstart

Prepare an API Key. Save the code below as quickstart.mjs and run it with Node.js. It creates a library, uploads an image, searches by tag, and produces delivery URLs.

npm install cloudinary@2
# Set PRAXIS_API_KEY in your environment.
node quickstart.mjs
import { v2 as cloudinary } from "cloudinary";

const base = "https://api.deepraxis.ai";
const key = process.env.PRAXIS_API_KEY;
if (!key) throw new Error("Set PRAXIS_API_KEY");
async function media(path, method = "GET", body) {
  const response = await fetch(base + "/media/v1" + path, {
    method,
    headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error(`Media HTTP ${response.status}`);
  return response.json();
}
const library = await media("/libraries", "POST", { name: "Agent media" });
cloudinary.config({
  cloud_name: library.id,
  oauth_token: key,
  upload_prefix: base + "/cloudinary",
});
const asset = await cloudinary.uploader.upload(
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAABWklEQVR4nO3OQQ0AMBAEofVv+iqDxzRBALvtg/wgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwgzg/i/CDOD+L8IM4P4vwg7gEgaMOyrMtNTwAAAABJRU5ErkJggg==",
  { public_id: "quickstart/pixel", tags: ["docs"] },
);
const found = await cloudinary.search.expression("resource_type:image AND tags:docs").execute();
const delivery = asset.secure_url;
const thumbnail = delivery.replace("/upload/", "/upload/w_160,c_fit/");
const understandings = await media(`/libraries/${library.id}/assets/${asset.asset_id}/understandings`);
const current = await media(`/libraries/${library.id}/assets/${asset.asset_id}`);
const matches = current.semantic === "ready"
  ? await media(`/libraries/${library.id}/search/semantic`, "POST", { query: "a small image", top_k: 5 })
  : null;
console.log({ library_id: library.id, asset_id: asset.asset_id, found, delivery, thumbnail, understandings, matches });

Understanding completes asynchronously. Upload responses carry no processing state; GET the asset to read processing and semantic. Measured scale: about half a minute for an image, about 50 seconds for a 3-second video, and immediately for raw files (no understanding). Initial results may be empty. When matches is null, read the asset again later and search once semantic is ready. Understanding and semantic search are available. Uploading, expression search, and original access do not need to wait for understanding.

The example keeps the library. When finished, set LIBRARY_ID to the returned library_id and delete it:

curl -X DELETE "https://api.deepraxis.ai/media/v1/libraries/${LIBRARY_ID}" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

API reference →

Concepts

A library isolates its assets. Searches only return assets within that library.

Libraries and assets

An asset has a stable asset_id. Its public_id names it in delivery URLs and may contain path segments. The asset_folder organizes assets; moving folders does not change public_id. Listing assets by asset_folder matches that level only, not its subfolders; an empty string is the root. The display_name is a label and does not determine delivery URLs. Audio uses resource_type=video and is_audio=true.

Library names are at most 64 characters and may repeat; libraries are identified by library_id. Updating an asset’s tags or context, or a library’s labels, replaces the whole value: what you send becomes the complete new content, and (or [] for tags) clears it.

A zero-byte upload returns 400 FILE_EMPTY. A file declared or detected as an image whose dimensions cannot be read (for example, a truncated file) returns 400 Invalid image file. Any other file is stored as raw.

Understanding

Understanding produces captions, tags, recognized image text, transcripts, and video-frame content. Segments include start and end times. Records are immutable; newer versions can supersede older records, excluded by default.

Overall states are pending, processing, ready, partial, and failed. Individual kinds use pending, running, ready, failed, unsupported, or skipped. Disabled understanding or unsupported content does not prevent original-file access.

Assets become searchable when semantic is ready. Default hybrid search combines keywords and semantics; semantic-only search is also available. Hits include matching content and millisecond timestamps for audio/video segments. Assets that have never completed understanding are excluded; an asset being reprocessed keeps its previous results in search until the new ones are ready.

Documents and other files (raw)

Raw files such as PDF and text are stored without understanding. Their semantic state is unavailable, they are not part of semantic search, and expression search can match only their metadata (public_id, display_name, tags, context, and so on). To search document text, use Knowledge.

Deletion and recovery

Deletion makes content immediately invisible. Libraries can be restored within 7 days; recovery is unavailable afterward. Individual assets have no restore endpoint.

File Library compared

File Library stores files. Media Library additionally organizes, understands, and searches content. Importing creates a copy. Later changes or deletion of either copy do not affect the other.

API reference →

Limits and errors

Videos larger than 100 MiB do not receive frame/transcript understanding or preview posters. Original storage and access remain available.

LimitValue
Libraries per account100
Assets per library1000000
Maximum image bytes (50 MiB)52428800
Maximum video bytes (2 GiB)2147483648
Maximum raw/document bytes (100 MiB)104857600
Maximum request bytes; use chunks above this100000000
Minimum non-final chunk bytes; equal-sized chunks5242880
Chunk session hours24
Video input bytes for frames and transcript104857600
Maximum expression results500
Default expression results100
Maximum semantic hits50
Maximum semantic candidates100
Maximum visual query image bytes (10 MiB)10485760
Text vectors per asset (image vectors additional)61
Maximum storyboard tiles12
Concurrent understanding jobs per library4
Understanding timeout milliseconds600000
Maximum understanding retries3
Tags per asset1000
Characters per tag255
Custom context bytes1024
IDs per batch1000
Library recovery days7
Library name characters; duplicates allowed64
Minimum bge-m3 semantic cosine similarity0.55

Error codes and retry rules →

Knowledge bases

Knowledge bases split file content into searchable chunks. Find relevant passages using meaning, keywords and file attributes for question answering, policy lookup and model context.

ServiceWhat it stores and finds
FilesUpload, store and download original files; obtain file IDs
MediaOrganize assets, folders and tags, and find media such as images
KnowledgeFind relevant text passages with their source file and content

A source file can belong to multiple knowledge bases. Removing it from a knowledge base does not delete the original. Deleting the original removes it from every knowledge base automatically, and it stops appearing in search results.

  • Quickstart: upload, ingest and search with the official openai Python SDK.
  • Concepts: processing states, chunks, search and restore.
  • Limits and errors: capacity, formats and failures.

Knowledge API → · OpenAI-compatible API →

Quickstart

Run Python on a server and put your develop API Key in PRAXIS_API_KEY. This example connects to develop, creates one knowledge base and two text files, and deletes them when finished.

python -m pip install openai

Save as quickstart.py and run python quickstart.py. Upload the policy file, create the store and wait for ingestion. upload_and_poll combines upload, attachment and polling for the second file. A successful upload still requires checking ingestion status.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["PRAXIS_API_KEY"],
    base_url="https://api.deepraxis.com/openai/v1",
    timeout=120.0,
)
store = None
file_ids = []
try:
    uploaded = client.files.create(
        file=("refund-policy.txt", b"Refunds are available within 30 days of purchase."),
        purpose="assistants",
    )
    file_ids.append(uploaded.id)
    print("uploaded:", uploaded.id)

    store = client.vector_stores.create(
        name="Knowledge quickstart",
        file_ids=[uploaded.id],
    )
    print("store:", store.id)
    first = client.vector_stores.files.poll(
        uploaded.id, vector_store_id=store.id, poll_interval_ms=1000,
    )
    if first.status != "completed":
        raise RuntimeError(f"Ingestion failed: {first.last_error}")

    attached = client.vector_stores.files.upload_and_poll(
        vector_store_id=store.id,
        file=("refund-timing.txt", b"Refunds are processed in 5 business days."),
        poll_interval_ms=1000,
    )
    file_ids.append(attached.id)
    print("ingestion:", attached.status)
    if attached.status != "completed":
        raise RuntimeError(f"Ingestion failed: {attached.last_error}")

    results = client.vector_stores.search(
        vector_store_id=store.id,
        query="How many business days does a refund take?",
        max_num_results=5,
    )
    assert results.data, "Expected at least one matching chunk"
    for result in results.data:
        print(result.filename, result.score)
        for part in result.content:
            print(part.text)
    print("pagination:", results.has_more, results.next_page)
finally:
    try:
        if store is not None:
            deleted = client.vector_stores.delete(store.id)
            print("store deleted:", deleted.deleted)
    finally:
        for file_id in file_ids:
            deleted = client.files.delete(file_id)
            print("file deleted:", file_id, deleted.deleted)
        client.close()

Output includes file IDs, the store ID, ingestion: completed, matching text and cleanup results. Each file appears once, with its matching chunks ordered by relevance. Search does not paginate; when nothing was cut off it prints pagination: False None.

In develop measurements on 2026-10-11, text, Markdown, PDF, DOCX, CSV, JSON and HTML took roughly 4–21 seconds; one PNG took about 87 seconds. These are sample timings, not latency guarantees. Processing has a 30-minute deadline; inspect last_error if it fails.

Deleting a knowledge base retains it for 7 days for restoration. Source files are deleted separately. If you need these documents later, move cleanup after their final use in your own program.

Request fields → · Understand search results →

Concepts

Knowledge bases and files

Knowledge base IDs start with vs_. /knowledge/v1 and /openai/v1/vector_stores access the same data. The native API also provides restore, retry, segments, events and search explanations. Call from a server using an API Key.

Upload through the file service to obtain a file_id, then attach it to a store. The store file has the same ID as the source. Attaching an existing file returns its original record without reprocessing or applying new attributes or chunking. Batches behave the same way: a file already in the store is not ingested again when it appears in a batch, and the batch's attributes and chunking_strategy apply only to newly added files. To change attributes, use the update file endpoint (POST /vector_stores/{id}/files/{file_id}); to change chunking, remove the file and attach it again.

Ingestion states

statusMeaningNext step
in_progressContent is being processedPoll the file or use an SDK polling helper
completedContent is searchableSearch
failedProcessing failedRead last_error, correct the file or retry through the native API
cancelledProcessing was cancelledRemove, then add again if needed

Images (PNG, JPEG, GIF, WebP) are ingested as a model-generated description of the picture, not verbatim OCR: text in the image may be summarized or left out, and search matches that description. Images take longer to ingest than text; a single image took from a few seconds to tens of seconds in measurements, and longer when several files are ingested at once.

The native praxis.stage reports queued, parse, chunk, embed and index, then becomes null on completion. Store status reflects file processing and expiry. An empty store can be completed; that status does not guarantee it contains searchable content.

A batch attaches up to 500 files. Provide exactly one of file_ids or per-file files. With files, top-level attributes and chunking_strategy are ignored. Cancelling only affects batches in progress; cancelling a completed, failed or cancelled batch returns it unchanged.

Chunks

The default auto strategy resolves to 800 estimated tokens per chunk with 400 overlap tokens. Static chunks allow 100–4096 tokens; overlap must not exceed half the chunk size. Token counts are estimates, not the output of a specific model tokenizer.

The native API exposes chunk text, index, estimated tokens, page, UTF-16 start/end offsets and overlap counts. Page can be null. File content returns chunk texts, so account for overlap before joining them. usage_bytes is the original file size in bytes for indexed files and 0 for files not yet indexed; a store's usage_bytes is the sum of its files.

Search results

/openai/v1/vector_stores/{id}/search matches OpenAI: one result per file, with content listing that file's matching chunks from most to least relevant, score set to the best chunk, and max_num_results counting files (default 10, at most 50). Search on /knowledge/v1 returns one result per chunk, so a file can appear more than once, which suits building model context directly.

query accepts a string or an array; arrays are joined with newlines for one search. File attribute filters support eq, ne, gt, gte, lt, lte, in, nin and and/or combinations. Comparisons are type-strict: numeric 1 differs from string "1".

Search combines meaning and keywords with default weights of 0.7/0.3. The native API accepts ranking_options.hybrid_search weights and praxis.explain: true for explanations. Sending hybrid_search to the compatible endpoint returns 400. Only ranker default-2024-11-15 requests reranking; none, auto and omission keep the initial ranking. Reranking has not been measured separately.

Scores range from 0 to 1 and rank results within one search; they are not relevance probabilities and have no default floor. Unrelated content in the store is still returned, sometimes with scores close to relevant content (measured: 0.70 for the relevant file, 0.61–0.62 for unrelated ones). To filter, set ranking_options.score_threshold; results below it are not returned. OpenAI also applies no threshold by default. Start around 0.5, then check the scores of relevant and unrelated results on your own content and typical questions, and raise it until relevant results start to drop.

With rewrite_query: true the query is rewritten before retrieval; see Models below. has_more reports that more results were cut by max_num_results; search does not paginate and next_page is always null. Native content includes chunk_id, page and span; compatible responses omit them and praxis.

Models

Each knowledge base has three model roles. Set them in models when creating a store through the native API, and change them later with the update endpoint; unset roles use the defaults. Reading a store returns the effective model for each role in models.

RolePurposeDefault model
embeddingGenerates vectors for chunks and queriesbge-m3
rerankReorders results when ranking_options.ranker is default-2024-11-15bge-reranker-base
query_rewriteRewrites queries when a search sets rewrite_query: trueqwen3.8-flash

Once a store has files, embedding cannot change (existing vectors come from that model) and changing it returns 409; the other roles can change at any time. The model must exist in the model catalog and serve the role, and an embedding model must output 1024-dimension vectors; otherwise the request returns 400. Setting a role to null restores its default; models: null restores all defaults. The OpenAI-compatible API does not return models and rejects it with 400, as OpenAI does for unknown arguments, but its searches use the models configured on the store.

With rewrite_query: true, the query_rewrite model first turns the question into a concise search query (for example "How long until my refund arrives after a return?" → "refund arrival time"). Retrieval uses the rewrite and search_query returns it; an array query is rewritten item by item. Each rewrite is one small model call that is billed by model usage and adds about 1–2 seconds. If a rewrite fails, takes longer than 5 seconds or comes back empty, that query falls back to the original text and the search continues; it never fails the search. Omitted or false makes no model call.

Updates, expiry and deletion

Omitting name creates an empty name. Creation rejects null; updating name to null clears it. Metadata updates replace all metadata; null clears it. Description is accepted but discarded. Unknown write fields are ignored, so misspellings may not produce errors.

expires_after accepts only last_active_at with 1–365 days. Search and attachment refresh activity; null disables automatic expiry. A deleted store enters the deleting state: on /knowledge/v1 it reads back with status: deleting and a purge_at time, default lists hide it (list it with status=deleting), and search, updates and attachments return 409. Within 7 days you can restore it, or purge it immediately and permanently. /openai/v1/vector_stores matches OpenAI and returns 404 after deletion. Deleting a store never deletes the source files.

API reference → · Limits and errors →

Limits and errors

Capacity

These limits apply to the current API. Processing timings below are measurements, not a service guarantee.

LimitValue
Non-deleted stores per organization100
Files per store10,000
Source file size100,000,000 bytes (100 MB)
Estimated tokens per file5,000,000
Files per batch1–500
Store name256 characters
metadata / attributes keys16
Key / string value length64 / 512 characters
Static chunk size100–4096 tokens
Default chunk / overlap800 / 400 tokens
Maximum overlap≤ ½ chunk size
Search results (files on the compatible API, chunks on the native API)1–50; default 10
List page size1–100; default 20
Expiry days1–365
JSON request body1,000,000 bytes
Ingestion deadline30 minutes
Processing retries3
Retention after deletion7 days

Metadata values are strings. Attribute values may be strings, numbers or booleans. Filter nesting is limited to 20 levels below the root. Search candidates are capped at 50 per retrieval method. Search has no next page.

File formats and processing time

A knowledge base can ingest these types (as detected by the file service):

TypeIngested content
PDFDocument text
DOCX, PPTX, XLSXDocument text
PNG, JPEG, GIF, WebPA model-generated description of the picture, not verbatim OCR
HTMLVisible text, without scripts and styles
Plain text, Markdown, CSV, JSONThe original text

An accepted upload is not necessarily ingestible. The file service accepts these types, but a knowledge base cannot ingest them: audio (MP3, WAV, FLAC, OGG, M4A), video (MP4, WebM), NDJSON, XML, YAML, SVG, and files detected as Python or JavaScript source. Attaching them is not refused; ingestion fails asynchronously, the file status becomes failed and last_error.code is unsupported_file. Encrypted PDF or Office files also fail during ingestion (invalid_file). Empty files, ZIP archives and other binary files are rejected by the file service at upload.

Develop measurements on 2026-10-11: text and documents finished ingestion in about 4–21 seconds; a single image took about 4–11 seconds, and one PNG took about 87 seconds while 7 files were ingested at once.

HTTP errors

HTTPcodeAction
400INVALID_PARAMS / LIMIT_EXCEEDEDCorrect parameters or reduce the request size.
401UNAUTHENTICATEDCheck the API Key; compatible code is null.
404VECTOR_STORE_NOT_FOUND / FILE_NOT_FOUND / FILE_NOT_IN_STORE / BATCH_NOT_FOUND / CHUNK_NOT_FOUNDCheck the resource ID and its store.
409VECTOR_STORE_INVALID_STATE / FILE_INVALID_STATECheck deletion, expiry or file processing state.
413PAYLOAD_TOO_LARGEJSON request exceeds 1 MB; compatible code can be null.
429RATE_LIMITEDRetry later.
500— / nullInternal error; retain the request ID and retry later.
501NOT_IMPLEMENTEDOperation is not implemented.
503PROVIDER_UNAVAILABLEProcessing is temporarily unavailable; retry later.

Native errors normally use error.code, error.message and error.details. Compatible errors use error.message, error.type, error.param and error.code; the official SDK raises the corresponding HTTP error class. Unknown routes can omit code, 401 can omit details and malformed URL encoding can produce HTML.

Asynchronous failures

A file with status failed reports last_error.code: unsupported_file for unsupported parsing, invalid_file for invalid/empty/encrypted content, or server_error for processing failure or timeout. The estimated-token limit currently maps to invalid_file; its over-limit behavior has not been measured. Check file status after a successful attachment. Retry is available through the native API for failed files; successful retry after failure was not covered in the measurements.

Actual expiry and purge after 7 days have not been observed end to end. No reliable per-service requests-per-second limit was established by the tests.

Complete error objects → · Search limitations →

Database

What Database is for

Database is a headless content store: people, agents and scripts read and write structured tables and rows directly through the public API, as with a headless CMS. It holds content such as itineraries and catalogs, independently of the applications that use it. It is neither an application's private runtime database nor a data warehouse.

RoleWho uses itLifecycleWhere to store it
Content storePeople, agents and scripts; applications can read it through public APIsContent is edited and reused independently of application releasesDatabase for structured content; File for files; Knowledge for searchable knowledge
Application runtime storeApplication code managing orders, sessions and business rulesState changes as the application runs, under its own business constraintsThe application's own d1, kv or r2 components
Data warehouseAnalysts and reporting pipelinesHistorical data accumulated for analysisA dedicated analytics warehouse, outside Database

A typical pattern is content store → build → publish to Application: read itinerary content from Database, build site files, then use praxis.application.publish to upload, version and deploy them. Editing the source content does not update those published files; build and publish again to include the edits.

Each database is an isolated unit with its own tables, fields, rows and events.

Create databases and table structure through the Database API; read and write rows with the official supabase-js client. Both row access URLs for a database share the same data.

  • Quickstart: create a database and table, then write and read your first row.
  • Concepts: field types, links, versions and recovery.
  • Limits and errors: capacity and query boundaries.

API reference → · Supabase-compatible API → · Cookbook →

Quickstart

Run this example in Node.js 22 or later with your develop API Key in the PRAXIS_API_KEY environment variable. It creates a new database on each run.

npm install @supabase/supabase-js

Save the following as quickstart.mjs and run node quickstart.mjs.

import { createClient } from "@supabase/supabase-js";

const api = "https://api.deepraxis.com";
const key = process.env.PRAXIS_API_KEY;
if (!key) throw new Error("Set PRAXIS_API_KEY");

async function request(path, method, body) {
  const response = await fetch(`${api}/database/v1${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  const result = await response.json();
  if (!response.ok) throw new Error(`${response.status}: ${result.message}`);
  return result;
}

const database = await request("/databases", "POST", {
  name: "Agent tasks",
  labels: { purpose: "docs" },
});
await request(`/databases/${database.id}/tables`, "POST", {
  name: "tasks",
  fields: [
    { name: "title", type: "string", required: true, unique: true },
    { name: "done", type: "boolean" },
  ],
});

const supabase = createClient(`${api}/supabase/${database.id}/`, key, {
  auth: { persistSession: false, autoRefreshToken: false },
});

const inserted = await supabase.from("tasks")
  .insert({ title: "Write a summary", done: false }).select();
if (inserted.error) throw inserted.error;

const selected = await supabase.from("tasks")
  .select("*").order("title").limit(10);
if (selected.error) throw selected.error;
console.log({ database_id: database.id, rows: selected.data });

The output includes database_id and the row you inserted. Keep the database ID for subsequent calls. The trailing slash in the Supabase URL is required.

Row operations → · Concepts →

When finished, delete this example database with the following request. The database recovery window is described in Concepts.

curl -X DELETE "https://api.deepraxis.com/database/v1/databases/${DATABASE_ID}" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

Set DATABASE_ID to the ID printed by the example.

Concepts

What Database is for

Database is a headless content store: people, agents and scripts read and write structured tables and rows directly through the public API, as with a headless CMS. It holds content such as itineraries and catalogs, independently of the applications that use it. It is neither an application's private runtime database nor a data warehouse.

RoleWho uses itLifecycleWhere to store it
Content storePeople, agents and scripts; applications can read it through public APIsContent is edited and reused independently of application releasesDatabase for structured content; File for files; Knowledge for searchable knowledge
Application runtime storeApplication code managing orders, sessions and business rulesState changes as the application runs, under its own business constraintsThe application's own d1, kv or r2 components
Data warehouseAnalysts and reporting pipelinesHistorical data accumulated for analysisA dedicated analytics warehouse, outside Database

A typical pattern is content store → build → publish to Application: read itinerary content from Database, build site files, then use praxis.application.publish to upload, version and deploy them. Editing the source content does not update those published files; build and publish again to include the edits.

A database is an isolation unit. Tables define record structure, fields define value types, and rows hold the records.

Databases, tables, fields and rows

Your API Key determines which databases you can access. Tables and fields accept IDs or names. Links use table IDs, so renaming a table does not change the link target. Database labels support categorization and filtering.

Default rows contain only the primary key id and user fields, including select=*, return=representation and RPC rows. Only id is a reserved field name; new fields cannot start with _. created_at, updated_at and version are ordinary field names. Field names start with a lowercase letter, followed by lowercase letters, digits or underscores, up to 63 characters; table names also allow a leading underscore. Existing underscore user fields remain readable and writable and take precedence over same-named metadata, making that metadata inaccessible in the table.

System metadata is read-only and opt-in: use select=*,_version,_created_at,_updated_at. Star never expands it. Use _version=eq.3 in filters or order=_created_at.desc for sorting. Without order, rows still sort by internal creation time, then id. Old metadata names in filters/order without a same-named user field return 400: details says use _created_at (or the corresponding name), and hint says system metadata moved to underscore columns.

A datetime field can use {"name":"created_at","type":"datetime","default":"now()"}. Omitting it on insertion stores current server UTC time; the field definition continues to show "now()". Explicit null and omitted PATCH fields do not use defaults. String/json defaults of "now()" remain literal strings. Adding this field to an existing table evaluates the default for the backfill too.

Seven field types

TypeValue
stringA string
integerAn integer
numberA number
booleantrue or false
datetimeAn RFC 3339 date-time such as 2026-10-11T08:30:00+08:00; impossible dates (such as February 30) return 400. Values are stored and read back in UTC with three fractional digits, such as 2026-10-11T00:30:00.000Z; precision is milliseconds and extra fractional digits are rounded half up
jsonA JSON value satisfying the supplied JSON Schema
linkA row ID, or multiple row IDs, in another table in the same database

required requires a non-null value. A new field with a default fills every existing row with it; a new field without one is null on existing rows. Adding a required field to a table that already has rows needs a default; making an existing field required returns 400 VALIDATION_FAILED with the number of empty rows while any row is null in that field, so fill those rows first. unique requires unique values. The first five types are indexed by default; set indexed: false where unnecessary. json and link cannot be indexed. A field's type cannot change after creation.

Batch writes and transactions

One POST inserts at most 500 rows. Each request is atomic: a failure in any row rolls back the entire batch. There are no transactions across requests. Put rows that must succeed together in one batch POST. For links, query the target first, then insert the referencing rows. Querying first does not prevent concurrent changes; still handle a missing-link error at insert time.

Rows in a batch are filled to the union of their keys with null (PostgREST behavior), so omitting a required field in one row can reject the whole batch. Send Prefer: missing=default to use field defaults for missing keys. Explicit null remains null, and required fields without defaults must still be provided.

The default return=minimal returns 201 with an empty body. Send Prefer: return=representation to read back inserted rows, or chain .select() after supabase-js .insert(...).

JSON Schema

A json field requires JSON Schema in options.schema. For example, this field only accepts objects containing a string summary.

{
  "name": "detail",
  "type": "json",
  "options": {
    "schema": {
      "type": "object",
      "properties": { "summary": { "type": "string" } },
      "required": ["summary"],
      "additionalProperties": false
    }
  }
}

Schema updates constrain subsequent writes without rewriting existing rows. Use detail->>summary to extract its string value in queries.

A link's options.linked_table_id points to a target table in the same database. multiple selects a single row ID or an array of row IDs. The target cannot change after creation.

{
  "name": "project",
  "type": "link",
  "options": { "linked_table_id": "tbl_example", "multiple": false }
}

Replace tbl_example with an existing table ID. References to nonexistent rows are rejected. Deleting a row that other rows still reference is also rejected; rows that reference each other can be deleted together in one delete. Remove references before deleting the target; deletion does not cascade automatically.

Use _version to avoid overwriting another update

A new row starts at _version: 1, increasing on every update. Filter by both the row ID and the _version you read. An empty result means the condition no longer matches; read again before deciding what to do next.

const { data: row, error: readError } = await supabase.from("tasks")
  .select("*,_version").eq("id", rowId).single();
if (readError) throw readError;

const { data, error } = await supabase.from("tasks")
  .update({ done: true })
  .eq("id", row.id)
  .eq("_version", row._version)
  .select();
if (error) throw error;
if (data.length === 0) throw new Error("Read the row again before updating");

Restore a deleted database within 7 days

After deleting a database, its rows cannot be read or written. Restore the database within 7 days to use it and its retained tables and rows again. After that window, recovery is unavailable. Recovery applies to the whole database, not individually deleted tables, fields or rows.

API reference → · Limits and errors →

Limits and errors

Plan databases and queries within these limits.

LimitMaximum
Tables per database50
Fields per table64
Indexed fields per table (including unique)16
Rows per table1000000
Bytes per row2097152 (2 MiB)
Rows per insert500
Parameter values per query100
Database recovery window, days7
Rows scanned per query3000000

Each request may run for up to 8 seconds; a timeout returns 57014 and retrying will not help. Index the fields you filter and sort on, or narrow the query. Counting without a filter is always fast. Without a limit a read returns at most 1000 rows; page with limit / offset or Range. To update or delete many rows in a large table, work in batches over primary-key or indexed ranges, for example tens of thousands of rows per batch. Batch inserts support the row limit above; do not multiply rows by fields to estimate query parameters.

Error codes and retryability → One batch POST inserts at most 500 rows. Each request is atomic: a failure in any row rolls back the entire batch. There are no transactions across requests. Put rows that must succeed together in one batch POST. For links, query first, then insert, and handle link errors caused by concurrent changes.

Rows in a batch are filled to the union of their keys with null (PostgREST behavior). Send Prefer: missing=default to use field defaults instead. Explicit null remains null; required fields without defaults must still be provided. The default return=minimal returns 201 with an empty body; send Prefer: return=representation to return rows.

Correct the request using error details

Only id is a reserved field name. New field names cannot start with _, which is reserved for system metadata. created_at, updated_at and version are ordinary user field names. Datetime fields can use "now()" as a default.

details identifies the field or JSON path and the received value. Batch validation includes a zero-based row index. Values are limited to 80 characters; the entire row is never echoed. hint suggests a correction, or is null when no suggestion applies. This example omits the required title in row 2:

{
  "code": "23514",
  "message": "row validation failed",
  "details": "row 1: title: required value (got null)",
  "hint": "Rows in one insert are filled to the union of their keys with null (PostgREST behavior). Send every key in every row, or send Prefer: missing=default to use field defaults."
}

A unique conflict returns 409, for example:

{
  "code": "23505",
  "message": "unique constraint violated",
  "details": "Key (email)=(a@b.c) already exists.",
  "hint": null
}

Vault

Vault keeps the credentials agents need to call third-party services, such as a GitHub token, an OpenAI API key or a Linear authorization. Each vault is a set of secrets; one secret is one credential.

Secrets are write-only: once written, no endpoint returns the value again. Every secret carries a list of allowed hosts, and only requests to those hosts can use it. Agents and tools reference secrets by name, for example GITHUB_TOKEN.

  • Quickstart: create a vault, add a secret, then list it.
  • Concepts: secret kinds, templates, import, authorization and refresh.
  • Limits and errors: capacity limits and error handling.

API reference → · Claude Managed Agents vaults → · Cookbook →

Quickstart

Put your API Key in the PRAXIS_API_KEY environment variable and run the three requests below in order. Each run creates a new vault. Install jq to extract its ID; optionally set VAULT_NAME to choose its display name.

1. Create a vault

VAULT_JSON=$(curl --fail-with-body -sS -X POST "https://api.deepraxis.com/vault/v1/vaults" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg name "${VAULT_NAME:-Agent credentials}" '{display_name: $name}')")
export VAULT_ID=$(printf '%s' "$VAULT_JSON" | jq -er '.id')
printf '%s\n' "$VAULT_JSON"

Expect HTTP 200 with an object whose type is vault, status is active and secret_count is 0. The command saves its vlt_ ID in VAULT_ID.

2. Add a secret

curl -X POST "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID/secrets" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "GITHUB_TOKEN", "value": "ghp_example", "allowed_hosts": ["api.github.com"]}'

Expect HTTP 200 with an object whose type is vault_secret, kind is static and version is 1. The response has no value: once written, a value is never returned. This secret can be used only on requests to api.github.com.

3. List secrets

curl "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID/secrets" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

Expect HTTP 200 with one item in data named GITHUB_TOKEN, and next_cursor set to null.

Clean up

curl -X POST "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID/archive" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

curl -X DELETE "https://api.deepraxis.com/vault/v1/vaults/$VAULT_ID" \
  -H "Authorization: Bearer $PRAXIS_API_KEY"

Expect HTTP 200 with status: "archived" from archive, then HTTP 200 with {"id": "<VAULT_ID>", "type": "vault_deleted"}. Deleting a vault also deletes every secret in it and cannot be undone.

Templates, .env import and one-click authorization → · All requests and objects →

Concepts

A vault is a set of secrets; a secret is one credential made of a name, a value and allowed hosts. Every other feature is a way to create or update secrets.

Vaults and secrets

Your API Key decides which vaults you can reach. A vault has a display name and up to 16 metadata pairs.

A secret's name is unique within its vault and cannot change after creation. It contains 1–128 uppercase letters, digits or underscores, may start with an underscore, and cannot start with a digit, for example GITHUB_TOKEN. Address a secret by its name or its vsec_ ID. Lookup by an invalid name also returns 404 SECRET_NOT_FOUND.

Values are write-only: after you create or replace one, no endpoint returns it. Each replacement increments version.

Allowed hosts

Every secret declares 1–16 allowed_hosts; an empty array returns 400 INVALID_PARAMS. Only requests to those hosts can use the secret.

EntryMatches
api.github.comThat host only
*.example.comAny subdomain of example.com, not example.com itself
192.0.2.1That IPv4 address only

Hosts are stored lowercase, without a trailing dot, with internationalized names in punycode. Duplicates are removed. There is no "any host" option. IPv6, single-label hosts (including localhost), *.internal, platform domains, and loopback, link-local, private or CGNAT IPv4 addresses return 400 INVALID_PARAMS.

Two kinds of secret

kindWhere the value comes fromStatus
staticThe value you supplyactive
oauthAn access token from authorization, refreshed by the platformactive, refreshing, refresh_failed

Either kind becomes archived when archived.

Templates

Templates prefill the name, allowed hosts and kind for common services, so you only supply the value. GET /vault/v1/templates lists them, with static templates github, openai, anthropic, stripe, and OAuth templates linear, atlassian. GitHub prefills GITHUB_TOKEN, api.github.com and uploads.github.com. When adding a static secret, template_id records its source as metadata.template.

metadata.source and metadata.template are system-owned keys. Callers cannot write them, and they do not count toward the 16-label limit. Vault and secret metadata updates merge by key: strings upsert, null deletes, and omitted keys remain. For secrets, description: null clears the description; omission preserves it.

Import a .env file

POST /vault/v1/vaults/{vault_id}/secrets:import creates several static secrets at once. Send dotenv (multi-line KEY=VALUE text) or a lines array, not both; For new secrets, default allowed_hosts applies to lines without their own hosts. Replacing an existing name changes only its value. Hosts change only from that line's own allowed_hosts, never from top-level defaults. The result lists created, replaced and failed lines; failed lines carry a line number and error code, and the other lines are still written.

One-click authorization

Linear (CIMD: Client ID Metadata Document) and Atlassian (DCR: Dynamic Client Registration) support one-click authorization without registering an app first. Add credentials manually for services requiring app registration: GitHub, HubSpot, Asana and Vercel. Asana is not a one-click authorization template.

  1. Call POST /vault/v1/authorizations with an authorization-capable template_id, or the resource_url to authorize.
  2. Give the returned connect_url to the end user. The link is valid for 30 minutes and works once.
  3. The user sees who is asking, the target service and the requested permissions, approves, and finishes on the service's own page.
  4. The vault then holds an oauth secret named secret_name. GET /vault/v1/authorizations/{id} reports the status: pending, authorizing, completed, failed or expired.

If the vault already has a secret with that name, authorization updates it in place: the ID stays the same and version increments.

To send the user back to your app afterwards, pass return_url. It must be on your return URL allowlist, set with PUT /vault/v1/settings/return_urls: at most 10 https URLs, matched exactly. The return carries authorization_id and status.

Refresh and validation

The platform refreshes an oauth secret's access token before it expires. If the service refuses the refresh, the secret becomes refresh_failed and oauth.last_error explains why; authorizing again or replacing the value makes it active again. If the service is temporarily unavailable, the secret stays active and the refresh is retried later.

POST …/secrets/{secret}/validate probes the service once with the current token and returns valid, invalid or unknown; it applies to oauth secrets only. A caller that receives a 401 from the service can call POST …/secrets/{secret}/refresh to refresh immediately; this also requires an oauth secret. Static validation returns 400 VALIDATE_UNSUPPORTED; static refresh returns 400 SECRET_KIND_MISMATCH.

Audit events

Events record creation, updates, archival, deletion, use, refresh and authorization outcomes, never secret values. GET /vault/v1/vaults/{vault_id}/events lists newest first. Filter by secret name or ID; unknown secrets return an empty list. Update events list changed fields, such as value and allowed_hosts, in detail.changed.

Events have no time-based expiry. Archiving preserves events; deleting a vault or secret removes its history and leaves a deletion record. limit defaults to 50 and caps at 200; invalid limits or cursors return 400 INVALID_PARAMS. See the event reference for all event types.

Archive and delete

Archiving a vault archives every secret in it and clears their values; nothing more can be written to it. Deleting a vault removes every secret and cannot be undone. Individual secrets can also be archived or deleted.

Claude Managed Agents vaults compatibility

The same vaults are reachable with the Claude Managed Agents SDK compatibility API (/anthropic/v1/vaults). Every request needs an API Key, anthropic-version: 2023-06-01, and anthropic-beta containing managed-agents-2026-04-01. The beta header may include other comma-separated betas. Lists paginate with page, not cursor; credentials are addressed only by vsec_ ID. Setting a vault's display_name to null returns 400.

Errors have shape {type: "error", error: {type, message}, request_id}; HTTP 409 maps to conflict_error. Non-retryable errors, including 409, carry x-should-retry: false to prevent SDK retries.

Credential typeMatching secret
environment_variableNamed secret_name, allowed hosts from networking.allowed_hosts; networking supports limited only
static_bearerAllowed host is the host of mcp_server_url; the name is generated as MCP_<HOST>
mcp_oauthCreation is not available yet and returns 501; use one-click authorization instead

Secrets created through /vault/v1 appear there as environment_variable, with injection_location substituting in request headers only.

Limits and errors

Plan vaults and secrets within these limits.

LimitMaximum
Active secrets per vault50
Secret value size16384 bytes (16 KiB)
Allowed hosts per secret16
Lines per import50
Authorization creation / OAuth validation (per account per minute, counted separately)30 / 30
Vault name length255
Metadata pairs16
Metadata key / value length64 / 512
Items per list page50 (max 200)

An existing secret with the same name returns 409 SECRET_CONFLICT; writing to an archived vault returns 409 VAULT_INVALID_STATE; exceeding the active-secret limit returns 400 VAULT_LIMIT_EXCEEDED; a value over 16 KiB returns 413 SECRET_TOO_LARGE. Authorization creation and OAuth validation each allow 30 requests per account per fixed one-minute window. Exceeding either quota returns 429 RATE_LIMITED; wait for the next window. An expired authorization link returns 410 AUTHORIZATION_EXPIRED; start a new authorization.

Through the Claude Managed Agents vaults API a vault holds at most 20 active credentials, and errors use that API's own error format.

Error codes and retryability →

Account & billing

Identity & permissions

(How-to guide, TODO.)

Pricing

Praxis pricing has one rule: each model is benchmarked to the vendor's published list price, and the Praxis price is below it. Per-model prices are in the model catalog, in US dollars per million tokens, before tax.

How prices are set

  • The benchmark is the price on the vendor's official pricing page; Praxis records the source and date.
  • The Praxis price is the same for every customer and below the vendor's list price; the discount is the same for input, output, cache-write and cache-read tokens.
  • When a vendor changes its price, the Praxis price follows; Praxis gives 30 days' notice before changing its own price.

How you pay

  • Prepaid credit, deducted by actual usage; you pay for what you use. Subscriptions are subscriptions to usage, not licences.
  • Unused credit is refundable.
  • Usage and balance are on the console's Usage and Billing pages.

Buying through a partner

Praxis may have a revenue-share arrangement with the partner who introduced you. It does not affect your price: buying through a partner never costs more than buying directly. The partner program's rules are under Partners.

Usage

(How-to guide, TODO.)

Billing & top-up

(How-to guide, TODO.)

Quotas & limits

This page is about your account's quota and how to raise it. The why is in Rate limits & errors; handling in Errors & limits.

(How-to guide, TODO.)

Help & reference

Cross-section help lives here. API authentication, endpoints, errors, and versioning now belong to the top-level API.

Glossary

(Reference, TODO.)

Service status

Status page: https://status.deepraxis.ai (in progress).

FAQ

(TODO)

Support & feedback

Security & compliance

(TODO: APAC residency · file retention · privacy / terms links.)