Agent Tool API
A metered HTTP API built for autonomous agents. Mint a key, discover the catalog, call tools. No human in the loop except for side-effecting actions, which require an explicit per-call confirmation header.
Base URL
https://3bi.ai/api/public/v1
Quickstart in four calls
The whole loop, copy-pasteable. Every step below is documented in detail further down this page.
# 1. mint a key + 500 free credits (no auth)
curl -X POST https://3bi.ai/api/public/v1/signup -H "content-type: application/json" -d '{"label":"my agent"}'
# 2. discover the catalog (typed JSON Schema per tool)
curl https://3bi.ai/api/public/v1/tools
# 3. call a live read-only tool (2 credits)
curl -X POST https://3bi.ai/api/public/v1/tools/fetch_url \
-H "Authorization: Bearer $RELAY_KEY" -H "content-type: application/json" \
-d '{"url":"https://example.com"}'
# 4a. call a side-effecting tool — returns 428 with a preview + confirmationToken
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_send_email \
-H "Authorization: Bearer $RELAY_KEY" -H "content-type: application/json" \
-d '{"to":"ops@example.com","subject":"hi","body":"hello"}'
# 4b. human approves the preview, then resend the IDENTICAL body with the token
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_send_email \
-H "Authorization: Bearer $RELAY_KEY" -H "content-type: application/json" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" -H "idempotency-key: $(uuidgen)" \
-d '{"to":"ops@example.com","subject":"hi","body":"hello"}'0. Agents sign themselves up
No browser, no password, no email verification loop. One unauthenticated POST creates a workspace, returns an API key (shown once) and grants 500 free credits.
curl -X POST https://3bi.ai/api/public/v1/signup \
-H "content-type: application/json" \
-d '{"label":"my agent"}'{
"ok": true,
"orgId": "…",
"apiKey": "sk_agent_xxxxxxxx_…",
"credits": { "granted": 500, "balance": 500 },
"claim": { "url": "https://3bi.ai/claim?token=…", "expiresAt": "…" }
}Signup is limited to 3 workspaces per address per 24h (429 signup_rate_limited). When credits run out, call POST /api/public/v1/claim for a fresh one-hour claim URL and hand it to your human operator: they sign in, take ownership of the workspace and buy credits. The agent's key keeps working throughout. Rotate a key with POST /api/public/v1/keys/rotate — the old key stays valid for 10 more minutes. Machine-readable version of this page: https://3bi.ai/llms.txt.
0b. Agents pay for themselves (x402)
When the balance runs out, the 402 is machine-payable. The body follows the x402 spec: settle accepts[0] in USDC on Base, then retry the same request with an X-PAYMENT header. Credits are added and the call executes in that one retry.
{
"x402Version": 1,
"error": "insufficient_credits",
"accepts": [{
"scheme": "exact",
"network": "base",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"maxAmountRequired": "900000",
"payTo": "0x…",
"resource": "https://3bi.ai/api/public/v1/tools/crm.update_contact",
"maxTimeoutSeconds": 120
}],
"payment": { "credits": 100, "amountUsd": 0.9, "asset": "USDC", "network": "base" }
}To top up ahead of time instead of on the failing call:
curl -X POST https://3bi.ai/api/public/v1/credits/purchase \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-d '{"credits":1000}' # → 402 with the offer, then retry with X-PAYMENTPayments are credited once per settled transaction, so a replayed X-PAYMENT never double-charges. If on-chain payment is not available to you, fall back to POST /api/public/v1/claim and a human buys credits with a card.
0c. Spend guardrails
Every key can carry owner-set limits, enforced server-side before a tool runs: max credits per call, a rolling 24-hour cap, a lifetime cap, an expiry date and a tool allowlist. Exceeding one returns 403 budget_exceeded, 403 tool_not_allowed or 401 key_expired with the limit and spend-to-date in the body — nothing is executed or charged. Configure them per key in the console.
1. Discovery
Machine-readable entry points. All are unauthenticated so an agent can plan — and price the job — before it has a key.
GET /api/public/v1/tools # catalog: JSON Schemas, credits and usdPerCall GET /api/public/v1/pricing # USD per credit, per tool and per pack GET /api/public/v1/openapi.json # OpenAPI 3.1 document GET /.well-known/agents.json # agent-native discovery document GET /.well-known/ai-plugin.json # OpenAI-style plugin manifest GET /.well-known/agent-manifest.json # legacy alias of agents.json
2. Authentication
Every call carries a workspace key as a bearer token. Keys are shown once at creation and stored hashed; revoke them any time from the console. Invoking tools requires the tools:invoke scope. New workspaces start with 500 free credits.
Authorization: Bearer sk_agent_xxxxxxxx_... # fallback for clients that cannot set Authorization: x-api-key: sk_agent_xxxxxxxx_...
3. Invoke a tool
curl -X POST https://3bi.ai/api/public/v1/tools/fetch_url \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: 9f1c-run-42" \
-d '{"url":"https://example.com"}'Arguments are validated against the tool's JSON Schema. Invalid input returns 422 invalid_input with the failing field paths, and no credits are charged. A successful call responds with:
{
"ok": true,
"requestId": "b0e1…",
"tool": "fetch_url",
"demo": false,
"credits": { "charged": 2, "balance": 498 },
"result": { … }
}Responses also carry x-request-id and x-credits-charged headers. Fetch a single tool's schema without a key with GET /api/public/v1/tools/{name}. CORS is open, so browser-based agents can call the API directly.
4. Side effects require confirmation
Tools that send email, write to a CRM, create payments or delete records return 428 confirmation_required with a preview of the exact arguments that would run, the credit cost, and a single-use confirmationToken. Show the preview to your operator; once they approve, resend the identical body with the token to execute. The rejected attempt is logged in your usage history and charges nothing.
-H "x-confirmation-token: $CONFIRMATION_TOKEN"
The token is bound to the exact arguments that were previewed, is valid for 10 minutes and can be redeemed once. Changing any field returns 409 confirmation_mismatch; reusing a token returns 409 confirmation_used; letting it lapse returns 410 confirmation_expired. There is no header an agent can set on its first call to skip the preview — that is the point.
5. Idempotency
Send an idempotency-key header on any call. The key is scoped to your API key. Repeating a completed call with the same key returns the original stored response with "replayed": true and is not charged again — safe for agent retry loops and timeouts. If the first call is still running, the retry gets 409 request_in_progress; failed calls release the key so you can retry.
6. Rate limits
60 calls per minute per API key, counted over a rolling window. Over the limit you get 429 rate_limited. Mint additional keys for parallel workers.
7. Error code reference
Every failure uses the same envelope: { "ok": false, "error": { "code", "message", … } }. This table is the single reference for the whole API — the OpenAPI document is generated from it.
| Status | Code | Cause | What to do | Retry |
|---|---|---|---|---|
| 401 | missing_api_key | No Authorization: Bearer header on the request. | Send Authorization: Bearer sk_agent_… , or POST /api/public/v1/signup to mint a key. | no |
| 401 | invalid_api_key | The key is unknown, revoked, or past its rotation grace window. | Rotate with POST /api/public/v1/keys/rotate, or create a new key in the console. | no |
| 402 | insufficient_credits+ required, balance, accepts, payment | The workspace balance is below this tool's price. Nothing was executed or charged. | Pay machine-to-machine: the body carries an x402 accepts[] offer — settle it and retry the same request with an X-PAYMENT header. Humans can instead buy credits via POST /api/public/v1/claim. | no |
| 402 | payment_failed+ required, balance | An X-PAYMENT header was supplied but the facilitator could not verify or settle it. | Re-read accepts[0] from a fresh 402, rebuild the payment payload for the exact amount, asset and network, and retry. | no |
| 401 | key_expired+ expiredAt | The key passed the expiry date set by its owner. | Rotate the key or issue a new one in the console. | no |
| 403 | budget_exceeded+ spent, required, limit, window | The call would exceed a per-call, 24-hour, or lifetime credit cap set on this key. | Wait for the window to roll over, or ask the key owner to raise the cap in the console. | no |
| 403 | tool_not_allowed+ allowedTools | This key has a tool allowlist that does not include the requested tool. | Call an allowed tool, or ask the key owner to widen the allowlist. | no |
| 403 | tool_disabled | The workspace owner has disabled this tool for the org. | Enable the tool in the console, or call a different tool. | no |
| 403 | insufficient_scope | The key is valid but does not carry the tools:invoke scope. | Issue a key with tools:invoke from the console and retry with it. | no |
| 404 | unknown_tool | No public tool with that name exists in the catalog. | Re-read GET /api/public/v1/tools and use an exact name from the catalog. | no |
| 409 | request_in_progress | Another call with the same idempotency-key is still executing, or an approved confirmation token is already running. | Wait and retry the same idempotency-key to receive the stored response; do not change the body. If the original request is known to have failed, retry with a new idempotency-key. For a confirmation token, call the tool again with no token to get a fresh preview. | yes |
| 422 | invalid_json | The request body was not valid JSON. | Send a JSON object with content-type: application/json. | no |
| 422 | invalid_input+ issues | Arguments failed the tool's JSON Schema. No credits were charged. | Fix the fields listed in error.issues[].path and resend. | no |
| 428 | confirmation_required+ tool, credits, preview, confirmationToken, expiresAt | A side-effecting tool was called without a confirmation token. Nothing was executed or charged. | Show error.preview to your operator. Once they approve, repeat the identical request with header x-confirmation-token: <error.confirmationToken>. | yes |
| 403 | confirmation_invalid | The x-confirmation-token header is unknown or belongs to another workspace. | Call the tool without a token to receive a fresh preview and token, then retry. | no |
| 409 | confirmation_mismatch | The token is bound to a specific tool and argument set; this request differs from what was previewed. | Send the exact body that was previewed, or request a new confirmation for the new body. | no |
| 409 | confirmation_used | That confirmation token was already redeemed. Tokens are single-use. | Request a new confirmation for the next call. | no |
| 410 | confirmation_expired | The confirmation token passed its 10-minute validity window. | Call again without a token to get a new preview and token. | yes |
| 429 | rate_limited | Over 60 calls per minute for this API key. | Back off ~1 minute, or mint additional keys for parallel workers. | yes |
| 429 | signup_rate_limited | More than 3 workspaces created from this address in 24 hours (signup only). | Reuse the key you already have, or have your operator claim an existing workspace. | no |
| 502 | tool_failed | The upstream tool threw while executing. Reserved credits are refunded automatically. | Retry with backoff; if it persists the integration is down — check status and report. | yes |
| 503 | metering_unavailable | Credit metering could not be reached, so the call was not authorized or charged. | Retry with backoff; the same idempotency-key is safe to reuse. | yes |
| 404 | approval_intent_not_found | No approval intent with that id exists in this workspace. | Use the intent_id from the 428 approval object; intents expire after 24 hours. | no |
| 409 | oauth_connection_required+ provider, connect | This tool needs a connected third-party account, and none is linked for this workspace. | POST the connect URL from the body with your API key to get an authorization URL, have your operator complete the OAuth flow, then retry the call. | no |
| 404 | oauth_provider_unknown | No OAuth provider with that slug is enabled. | GET /api/public/v1/oauth/providers for the supported catalog. | no |
| 502 | oauth_callback_failed | The OAuth provider refused the code exchange or the profile lookup. | Restart the connect flow; if it persists the provider app credentials may be misconfigured. | no |
| 502 | oauth_provider_not_configured | The OAuth provider is enabled in the catalog but its app credentials are missing on RELAY. | The workspace owner should contact support; agents cannot fix this themselves. | no |
| 500 | oauth_authorize_failed | The OAuth authorization URL could not be generated. | Retry; if it persists, check that the provider is still enabled. | yes |
| 500 | catalog_unavailable | The OAuth provider catalog could not be loaded. | Retry shortly. | yes |
| 500 | connections_unavailable | The workspace's OAuth connections could not be listed. | Retry shortly. | yes |
| 404 | connection_not_found | No OAuth connection with that id exists in this workspace. | List connections with GET /api/public/v1/oauth/connections and use a current id. | no |
| 500 | revoke_failed | The OAuth connection could not be revoked. | Retry; the connection remains active until revocation succeeds. | yes |
{"ok":false,"error":{"code":"insufficient_credits","message":"Not enough credits for this call","required":5,"balance":2}}8. Try a tool
Pick a tool, edit the example body, and send it live from your browser. Leave the key blank to see the real 401, or flip the confirmation switch off on a side-effecting tool to see the 428 preview payload.
Fetch a public web page or JSON endpoint over HTTPS and return its readable text content plus metadata. Real network call, not a simulation. Read-only. Costs 2 credit(s).
curl -X POST https://3bi.ai/api/public/v1/tools/fetch_url \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{ "url": "https://example.com" }'Expected outputs for fetch_url
200 — success envelope
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "fetch_url",
"demo": false,
"credits": {
"charged": 2,
"balance": 498
},
"result": {
"ok": true,
"url": "https://example.com/",
"status": 200,
"contentType": "text/html",
"title": "Example Domain",
"text": "Example Domain. This domain is for use in illustrative examples in documents...",
"chars": 208,
"truncated": false,
"fetchedAt": "2026-08-09T20:15:18.358Z"
}
}- 401 missing_api_key — No Authorization: Bearer header on the request. Send Authorization: Bearer sk_agent_… , or POST /api/public/v1/signup to mint a key.
- 401 invalid_api_key — The key is unknown, revoked, or past its rotation grace window. Rotate with POST /api/public/v1/keys/rotate, or create a new key in the console.
- 402 insufficient_credits — The workspace balance is below this tool's price. Nothing was executed or charged. Pay machine-to-machine: the body carries an x402 accepts[] offer — settle it and retry the same request with an X-PAYMENT header. Humans can instead buy credits via POST /api/public/v1/claim.
- 402 payment_failed — An X-PAYMENT header was supplied but the facilitator could not verify or settle it. Re-read accepts[0] from a fresh 402, rebuild the payment payload for the exact amount, asset and network, and retry.
- 401 key_expired — The key passed the expiry date set by its owner. Rotate the key or issue a new one in the console.
- 403 budget_exceeded — The call would exceed a per-call, 24-hour, or lifetime credit cap set on this key. Wait for the window to roll over, or ask the key owner to raise the cap in the console.
- 403 tool_not_allowed — This key has a tool allowlist that does not include the requested tool. Call an allowed tool, or ask the key owner to widen the allowlist.
- 403 tool_disabled — The workspace owner has disabled this tool for the org. Enable the tool in the console, or call a different tool.
- 403 insufficient_scope — The key is valid but does not carry the tools:invoke scope. Issue a key with tools:invoke from the console and retry with it.
- 404 unknown_tool — No public tool with that name exists in the catalog. Re-read GET /api/public/v1/tools and use an exact name from the catalog.
- 409 request_in_progress — Another call with the same idempotency-key is still executing, or an approved confirmation token is already running. Wait and retry the same idempotency-key to receive the stored response; do not change the body. If the original request is known to have failed, retry with a new idempotency-key. For a confirmation token, call the tool again with no token to get a fresh preview.
- 422 invalid_json — The request body was not valid JSON. Send a JSON object with content-type: application/json.
- 422 invalid_input — Arguments failed the tool's JSON Schema. No credits were charged. Fix the fields listed in error.issues[].path and resend.
- 429 rate_limited — Over 60 calls per minute for this API key. Back off ~1 minute, or mint additional keys for parallel workers.
- 502 tool_failed — The upstream tool threw while executing. Reserved credits are refunded automatically. Retry with backoff; if it persists the integration is down — check status and report.
- 503 metering_unavailable — Credit metering could not be reached, so the call was not authorized or charged. Retry with backoff; the same idempotency-key is safe to reuse.
- 404 approval_intent_not_found — No approval intent with that id exists in this workspace. Use the intent_id from the 428 approval object; intents expire after 24 hours.
- 409 oauth_connection_required — This tool needs a connected third-party account, and none is linked for this workspace. POST the connect URL from the body with your API key to get an authorization URL, have your operator complete the OAuth flow, then retry the call.
- 404 oauth_provider_unknown — No OAuth provider with that slug is enabled. GET /api/public/v1/oauth/providers for the supported catalog.
- 502 oauth_callback_failed — The OAuth provider refused the code exchange or the profile lookup. Restart the connect flow; if it persists the provider app credentials may be misconfigured.
- 502 oauth_provider_not_configured — The OAuth provider is enabled in the catalog but its app credentials are missing on RELAY. The workspace owner should contact support; agents cannot fix this themselves.
- 500 oauth_authorize_failed — The OAuth authorization URL could not be generated. Retry; if it persists, check that the provider is still enabled.
- 500 catalog_unavailable — The OAuth provider catalog could not be loaded. Retry shortly.
- 500 connections_unavailable — The workspace's OAuth connections could not be listed. Retry shortly.
- 404 connection_not_found — No OAuth connection with that id exists in this workspace. List connections with GET /api/public/v1/oauth/connections and use a current id.
- 500 revoke_failed — The OAuth connection could not be revoked. Retry; the connection remains active until revocation succeeds.
9. Starter catalog, examples & credit prices
The catalog has two tiers. Live tools (fetch_url, crawl_site, extract_structured, search_web, search_knowledge_base, execute_code, browse_page) do real network, model, vector search, code execution and remote browser work, cost credits and return "demo": false. search_knowledge_base runs semantic search over documents uploaded to the workspace knowledge base. execute_code runs Python or JavaScript in an isolated E2B sandbox. browse_page renders a URL in a Browserbase cloud browser and returns the markdown text. Every sandbox_* tool is free (0 credits), returns fixture data with "demo": true and changes nothing — use them to rehearse auth, schemas, idempotency and the confirmation gate. Workspace owners can disable any tool in the console.
fetch_url2 cr
Fetch a public web page or JSON endpoint over HTTPS and return its readable text content plus metadata. Real network call, not a simulation. Read-only.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/fetch_url \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"url":"https://example.com"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "fetch_url",
"demo": false,
"credits": {
"charged": 2,
"balance": 498
},
"result": {
"ok": true,
"url": "https://example.com/",
"status": 200,
"contentType": "text/html",
"title": "Example Domain",
"text": "Example Domain. This domain is for use in illustrative examples in documents...",
"chars": 208,
"truncated": false,
"fetchedAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → fetch_url
arguments: {"url":"https://example.com"}
structuredContent: {"ok":true,"url":"https://example.com/","status":200,"contentType":"text/html","title":"Example Domain","text":"Example Domain. This domain is for use in illustrative examples in documents...","chars":208,"truncated":false,"fetchedAt":"2026-08-09T20:15:18.358Z","demo":false,"credits":{"charged":2,"balance":498}}crawl_site6 cr
Crawl a public site starting from one https:// URL and return readable text for that page plus up to nine same-origin pages it links to. Real network calls. Read-only.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/crawl_site \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"url":"https://example.com","maxPages":3}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "crawl_site",
"demo": false,
"credits": {
"charged": 6,
"balance": 494
},
"result": {
"ok": true,
"seed": "https://example.com",
"pageCount": 2,
"pages": [
{
"url": "https://example.com/",
"status": 200,
"title": "Example Domain",
"text": "Example Domain. This domain is for use in illustrative examples...",
"chars": 208
}
],
"crawledAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → crawl_site
arguments: {"url":"https://example.com","maxPages":3}
structuredContent: {"ok":true,"seed":"https://example.com","pageCount":2,"pages":[{"url":"https://example.com/","status":200,"title":"Example Domain","text":"Example Domain. This domain is for use in illustrative examples...","chars":208}],"crawledAt":"2026-08-09T20:15:18.358Z","demo":false,"credits":{"charged":6,"balance":494}}extract_structured8 cr
Extract named fields as JSON from a public URL or supplied text, using a server-side model. Returns one value per requested field, or null when absent. Read-only.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/extract_structured \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"url":"https://example.com","fields":["title","purpose"]}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "extract_structured",
"demo": false,
"credits": {
"charged": 8,
"balance": 492
},
"result": {
"ok": true,
"sourceUrl": "https://example.com/",
"fields": {
"title": "Example Domain",
"purpose": "Illustrative examples in documents"
},
"missing": [],
"extractedAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → extract_structured
arguments: {"url":"https://example.com","fields":["title","purpose"]}
structuredContent: {"ok":true,"sourceUrl":"https://example.com/","fields":{"title":"Example Domain","purpose":"Illustrative examples in documents"},"missing":[],"extractedAt":"2026-08-09T20:15:18.358Z","demo":false,"credits":{"charged":8,"balance":492}}search_web4 cr
Search the public web and return ranked results with titles, snippets, and source URLs. Uses a server-side search API. Read-only.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/search_web \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"query":"x402 payment protocol summary"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "search_web",
"demo": false,
"credits": {
"charged": 4,
"balance": 496
},
"result": {
"ok": true,
"query": "x402 payment protocol summary",
"results": [
{
"title": "x402 - Machine-payable HTTP",
"url": "https://x402.org",
"content": "x402 is a protocol for machine payments over HTTP using stablecoins...",
"score": 0.94
}
],
"answer": "x402 lets servers request on-chain payment by returning HTTP 402.",
"searchedAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → search_web
arguments: {"query":"x402 payment protocol summary"}
structuredContent: {"ok":true,"query":"x402 payment protocol summary","results":[{"title":"x402 - Machine-payable HTTP","url":"https://x402.org","content":"x402 is a protocol for machine payments over HTTP using stablecoins...","score":0.94}],"answer":"x402 lets servers request on-chain payment by returning HTTP 402.","searchedAt":"2026-08-09T20:15:18.358Z","demo":false,"credits":{"charged":4,"balance":496}}search_knowledge_base3 cr
Search this workspace's uploaded documents using semantic similarity. Returns the most relevant text chunks with document titles and source URLs. Read-only.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/search_knowledge_base \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"query":"refund policy"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "search_knowledge_base",
"demo": false,
"credits": {
"charged": 3,
"balance": 497
},
"result": {
"ok": true,
"matches": [
{
"documentId": "doc_...",
"chunkIndex": 0,
"title": "Refund policy",
"sourceUrl": null,
"content": "Refunds are issued within 14 days of purchase for annual plans...",
"similarity": 0.91
}
]
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → search_knowledge_base
arguments: {"query":"refund policy"}
structuredContent: {"ok":true,"matches":[{"documentId":"doc_...","chunkIndex":0,"title":"Refund policy","sourceUrl":null,"content":"Refunds are issued within 14 days of purchase for annual plans...","similarity":0.91}],"demo":false,"credits":{"charged":3,"balance":497}}execute_codeconfirm8 cr
Run Python or JavaScript code in a sandboxed E2B environment and return stdout, stderr, and the exit code. Files are ephemeral, but code can make external network requests with side effects. Requires the configured side-effect confirmation flow.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/execute_code \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"code":"print('hello')","language":"python"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "execute_code",
"demo": false,
"credits": {
"charged": 8,
"balance": 492
},
"result": {
"ok": true,
"language": "python",
"stdout": "hello\n",
"stderr": "",
"exitCode": 0,
"executedAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → execute_code
arguments: {"code":"print('hello')","language":"python"}
structuredContent: {"ok":true,"language":"python","stdout":"hello\n","stderr":"","exitCode":0,"executedAt":"2026-08-09T20:15:18.358Z","demo":false,"credits":{"charged":8,"balance":492}}browse_page10 cr
Open a URL in a remote Browserbase browser and return the rendered page as markdown text, including title, status code, and content type. Runs in Browserbase's cloud so it works from serverless runtimes.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/browse_page \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"url":"https://example.com"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "browse_page",
"demo": false,
"credits": {
"charged": 10,
"balance": 490
},
"result": {
"ok": true,
"url": "https://example.com/",
"statusCode": 200,
"contentType": "text/markdown",
"title": "Example Domain",
"text": "Example Domain\n\nThis domain is for use in illustrative examples...",
"finishedAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → browse_page
arguments: {"url":"https://example.com"}
structuredContent: {"ok":true,"url":"https://example.com/","statusCode":200,"contentType":"text/markdown","title":"Example Domain","text":"Example Domain\n\nThis domain is for use in illustrative examples...","finishedAt":"2026-08-09T20:15:18.358Z","demo":false,"credits":{"charged":10,"balance":490}}sandbox_search_knowledge_base0 cr
Sandbox: searches a fixed set of fixture documents and returns simulated matches. Free — use it to exercise the API, not for real knowledge.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_search_knowledge_base \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"query":"refund policy"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "sandbox_search_knowledge_base",
"demo": true,
"credits": {
"charged": 0,
"balance": 500
},
"result": {
"ok": true,
"matches": [
{
"title": "Refund policy",
"body": "Refunds are issued within 14 days of purchase for annual plans, pro-rated after that."
}
]
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → sandbox_search_knowledge_base
arguments: {"query":"refund policy"}
structuredContent: {"ok":true,"matches":[{"title":"Refund policy","body":"Refunds are issued within 14 days of purchase for annual plans, pro-rated after that."}],"demo":true,"credits":{"charged":0,"balance":500}}sandbox_lookup_crm_contact0 cr
Sandbox: returns a fixture CRM contact by email. Free — no real CRM is queried.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_lookup_crm_contact \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"email":"dana@northwind.io"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "sandbox_lookup_crm_contact",
"demo": true,
"credits": {
"charged": 0,
"balance": 500
},
"result": {
"ok": true,
"contact": {
"id": "c_1024",
"name": "Dana Whitfield",
"email": "dana@northwind.io",
"company": "Northwind",
"stage": "customer",
"mrr": 4200
}
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → sandbox_lookup_crm_contact
arguments: {"email":"dana@northwind.io"}
structuredContent: {"ok":true,"contact":{"id":"c_1024","name":"Dana Whitfield","email":"dana@northwind.io","company":"Northwind","stage":"customer","mrr":4200},"demo":true,"credits":{"charged":0,"balance":500}}sandbox_list_records0 cr
Sandbox: lists fixture records (contacts, invoices, tickets) with paging and filtering. Free — no real data.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_list_records \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-d '{"type":"invoices","limit":25}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "sandbox_list_records",
"demo": true,
"credits": {
"charged": 0,
"balance": 500
},
"result": {
"ok": true,
"type": "invoices",
"count": 2,
"rows": [
{
"id": "in_881",
"contact": "dana@northwind.io",
"amountCents": 420000,
"status": "paid"
},
{
"id": "in_882",
"contact": "priya@fernbrook.co",
"amountCents": 89000,
"status": "open"
}
],
"nextCursor": null
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing.
MCP
tools/call → sandbox_list_records
arguments: {"type":"invoices","limit":25}
structuredContent: {"ok":true,"type":"invoices","count":2,"rows":[{"id":"in_881","contact":"dana@northwind.io","amountCents":420000,"status":"paid"},{"id":"in_882","contact":"priya@fernbrook.co","amountCents":89000,"status":"open"}],"nextCursor":null,"demo":true,"credits":{"charged":0,"balance":500}}sandbox_send_emailconfirm0 cr
Sandbox: simulates sending an email and returns a fake message id. Nothing is delivered. Free, and still side-effecting so you can exercise the confirmation flow.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_send_email \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"to":"dana@northwind.io","subject":"Your invoice is ready","body":"Hi Dana — invoice in_881 is attached. Thanks!"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "sandbox_send_email",
"demo": true,
"credits": {
"charged": 0,
"balance": 500
},
"result": {
"ok": true,
"simulated": true,
"messageId": "sim_4f2a91cd",
"to": "dana@northwind.io",
"subject": "Your invoice is ready",
"deliveredAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → sandbox_send_email
arguments: {"to":"dana@northwind.io","subject":"Your invoice is ready","body":"Hi Dana — invoice in_881 is attached. Thanks!"}
structuredContent: {"ok":true,"simulated":true,"messageId":"sim_4f2a91cd","to":"dana@northwind.io","subject":"Your invoice is ready","deliveredAt":"2026-08-09T20:15:18.358Z","demo":true,"credits":{"charged":0,"balance":500}}sandbox_update_crm_recordconfirm0 cr
Sandbox: simulates updating a CRM record. Nothing is written. Free, and still side-effecting so you can exercise the confirmation flow.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_update_crm_record \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"recordId":"c_1024","fields":{"stage":"churn_risk","owner":"ae_12"}}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "sandbox_update_crm_record",
"demo": true,
"credits": {
"charged": 0,
"balance": 500
},
"result": {
"ok": true,
"simulated": true,
"recordId": "c_1024",
"updatedFields": {
"stage": "churn_risk",
"owner": "ae_12"
},
"updatedAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → sandbox_update_crm_record
arguments: {"recordId":"c_1024","fields":{"stage":"churn_risk","owner":"ae_12"}}
structuredContent: {"ok":true,"simulated":true,"recordId":"c_1024","updatedFields":{"stage":"churn_risk","owner":"ae_12"},"updatedAt":"2026-08-09T20:15:18.358Z","demo":true,"credits":{"charged":0,"balance":500}}sandbox_create_paymentconfirm0 cr
Sandbox: simulates creating a payment charge. No money moves. Free, and still side-effecting so you can exercise the confirmation flow.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_create_payment \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"customerId":"c_1024","amountCents":4200,"currency":"usd"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "sandbox_create_payment",
"demo": true,
"credits": {
"charged": 0,
"balance": 500
},
"result": {
"ok": true,
"simulated": true,
"paymentId": "pay_9c31be40",
"customerId": "c_1024",
"amountCents": 4200,
"currency": "usd",
"status": "succeeded"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → sandbox_create_payment
arguments: {"customerId":"c_1024","amountCents":4200,"currency":"usd"}
structuredContent: {"ok":true,"simulated":true,"paymentId":"pay_9c31be40","customerId":"c_1024","amountCents":4200,"currency":"usd","status":"succeeded","demo":true,"credits":{"charged":0,"balance":500}}sandbox_delete_recordconfirm0 cr
Sandbox: simulates deleting a record. Nothing is deleted. Free, and still side-effecting so you can exercise the confirmation flow.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/sandbox_delete_record \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"type":"tickets","recordId":"t_51"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "sandbox_delete_record",
"demo": true,
"credits": {
"charged": 0,
"balance": 500
},
"result": {
"ok": true,
"simulated": true,
"deleted": {
"type": "tickets",
"recordId": "t_51"
},
"deletedAt": "2026-08-09T20:15:18.358Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → sandbox_delete_record
arguments: {"type":"tickets","recordId":"t_51"}
structuredContent: {"ok":true,"simulated":true,"deleted":{"type":"tickets","recordId":"t_51"},"deletedAt":"2026-08-09T20:15:18.358Z","demo":true,"credits":{"charged":0,"balance":500}}gmail_sendconfirm6 cr
Send an email through the workspace's connected Google account. Uses the managed OAuth connection — the agent never sees credentials. Requires the workspace to connect Google first (POST /api/public/v1/oauth/google/authorize). Side-effecting: requires confirmation.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/gmail_send \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"to":"dana@northwind.io","subject":"Your invoice is ready","body":"Hi Dana — invoice in_881 is attached. Thanks!"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "gmail_send",
"demo": false,
"credits": {
"charged": 6,
"balance": 494
},
"result": {
"ok": true,
"messageId": "18f2a91cd4b5e607",
"to": "dana@northwind.io",
"threadId": "18f2a91cd4b5e607",
"sentAt": "2026-09-23T18:00:00.000Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → gmail_send
arguments: {"to":"dana@northwind.io","subject":"Your invoice is ready","body":"Hi Dana — invoice in_881 is attached. Thanks!"}
structuredContent: {"ok":true,"messageId":"18f2a91cd4b5e607","to":"dana@northwind.io","threadId":"18f2a91cd4b5e607","sentAt":"2026-09-23T18:00:00.000Z","demo":false,"credits":{"charged":6,"balance":494}}slack_post_messageconfirm4 cr
Post a message to a Slack channel as the workspace's connected Slack identity. Uses the managed OAuth connection — the agent never sees credentials. Requires the workspace to connect Slack first (POST /api/public/v1/oauth/slack/authorize). Side-effecting: requires confirmation.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/slack_post_message \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"channel":"#general","text":"Deploy finished :white_check_mark:"}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "slack_post_message",
"demo": false,
"credits": {
"charged": 4,
"balance": 496
},
"result": {
"ok": true,
"channel": "C0123456789",
"ts": "1758650400.000100",
"messageId": "C0123456789:1758650400.000100",
"postedAt": "2026-09-23T18:00:00.000Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → slack_post_message
arguments: {"channel":"#general","text":"Deploy finished :white_check_mark:"}
structuredContent: {"ok":true,"channel":"C0123456789","ts":"1758650400.000100","messageId":"C0123456789:1758650400.000100","postedAt":"2026-09-23T18:00:00.000Z","demo":false,"credits":{"charged":4,"balance":496}}github_create_issueconfirm4 cr
Create an issue in a GitHub repository as the workspace's connected GitHub identity. Uses the managed OAuth connection — the agent never sees credentials. Requires the workspace to connect GitHub first (POST /api/public/v1/oauth/github/authorize). Side-effecting: requires confirmation.
Request
curl -X POST https://3bi.ai/api/public/v1/tools/github_create_issue \
-H "Authorization: Bearer $RELAY_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: run-42" \
-H "x-confirmation-token: $CONFIRMATION_TOKEN" \
-d '{"owner":"octocat","repo":"hello-world","title":"Found a bug","body":"Steps to reproduce..."}'Response — 200
{
"ok": true,
"requestId": "b0e1c8a2-9f4d-4d0f-9a1e-2c5d7f8e1a30",
"tool": "github_create_issue",
"demo": false,
"credits": {
"charged": 4,
"balance": 496
},
"result": {
"ok": true,
"issueNumber": 1347,
"url": "https://github.com/octocat/hello-world/issues/1347",
"state": "open",
"createdAt": "2026-09-23T18:00:00.000Z"
}
}Replaying the same idempotency-key returns this exact body with "replayed": true and charges nothing. Call it first without a token: the 428 response carries a preview of these arguments plus the single-use confirmationToken used above.
MCP
tools/call → github_create_issue
arguments: {"owner":"octocat","repo":"hello-world","title":"Found a bug","body":"Steps to reproduce..."}
structuredContent: {"ok":true,"issueNumber":1347,"url":"https://github.com/octocat/hello-world/issues/1347","state":"open","createdAt":"2026-09-23T18:00:00.000Z","demo":false,"credits":{"charged":4,"balance":496}}sandbox_* tools are free and return fixtures with "demo": true. Their pre-rename names (e.g. send_email) still resolve for now and answer with a deprecated pointer to the new name.
What credits cost
- Starter — 1,000 credits$9.00
- Builder — 5,000 credits$39.00
- Scale — 25,000 credits$149.00
One-time purchases in USD, excluding tax. See full pricing.
10. Security best practices for operators
- Issue a dedicated API key for each MCP client or autonomous agent. Narrow the
allowedToolslist to only the tools that agent needs, and set a per-key daily or lifetime credit cap. - Disable any tool you do not expect your agents to use from the console
/toolspage. Workspace-level disables apply across every key and MCP connection. - Side-effecting tools (sandbox_send_email, sandbox_update_crm_record, sandbox_create_payment, sandbox_delete_record) always take two calls: an unconfirmed call that returns a preview and a single-use token, then the confirmed call. Because the token is issued by the server and bound to the previewed arguments, an agent cannot authorize itself in advance.
- Rotate keys regularly. The rotation endpoint keeps the old key valid for 10 minutes, so you can update your agents without downtime.
- Monitor the
audit_logstable and usage page for unexpected tool names or credit spend spikes.
11. MCP server
The same catalog is exposed over Model Context Protocol for clients like Claude, Cursor and ChatGPT. Connect with OAuth 2.1 — you approve the client once, then calls are metered against your workspace credits exactly like HTTP calls. Side-effecting tools are marked with the MCP destructiveHint annotation, so compliant clients ask the human to approve the call before it runs.
https://3bi.ai/mcp # Streamable HTTP, OAuth 2.1 (dynamic client registration)
Differences from the HTTP API: each tool description carries its credit cost, results come back as JSON text plus structuredContent with the same demo and credits fields, and failures (including an exhausted balance) surface as an MCP tool error rather than an HTTP status code. The idempotency-key header is HTTP-only. Confirmation is not: over MCP the same gate is expressed as a confirmation_token argument — the first call returns the preview and token as a tool error, the second call carries the token. Usage from MCP appears in the same console usage history.
Tool visibility note: The MCP list-tools endpoint may show the full starter catalog even when a workspace has disabled a tool. Workspace-level disables and per-key allowedTools are enforced at invocation time, so a disabled or disallowed tool returns a clear tool_disabled error instead of running.
12. Account status
GET /api/public/v1/me
{
"ok": true,
"orgId": "…",
"scopes": ["tools:invoke"],
"credits": { "balance": 500 },
"usage": { "totalCalls": 12 },
"rateLimit": { "perMinute": 60 }
}Poll this before a long run, or read the balance from the console's usage page.