PhysiCar API¶
How to call PhysiCar AI's cloud AI services from your own code. There are two:
chat (text, POST https://api.physicar.ai/chat) and realtime (voice,
wss://api.physicar.ai/realtime). Both speak one common format and route to
multiple providers behind the scenes, and both take the same prompt shape —
instructions + tool definitions — so one "brain" can serve a text agent and a
voice agent alike.
A complete working example is the workspace notebook examples/agent.ipynb
(kernel Python 3 (PhysiCar AI)): a MyApp page that runs both agents in the
browser and drives the robot through tool calls.
Authentication¶
- Authenticate with your session token. HTTP requests send it as
Authorization: Bearer <token>; WebSockets pass it as the subprotocoltoken.<token>(browsers cannot set WS headers). - On any
/myapp/page,physicarSession.token()is auto-injected by nginx and returns the signed-in user's token — no setup code. Read it per request: on a shared robot, caching one token globally would put every user on one account. /chatalso accepts guest requests (no token) up to a small daily limit per network; signed-in usage draws on your credits.
Chat¶
POST https://api.physicar.ai/chat — one request per turn. The prompt (model +
instructions + tools) is sent inline on every request; the server keeps only the
conversation, addressed by chat_id/turn and retained for 7 days.
GET /chat/models lists the available models with their price per 1M tokens (and,
for classroom students, whether the teacher allows them).
Request body¶
{
"user_message": { "contents": [ { "type": "text", "text": "Hello!" } ] },
"prompt": { "model": "...", "instructions": "...", "tools": [ ... ] },
"stream": true,
"audio": false,
"chat_id": "…",
"turn": 0
}
user_message.contents—textand/orimage({ "type": "image", "mime": "image/jpeg|image/png", "base64": "…" }) parts.user_message.tool_call_outputs— results of the previous turn's tool calls (see the tool loop below).prompt—model,instructions,tools,reasoning_effort,max_completion_tokens. The server capsmax_completion_tokensto what your balance affords. Prompts can also be stored server-side (/chat/promptCRUD) and referenced withprompt_id.chat_id+turn— continue an existing conversation. Omit both on the first turn; thedoneevent returns the issuedchat_id. Sendturn + 1next time (or omitturnto continue at the end). Sending an earlierturnrewinds the conversation to that point.
Tool definitions¶
{
"name": "drive",
"description": "Drive the robot.",
"properties": [
{ "name": "speed", "type": "number", "description": "m/s", "required": false }
]
}
Stream events¶
With "stream": true the response is Server-Sent Events; each data: line is a
JSON object with a type:
type |
Carries |
|---|---|
text |
a chunk of the reply (content) |
tool_call |
call_id, name, arguments |
audio |
data (base64 PCM) — only with "audio": true |
done |
chat_id, turn, full_text, tool_calls, usage, finish_reason |
error |
message, code |
usage is the credits deducted for the turn. finish_reason is provider-neutral:
stop (normal, including tool calls), length (cut off), refusal.
Without stream the response is a single JSON object with the same fields
(chat_id, turn, text, tool_calls, usage, finish_reason, and audio
with {data, duration_ms} when requested).
The tool loop¶
When done.tool_calls is non-empty, run each {call_id, name, arguments} yourself
and send the results as the next request's user_message:
{
"chat_id": "…", "turn": 1,
"prompt": { "model": "...", "instructions": "...", "tools": [ ... ] },
"user_message": {
"contents": [],
"tool_call_outputs": [
{ "call_id": "…", "name": "drive",
"contents": [ { "type": "text", "text": "speed 0.5 m/s, drove 2 s" } ] }
]
},
"stream": true
}
A tool result's contents may include images — that is how a camera tool puts its
photo into the conversation for the model to describe.
From the browser¶
const res = await fetch("https://api.physicar.ai/chat", {
method: "POST",
headers: { "Content-Type": "application/json",
"Authorization": "Bearer " + physicarSession.token() },
body: JSON.stringify({
chat_id: chatId, // undefined on the first turn
turn, // 0 on the first turn
prompt: { model, instructions, tools },
user_message: { contents: [{ type: "text", text: "Hello!" }] },
stream: true,
}),
});
Realtime¶
wss://api.physicar.ai/realtime — speech-to-speech voice conversation:
mic ──▶ realtime cloud (speech-to-speech LLM) ──▶ speaker
│ ▲
tool_call tool_result
▼ │
your tools ──▶ robot web API (/speed, /camera, ...)
GET https://api.physicar.ai/realtime/models lists the available realtime models.
Browsers authenticate with the token.<token> subprotocol; non-browser clients may
use ?token= instead (the subprotocol form is recommended — query strings can end
up in logs).
The protocol uses JSON text frames only — audio rides inside JSON as base64
pcm16. Sample rates differ per model and are advertised in session.ready's
audio_config.
Client → server¶
| Event | Carries |
|---|---|
session.start |
prompt (model, instructions, tools, voice), optional chat_id to continue a conversation |
audio |
data — base64 pcm16 at audio_config.input_rate |
text |
text — text input, triggers a turn |
image |
data, mime, turn_complete — explicit image input (e.g. a camera shot) |
tool_result |
call_id, name, output |
session.end |
end the session |
Server → client¶
| Event | Carries |
|---|---|
session.ready |
model, audio_config (format, input_rate, output_rate), chat_id |
audio.delta |
data — base64 pcm16 at audio_config.output_rate |
transcript.delta |
role (user/assistant), text |
tool_call |
call_id, name, arguments — same shape as chat |
turn.complete |
the turn finished |
interrupted |
barge-in — discard any audio you are still playing |
error |
code, message |
session.end |
code, reason, session_cost (credits) |
Sessions¶
- A session is capped at 30 minutes and 100 credits; it also ends after
90 seconds of inactivity. The
session.endevent carries the reason code (CLIENT_END,TIME_LIMIT,CREDIT_LIMIT,IDLE_TIMEOUT, …) and the credits the session consumed. Up to 10 concurrent sessions per user. - Transcripts share the conversation store with
/chat: pass the session'schat_idto a latersession.start— or to a/chatrequest — and the conversation continues.
From the browser¶
const ws = new WebSocket("wss://api.physicar.ai/realtime",
["token." + physicarSession.token()]);
ws.onopen = () => ws.send(JSON.stringify({
type: "session.start",
prompt: { instructions, tools },
}));
Learn more
- Build the voice agent step by step → Agent
- How tool calling works → Agents & tool calls
- Robot endpoints your tools will call → PhysiCar ROS