Skip to content

Public API

The public API lets your own servers call your agents. It’s separate from the dashboard: its own keys, its own docs, its own limits.

  • Base URL: https://api.eu.backcrew.ai/v1 for workspaces hosted in Europe, the examples below use it. Each regional deployment of backcrew has its own address, and the snippets on an agent’s Embed page always show your workspace’s.
  • Interactive reference: https://api.eu.backcrew.ai/v1/docs

Create a secret key in Settings → API Keys. It starts with bk_live_ and is shown once - store it somewhere safe. Send it on every request:

Authorization: Bearer bk_live_...

Each key also comes with a signing secret (bks_...), shown once next to it. You only need it for callbacks: it lets your server check that a result really came from backcrew. Lost it, or never had one? Choose New signing secret on the key. The old secret stops matching at once.

Terminal window
curl https://api.eu.backcrew.ai/v1/agents \
-H "Authorization: Bearer $BACKCREW_API_KEY"
[{ "id": "7c9f…", "name": "support-bot", "status": "deployed" }]

Only deployed agents can answer.

POST /v1/agents/{agent_id}/invoke

Field Required Meaning
prompt yes The message.
session_id no Groups messages into one conversation.
user_id no Your own id for the person you’re acting for. With memory on, each user_id gets its own memory.
Terminal window
curl -X POST https://api.eu.backcrew.ai/v1/agents/$AGENT_ID/invoke \
-H "Authorization: Bearer $BACKCREW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Where is my order?", "user_id": "customer-123", "session_id": "conversation-1"}'
{
"response": "Your order shipped yesterday and should arrive on Thursday.",
"usage": { "input_tokens": 1240, "output_tokens": 310, "credits_charged": 5, "unmetered": [] }
}

The full reply comes back once the agent finishes - responses aren’t streamed.

A request that takes more than 100 seconds can time out before the reply arrives. If your agent may take longer, start a run instead.

import os, requests
res = requests.post(
f"https://api.eu.backcrew.ai/v1/agents/{os.environ['AGENT_ID']}/invoke",
headers={"Authorization": f"Bearer {os.environ['BACKCREW_API_KEY']}"},
json={"prompt": "Where is my order?", "user_id": "customer-123"},
timeout=120,
)
res.raise_for_status()
print(res.json()["response"])
const res = await fetch(`https://api.eu.backcrew.ai/v1/agents/${agentId}/invoke`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BACKCREW_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "Where is my order?", user_id: "customer-123" }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log((await res.json()).response);

An agent can work for up to 5 minutes, but a single request that stays open for more than 100 seconds can be cut off on its way back. A run avoids that: you start it, backcrew answers at once, and the agent works in the background. Then either poll the run, or give a callback_url and backcrew sends you the result when it’s done.

Runs cost and are checked exactly like invoke: an agent that isn’t deployed (409), no credits or a reached monthly limit (402) are refused when you start the run, and nothing is charged. Runs are kept for an hour.

POST /v1/agents/{agent_id}/runs takes the same fields as invoke, plus an optional callback_url, and answers 202:

Terminal window
curl -X POST https://api.eu.backcrew.ai/v1/agents/$AGENT_ID/runs \
-H "Authorization: Bearer $BACKCREW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Reconcile the October invoices", "user_id": "finance-team"}'
{ "id": "3f1c…", "agent_id": "7c9f…", "status": "running", "started_at": "2026-10-22T09:00:00+00:00",
"finished_at": null, "response": null, "usage": null, "error": null }

GET /v1/agents/{agent_id}/runs/{run_id} returns the same object. status is one of:

Status Meaning
running The agent is still working. Ask again in a few seconds.
answered Done: response and usage are filled in.
failed It didn’t answer: error says why (for example, out of credits or the agent’s code failed).
lost It stopped without an answer. Check the agent’s activity, then try again.

Polling counts towards your key’s rate limit, so every 2 to 5 seconds is plenty. A run id from another workspace or another agent is a 404.

import os, time, requests
API = f"https://api.eu.backcrew.ai/v1/agents/{os.environ['AGENT_ID']}"
HEADERS = {"Authorization": f"Bearer {os.environ['BACKCREW_API_KEY']}"}
run = requests.post(f"{API}/runs", headers=HEADERS, json={"prompt": "Reconcile the October invoices"}, timeout=30)
run.raise_for_status()
run = run.json()
while run["status"] == "running":
time.sleep(3)
run = requests.get(f"{API}/runs/{run['id']}", headers=HEADERS, timeout=30).json()
if run["status"] == "answered":
print(run["response"])
else:
print(f"{run['status']}: {run['error']}")

Add "callback_url": "https://your-server.example.com/backcrew" when you start the run. When it ends (answered or failed), backcrew sends a POST there with the run object as its JSON body. Answer with any 2xx. A delivery that gets an error, a non-2xx answer or no answer within 10 seconds is tried again after 5 and then 30 seconds: 3 times in all. Redirects aren’t followed.

The address must be https:// and reach the public internet: private, local and reserved addresses are refused with 422 when you start the run.

Every delivery carries a signature made with your key’s signing secret:

X-Backcrew-Signature: t=1792659600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is the HMAC-SHA256 of the timestamp, a full stop and the raw request body ("{t}.{body}"), as hex. Check it before trusting the body, against the raw bytes you received (not re-encoded JSON), and refuse an old timestamp so a captured delivery can’t be replayed:

import hashlib, hmac, os, time
def verify(body: bytes, header: str, secret: str = os.environ["BACKCREW_SIGNING_SECRET"]) -> bool:
try:
fields = dict(part.split("=", 1) for part in header.split(","))
timestamp = int(fields["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > 300: # older than 5 minutes
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, fields.get("v1", ""))
# In your web framework, e.g. Flask:
# if not verify(request.get_data(), request.headers.get("X-Backcrew-Signature", "")): abort(401)
import crypto from "node:crypto";
// `body` is the raw request body as a Buffer, e.g. express.raw({ type: "application/json" }).
function verify(body, header, secret = process.env.BACKCREW_SIGNING_SECRET) {
const fields = Object.fromEntries((header || "").split(",").map((part) => part.split("=", 2)));
const timestamp = Number(fields.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(body).digest("hex");
const given = Buffer.from(fields.v1 || "", "utf8");
return given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected, "utf8"));
}

A run’s result can arrive more than once (for example, if your server answered too slowly), so use the run id to handle each one once. A key revoked before the run ends gets no callback, and neither does a lost run: poll if you never hear back.

Status Meaning
401 Missing, unknown or revoked key.
402 The workspace is out of credits (or past its monthly limit).
404 No agent with that id in this key’s workspace, or no such run (runs are kept for an hour).
409 The agent isn’t deployed right now (e.g. it’s updating).
422 The callback_url isn’t a public https:// address, or the key has no signing secret yet.
429 Rate limit reached - wait for the Retry-After seconds.
502 The agent failed to answer.
504 The agent took longer than the maximum request time (5 minutes).
  • 60 requests a minute per key, counting starting and polling runs.
  • Each call costs credits exactly like a request from the dashboard - see Credits.