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/v1for 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
Authentication
Section titled “Authentication”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.
List your agents
Section titled “List your agents”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.
Send a message
Section titled “Send a message”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. |
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.
Python
Section titled “Python”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"])JavaScript
Section titled “JavaScript”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);Long requests: runs and callbacks
Section titled “Long requests: runs and callbacks”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.
Start a run
Section titled “Start a run”POST /v1/agents/{agent_id}/runs takes the same fields as invoke, plus an optional callback_url, and answers 202:
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 }Poll it
Section titled “Poll it”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']}")Get the result sent to you
Section titled “Get the result sent to you”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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 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.
Errors
Section titled “Errors”| 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). |
Limits and cost
Section titled “Limits and cost”- 60 requests a minute per key, counting starting and polling runs.
- Each call costs credits exactly like a request from the dashboard - see Credits.