Invoke API
POST /api/invoke/{id} is BasePeak.AI's native way to run an agent. Unlike
the OpenAI-compatible surface, it exposes the agent's
full event stream: tool calls, step transitions, OAuth prompts and
errors, not only the assistant's text.
Use it when you want to show what the agent is doing, or when you need a long-running turn that does not hold a connection open.
Request
POST /api/invoke/{id} HTTP/1.1
Authorization: Bearer sk-bpai-1-42-…
Accept: text/event-stream
Content-Type: text/plain
Summarise the Q3 report and list the three biggest risks.
The body is plain text, not JSON
This is the most common mistake when moving from the OpenAI surface. The
request body is used verbatim as the agent's prompt. There is no
messages array, no JSON envelope, no model field.
If you send {"messages":[…]} here, the agent receives that JSON as the
literal text of your question.
Path parameter {id}
{id} selects what to run and accepts three forms:
| Form | Example | Behavior |
|---|---|---|
| Agent ID | a17jlm7 | Runs that agent. |
| Agent alias | support-bot | Resolved to the agent behind the alias. |
| Thread ID | t1abcde | Runs the agent that owns the thread, continuing that thread. |
The required scope is agent:chat:<the value in the path>. This differs
from /v1/chat/completions, which derives it from the X-BPAI-Agent
header.
So a key scoped to agent:chat:a17jlm7 can call /api/invoke/a17jlm7,
but calling /api/invoke/t1abcde with a thread ID in the path
requires agent:chat:t1abcde or a wildcard agent:chat:* key. To
continue a thread with an agent-scoped key, keep the agent in the path and
pass the thread separately — see Selecting a thread.
Selecting a thread
Two routes carry the thread in the path, and there is also a header:
POST /api/invoke/{id}
POST /api/invoke/{id}/thread/{thread}
POST /api/invoke/{id}/threads/{thread}
| How | Precedence |
|---|---|
{thread} path segment | Wins if present. |
X-BPAI-Thread-Id header | Used when the path carries no thread. |
| Neither | A new thread is created. |
The response always carries X-BPAI-Thread-Id, for new and resumed
threads alike. Record it — that remains the most direct way to continue the
conversation. See Threads.
Headers
| Header | Required | Purpose |
|---|---|---|
Authorization | yes | Bearer sk-bpai-<user>-<key>-<secret> |
Accept | no | Selects the response format — see below. |
X-BPAI-Thread-Id | no | Resume a thread. |
X-BPAI-Scoped-Tools | no | Restrict the run to a subset of the agent's tools. |
Query parameters
| Parameter | Default | Meaning |
|---|---|---|
async | false | true returns immediately with IDs instead of streaming. |
Choosing a response format
For a synchronous call the Accept header — and nothing else — decides
what comes back:
Accept | Response |
|---|---|
text/event-stream | SSE stream of events, live as the run progresses. |
application/json | One JSON envelope {"items":[…]} after the run finishes. |
| anything else, or absent | Plain text: the content of each event, concatenated, streamed. |
Accept is matched exactly, so */* breaks streamingThe header value is compared literally against the whole Accept header.
Accept: text/event-stream works. Accept: text/event-stream, */* does
not — it falls through to the plain-text branch.
Many HTTP clients append */* or send it by default. If you expect SSE
and get an unframed text blob, this is why. Send the bare value:
curl -H 'Accept: text/event-stream' … # correct
curl -H 'Accept: text/event-stream, */*' … # silently NOT SSE
The same applies to application/json.
The plain-text form flushes at most every 500 ms, so it streams but in small batches rather than token-by-token.
Server-sent events
With Accept: text/event-stream the stream is framed as follows.
1. Stream opens immediately, before the agent produces anything:
event: start
data: {}
2. Each event carries the run ID as the SSE id field, then the
event object as data:
id: r1xyz
data: {"runID":"r1xyz","threadID":"t1abcde","content":"The Q3 report "}
3. The final event of a run uses a distinct id suffix — :after —
alongside runComplete:
id: r1xyz:after
data: {"runID":"r1xyz","runComplete":true,"content":""}
4. Keepalives appear as SSE comments whenever the run goes quiet for 20 seconds:
: ping
This exists because a slow first token or a long tool call would otherwise
leave the socket idle until an intermediate proxy closes it. Comment lines
are dropped automatically by
EventSource and every conformant SSE parser — you only need to handle
them if you parse the stream by hand.
5. Stream closes:
event: close
data: {}
Treat a missing event: close as an abnormal end.
The event object
Every data: frame is a JSON object. The fields you will actually use:
| Field | Meaning |
|---|---|
content | Output text. Concatenate this across events to build the answer. |
contentID | Identifies a content run, so repeated fragments can be tracked. |
runID / threadID | Which run and thread the event belongs to. |
runComplete | true on the last event of the run. |
error | Set when the run failed. |
time | When the event was generated. |
Richer fields, at most one of which is set per event:
| Field | Meaning |
|---|---|
waitingOnModel | The model has not started responding yet — good for a spinner. |
toolInput | The model is composing tool arguments (this can be slow). |
toolCall | A tool is being invoked. |
step | The current step changed; following events belong to it. |
prompt | The agent needs something from the user — most often an OAuth login. Carries fields, message and a sensitive flag. |
input | Input that was supplied to the run. |
replayComplete | All pre-existing events have been delivered; what follows is live. |
username | Who triggered the run. |
The simple integration is genuinely simple: if none of the richer fields
is set, just print content. Handle the others only when you want to
surface progress.
Fire-and-forget with async
curl -sS -X POST \
'https://your-instance.platform.basepeak.ai/api/invoke/a17jlm7?async=true' \
-H "Authorization: Bearer sk-bpai-1-42-…" \
--data 'Produce the monthly report and email it to the team.'
Returns at once:
{
"threadID": "t1abcde",
"runID": "r1xyz"
}
The run continues server-side. This is the right choice for work that outlives an HTTP timeout.
An async invoke is not one-way: with a conversation-read scope —
thread:read for the agent, or project:threads for the conversation's
project — GET /api/threads/{id}/events delivers a run's events after the
fact, and POST /api/threads/{id}/abort cancels it (covered by agent:chat).
Pass the runID from the response above. Without it the route returns the
thread's last completed run — on a resumed thread, the previous turn's
answer.
curl -sS -H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Accept: application/json" \
'https://your-instance.platform.basepeak.ai/api/threads/t1abcde/events?runID=r1xyz'
With Accept: application/json this call stays open until the run finishes. For
runs that outlast an HTTP timeout, poll GET /api/threads/{id} instead — it
answers immediately and reports completion through lastRunID.
Details and both options in full: Threads.
Restricting the agent's tools
X-BPAI-Scoped-Tools limits a single run to a subset of the agent's
configured tools, without changing the agent:
X-BPAI-Scoped-Tools: google-calendar, gmail
Comma-separated tool references; whitespace is trimmed and blanks dropped. Omit the header to inherit the agent's tools — subject to the key's credential scopes, which can strip credential-backed tools from the run. See Credential scopes.
Useful for making one integration narrower than the agent it calls — a status-page bot that may read the calendar but not send mail, for instance.
Chatting inside a project
POST /api/invoke/{id} always talks to the bare agent — no shared
workspace, none of its tasks as tools, no project-scoped credentials or
model settings. That is the thinner surface, and deliberately so: correct
for headless agents and system agents that belong to no project, but not the
conversation the browser runs.
For that, there is a matching route pair addressed by project:
POST /api/projects/{project_id}/invoke
POST /api/projects/{project_id}/invoke/thread/{thread_id}
Scope project:chat:<project_id> instead of agent:chat:<id>; otherwise
identical behavior — same response body, same X-BPAI-Thread-Id header, same
Accept variants, same ?async=true. The agent comes from the project — you
never name one:
curl -sS -X POST \
https://your-instance.platform.basepeak.ai/api/projects/p1abc/invoke \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Accept: application/json" \
-H "Content-Type: text/plain" \
--data 'Summarise the files in this project.'
Full details, the scope table, and the other project routes (files, knowledge, tasks, environment variables): Projects.
Errors
Errors on this surface use the platform's problem format, not the OpenAI error envelope:
| Status | When |
|---|---|
400 | {id} resolves to no agent. |
401 | Missing, malformed, unknown or expired bearer. |
403 | The key's scopes do not cover the requested agent; or the key's owner has no access to that agent; or API access is disabled for the instance. |
404 | The agent, alias or thread does not exist. |
500 | Server-side failure resolving the agent or starting the run. |
A wildcard agent:chat:* key is re-checked against its owner's own
permissions, so it cannot reach agents its owner could not open in the
browser.
Once the SSE stream has started, a failure arrives as an event with
error set rather than as an HTTP status — the status line was already
sent. If the client disconnects, the server notices on the next write and
cancels the run.
Examples
curl — streaming
curl -N -sS -X POST \
https://your-instance.platform.basepeak.ai/api/invoke/a17jlm7 \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Accept: text/event-stream" \
-H "Content-Type: text/plain" \
--data 'Give me three bullet points on the Q3 numbers.'
event: start
data: {}
id: r1xyz
data: {"runID":"r1xyz","threadID":"t1abcde","waitingOnModel":true,"content":""}
id: r1xyz
data: {"runID":"r1xyz","content":"- Revenue up 12%"}
: ping
id: r1xyz:after
data: {"runID":"r1xyz","runComplete":true,"content":""}
event: close
data: {}
Python — stream and print text
import httpx
BASE = "https://your-instance.platform.basepeak.ai"
TOKEN = "sk-bpai-1-42-…"
AGENT = "a17jlm7"
with httpx.stream(
"POST", f"{BASE}/api/invoke/{AGENT}",
headers={
"Authorization": f"Bearer {TOKEN}",
# Exactly this value — appending */* silently disables SSE.
"Accept": "text/event-stream",
"Content-Type": "text/plain",
},
content="Summarise today's support tickets.",
timeout=None,
) as r:
r.raise_for_status()
thread = r.headers["x-bpai-thread-id"] # keep this to continue later
import json
for line in r.iter_lines():
if not line.startswith("data: "):
continue # skips ': ping' and 'id:'/'event:'
event = json.loads(line[6:])
if event.get("error"):
raise RuntimeError(event["error"])
if text := event.get("content"):
print(text, end="", flush=True)
if event.get("runComplete"):
break
print(f"\n\nthread: {thread}")
Python — show tool activity
for line in r.iter_lines():
if not line.startswith("data: "):
continue
e = json.loads(line[6:])
if e.get("waitingOnModel"):
print("[thinking…]")
elif tc := e.get("toolCall"):
print(f"[calling {tc.get('name', 'tool')}]")
elif st := e.get("step"):
print(f"[step: {st.get('description', '')}]")
elif p := e.get("prompt"):
# Usually an OAuth consent the agent needs before it can continue.
print(f"[action needed: {p.get('message', '')}]")
elif txt := e.get("content"):
print(txt, end="", flush=True)
Node — collect the whole answer as JSON
Ask for one envelope instead of a stream when you only want the result:
const res = await fetch(`${BASE}/api/invoke/${AGENT}`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
Accept: "application/json", // exactly this — no ', */*'
"Content-Type": "text/plain",
},
body: "What changed in the roadmap this week?",
});
const thread = res.headers.get("x-bpai-thread-id");
const { items } = await res.json();
const answer = items.map((e) => e.content ?? "").join("");
console.log(answer, "\nthread:", thread);
Related docs
- Threads — running several conversations
- Files — giving the agent documents
- OpenAI-compatible API — the drop-in alternative
- API Keys — scopes and tokens
- Projects — chatting inside a project