pinecallDocs
The gateway / Gateway API

Gateway API

The tenant's endpoints: agents, calls, sessions, knowledge and memory.

Every endpoint a tenant's own code may knock at, and what comes back. The pinecall package speaks exactly these endpoints, so an app written against this page in any language is a first-class client. The operator's half is Operator API; who a key is, Orgs, people and keys; every endpoint in one table, Every endpoint.

The shape of it#

One gateway serves both environments, production and the sandbox, at /v1, and beside it the console at / and the widget at /widget/pinecall-widget.js, the one answer carrying Access-Control-Allow-Origin: *. The console's page and its assets are framed by nobody (frame-ancestors 'none', X-Frame-Options: DENY), kept on HTTPS (Strict-Transport-Security), read as served (nosniff), and tell another site its origin alone (strict-origin-when-cross-origin). The API's own description is public too, GET /v1/docs and /openapi.json: every endpoint and every body, none of them a secret. Three kinds of connection:

what it is who opens it
HTTP read a log, mint a token, push knowledge, set a knob your backend, with the org's key
WS /v1/apps the app socket: your process holds an agent and answers its tool calls your backend, with the org's key
WS /v1/chat · a LiveKit room · a phone · WhatsApp one caller, one call a caller

The log is the truth: everything that happens to a call is an entry with a seq, written before control returns, and every endpoint that shows a call shows those entries.

Authorization: Bearer <key>             every door, HTTP and WebSocket alike
pinecall-env: production | sandbox      the world a person's key acts in (the sandbox when unsaid)

A key opens what its scopes say, and every tenant endpoint asks for exactly one: 403 this key does not open knowledge: it opens calls · evals. A server's key holds app · calls · talk · knowledge · evals and lives in the one environment it was made for; a person's key holds their role's scopes and names the environment per request, production only with production access, read from their row on every request (People). pinecall-corner: <member id> answers an HTTP endpoint in a colleague's sandbox scope, for a key that opens team and app.

fleet is the platform's own workers', one key per environment, and it acts for one call at a time. A request that names a call, in its path or as ?call=, acts in the scope that call's head row keeps, and in no other: a ?org=&env=&holder= that disagrees is 404, and so is a call nobody opened yet. A call a dial placed is opened by the worker only in the scope the dial wrote. Before its call is opened, a worker asks in the scope its dispatch named, ?org=&env=&holder= (the agent's routes, declaration, stages and hold audio, whether a ring is a developer's). It reads a call's events, state and recording without naming a scope, a call of its own environment once opened; it reads no agent's log, no org's floor and no list of calls. The overflow's POST /v1/callbacks names the call it answered, and the agent must be that call's org's.

The one exception to the header is ?token=, because an EventSource cannot set one: only a token of ours for one call (a page's log_token), never an API key, since a URL ends up in an access log.

Refusals are {"detail": "…"} under the status, and the sentence names the fix: 401 no key; 403 a key that does not open the endpoint, or another environment's; 404 a thing that is not there, and another org's call, whose existence is nobody else's business; 409 a request that disagrees with what is stored; 400 or 422 a body that is not the shape; 413 a body over 32 MiB, more than any endpoint takes, refused as it streams in (a webhook, which carries no key, over 4 MiB, before its signature is read); 429 a quota, an endpoint that runs a model past its minute's pace (120 caller lines, 6 memory extraction runs of 50 cases at most, per org), or a minute's requests spent (below); 502 a vendor or a carrier that did not answer, in its own words; 503 the request was right and this deployment cannot honour it, or not now: a database too busy to answer within its timeouts is a 503 to retry. A socket closes with 1008 and the sentence. A key past its expiry is 401 saying when it expired, never the silence of a key nobody made.

A minute's requests. Every endpoint a key opens by its scopes counts the org's requests in the environment, per family of endpoints (the scopes the endpoint asks for, calls for a log's readers): 6 000 a minute each, and the next one that minute is 429 this org sent its calls endpoints 6000 requests this minute, in production: try again in 12 s with Retry-After: 12. One org's count never slows another's, nor its other environment, nor its other families. A call's own endpoints (the ones a worker writes and reads a call through, which admission already bounds), the fleet's and the runner's keys and a person who runs the platform are never counted; the endpoints that take no key or any key (/v1/login…, /v1/whoami, /v1/keys, /v1/limits) and the sockets are not either. The count is the platform's, not one gateway's: each gateway counts what reaches it and says its busy names on the signal every second, and the wall is the sum, so an org spread over every gateway meets it within a second of reaching it (a gateway with no PINECALL_REDIS_URL counts alone).

1. Your own app: WS /v1/apps#

An app is a process that holds an agent: it declares what the agent is, receives every entry of every call the agent takes, and answers the tool calls the model makes. It binds no port; the socket is outbound. Commands go up, entries come down, one JSON object each.

command what it does
agent.register this socket speaks for this agent (routes, sdk, takes_unclaimed); answers agent.registered with this socket's app id
agent.configure what the agent is: tools, language, the prompt's layout, the state fields it declares. What it runs on (vendors, voice, greeting, hang-up, memory, bases) is the environment's, Settings
ping pong with the gateway's clock
agent.drain this process is leaving; A deploy never cuts a call

takes_unclaimed: false makes a process a console: it holds the agent but is never handed a call that named no app. answers_dev: true makes it the one the console's directory verbs go to (Dev verbs, "Which process answers"). Several sockets may hold one agent; a call goes to the one it names or to the newest that takes unclaimed calls. In the sandbox an agent is held per person: two developers each run the same slug and reach their own. The same slug in production and the sandbox is two agents.

When a call opens the socket receives call.ringing or call.dialing, call.started, then every entry of the call through call.score. The call-scoped commands carry "call": "<id>":

command when
session.configure a declaration for this call only, before the first turn
state.set the app's state changed, and this is all of it; state.changed is written and the prompt re-rendered
prompt.set · tools.set one named block of the prompt, whole; the tools the model may see now
agent.say · agent.reply say these words; make the model speak now, guided by an instruction the caller never hears
call.event · call.log a fact from your backend (event.received); a line of your own (custom)
call.hangup end the call
call.transfer · call.attention · call.hold · call.unhold · call.dtmf · call.callback the line: The line
call.claim the caller said a page's code: Codes
tool.result the answer to a tool.call, by its call_id: an output or an error, never both

A refusal (error, with the command's type and id) and a pong land on the agent's log only when this socket holds that agent; a frame naming any other slug, or no JSON at all, is answered down the socket alone (seq: 0) and the socket stays. A tool.result answers a call of an agent this socket holds, in its own org.

The prompt has three regions, in this order: the static blocks (cached by the provider), the history, and the dynamic blocks at the end. The default layout is identity, knowledge, tools before the history and view after it. A spoken call runs in a worker; the tool round trip is the same from your side. The smallest app that works: The smallest app that works. What only the process in the agent's directory can do: Dev verbs.

2. Callers#

endpoint the caller
WS /v1/chat?agent=<slug> text: send {"text": "…"}, receive the call's entries; {"hangup": true} ends the call, then closes the socket with the call ended: caller_hung_up; app=, contact=, caller=, persona=, state= (a JSON object, 16 KB at most: the state the call opens in, carried on its call.started), and call= to take up a call whose gateway restarted
POST /v1/tokens web voice: Tokens
a phone number a route to the agent: Numbers
POST /v1/codes a phone call tied to a page: Codes
WhatsApp WhatsApp
POST /v1/agents/{slug}/dial a call the agent places, past the org's dial guards: Numbers

When every worker is full the token endpoint answers 503 and a page offers a call back: POST /v1/callbacks {agent, number, channel?, via?, call?} writes callback.requested on the agent's log; GET /v1/callbacks?agent=&after= lists them for your app to dial, each in the environment of the call it names: the sandbox's key lists the sandbox's, production's lists production's.

The worker's endpoints (POST /v1/calls, /events, /entries, /sealed, /tools, /remember, /recording/key, /commands) belong to whoever opened the call: the fleet's key for a call the fleet's worker opened, an org's app key for one its own worker opened, nobody for a written call, which the gateway runs itself. Another key is answered 404. /lookup is the one exception: the agent's own process searches its knowledge on a call it serves (the framework's knowledge.search), whoever opened the call, so it takes the org's app key in the call's environment, and the fleet's. What the gateway writes on a log itself — call.ringing, call.dialing, call.attached, call.claimed, call.summary, call.score, memory.ops, docs.sources — is refused 403 at every append endpoint, the fleet's included: the summary and the score are the gateway's own words.

3. Reading a log#

GET /v1/calls/{call}/events answers a page of JSON, {entries, live, next}, or with Accept: text/event-stream the same entries as a stream that ends where the log does, at call.score. after is the cursor (a reconnecting EventSource sends Last-Event-ID; the higher wins), limit up to 500, types a comma list, durable=1 drops the ephemeral entries, token a page's log token. next is the last seq the page read, so a filtered page still moves you on. The SSE frames are id: <seq>, event: <type>, data: <the entry>, a : ping every 25 s.

A reader may watch before there is anything to read. A call id no worker has opened yet reads as an empty log to any key, so a backend that minted a token can tail the call it names before the caller arrives; a call id is random, and nothing is in it until its org's worker opens it, after which only its org reads it (404 to anyone else). An agent's own log reads the same way while no org has registered the slug, so pinecall logs can wait for a first start; once an org holds it, it is that org's alone.

endpoint
GET /v1/agents/{slug}/calls an agent's own log: registrations, declarations, errors
GET /v1/calls/{call}/state the call reduced
GET /v1/calls/{call}/recording the audio, seekable; a written call keeps none. It is kept sealed (../security/private-values.md) and answered as recorded
GET /v1/agents/{slug}/sessions · GET /v1/sessions one line per call, filtered, counted and paged, in the reader's scope
GET /v1/calls/{call}/settings the exact settings the call was built on
GET /v1/calls/{call}/prompt the exact prompt the call was told: {call, blocks: [{seq, name, hash, chars, text}]}, one per prompt.changed in order, each block's words as the org kept them under its hash the first time a gateway met them (a call's knowledge when it opened, an app's prompt.set), once per distinct text and never per call; text is null for a block nobody kept (a call from before they were kept). A key's read, never a page's token
GET /v1/events the org's floor as it changes, in the environment the key acts in: agents registered and detached, calls ringing, starting and ending, a person asked for and taken

What each reader receives is its projection: Projections.

Erasing#

The log is append-only: a trigger on call_log refuses every UPDATE, and every DELETE but the one path below, which sets pinecall.erasing in its own transaction. An erasure deletes, in one transaction, the log's entries and head, the private values sealed beside its entries, the call's facts and tokens, the memories the call taught, and the call's recording (its directory on the disk, its object in the recordings bucket when the platform has one, and the key it is sealed under); then it writes one row of the org's trail, {id, at, what, subject, env, asked_by, calls, entries, memories, recordings}. The dial ledger stays, and a phone call leaves its detail record in call_records — the numbers, the direction, when it started and ended, how it ended; no name, no words, no outcome — for a carrier's traceback, until the nightly run forgets it, and every dial, 24 months on. A backup of the database taken before the erasure still holds what was erased until the backup expires, which an answer to a data subject says.

A sealed log is the database's word too, not only the gateway's: call_log refuses an entry on a sealed log but its call.score (a judge scores a call again after its seal), and call_log_head refuses opening a sealed log again. An erasure deletes, so neither stands in its way.

endpoint
DELETE /v1/calls/{call} one ended call (team); 409 while it runs, 404 for a call the key does not read
DELETE /v1/contacts/{contact} a contact in the key's environment (team): every call they were on (by call_facts.contact), every fact kept of them, what each reader had read of their thread; 409 while they are on a call
GET /v1/org/erasures the trail, newest first (team)

Who read what#

Every read of what a call or a contact left writes one row of the org's access log, once an hour at most for the same reader, subject and kind; reader is the person's member id, or a server's key id (k_…):

what subject the endpoints
log · recording the call GET /v1/calls/{call}/events, /state, /prompt, /recording
listen · supervise the call a seat handed out, POST /v1/calls/{call}/listen, /supervise
export the org GET /v1/org/export
memory the contact, the agent, or the org GET /v1/contacts/{contact}/memory, /v1/agents/{slug}/memory, /v1/memory

A visitor's token reading its own call and the fleet reading the call it serves write none. The operator's reads off the platform (pinecall-runtime sessions show|tail|recording) and its tracebacks (pinecall-runtime traceback, GET /v1/ops/traceback, one row in each org the lookup showed, what: traceback, the number as subject) write rows with reader: "operator". A row names what was read, never what it said. The row is one statement the read waits on; one that cannot be written is said in the gateway's journal and the read is answered all the same. GET /v1/org/reads?subject= (team) answers {reads: [{subject, what, env, reader, at}]}, newest first, of one subject when named.

An org is erased whole by the operator: DELETE /v1/ops/orgs/{named} (Operator API). The trail row outlives the org.

4. Watching and steering a live call#

POST /v1/calls/{call}/listen and /supervise (supervise) mint a seat, a LiveKit token for one call: listening is hidden and silent; supervising publishes a microphone and sends verbs. POST /v1/calls/{call}/verbs takes one verb, say, whisper, takeover, release, transfer or end, with the seat or the org's key as the bearer, and answers 202; each lands in the log as its own supervisor.* entry. A call with no room is steered the same way, less transfer. Where a ring lands, a developer's own phone and the agent's line: PUT·DELETE /v1/line/from, GET /v1/line/numbers, GET·POST·DELETE /v1/agents/{slug}/line.

5. What an agent runs on, knows and remembers#

An agent's settings and its lexicon: Settings. The pipeline and its hold melody: Pipeline. The knowledge bases and a contact's memory, and when a call looks either up: How retrieval works. The vendors, the org's own keys, the voices: Provider keys. The widget: Console API. An agent the platform runs itself, from sources the org uploads, and the org's secrets: Hosted apps.

6. Evals#

The goldens, the replay and the judges, the simulated callers: Evals.

7. The org#

People, keys, sign-in, sign-up, the org's provider and mailbox: People. Numbers and carriers: Numbers. Usage, insights, limits and judging: Console API.

The org's compliance settings are one row, replaced whole (team): GET /v1/org/policy answers {policy: {retention_days, calling_hours: {from, until} | null, per_number_day, consent_everywhere, disclosure, recording_notice}, set_by, set_at} and PUT /v1/org/policy takes the policy. retention_days is how many days a sealed call is kept before the nightly run erases it through the erasure path (§3); null keeps everything, which is also an org nobody set. calling_hours and per_number_day are the org's outbound calling rules, applied by destination at the dial (Numbers): a +1 number keeps the US hours and three calls a day whatever the org sets wider, and a consent on file; consent_everywhere asks for one for every country. disclosure and recording_notice are what a spoken call says before its greeting, spoken by the agent's voice and logged as its first turn.agent: an outbound call opens with disclosure — null is the platform's sentence in the agent's language ("This is an automated assistant calling on behalf of {org name}."), "" is none, anything else is said as written — and every recorded spoken call, either direction, then says "This call may be recorded." unless recording_notice is false. A call nobody records says no notice; a written call says neither. Both play in the sandbox too, so a test hears what a caller will. A client that changes one field reads the row and writes it back whole. The trail is GET /v1/org/erasures.

GET /v1/org/export (team) is the org's calls, memories, settings, words, documents and consents in the key's environment, as a download of JSON Lines (application/x-ndjson): a header {kind: "export", org, env, exported_at}, then one line per call ({kind: "call", call, agent, holder, started_at, sealed, facts, entries}, oldest first, each with its whole log), then every memory (kind: "memory", without its embedding), every version of every agent's settings (agent_config) and words (lexicon), and every document of its knowledge bases (knowledge_file), and every fact about a number (consent). A recording is not inlined: GET /v1/calls/{call}/recording.