Reference
API reference
The machine entrance's whole HTTP surface. Base URL: https://api.bubblio.dev. The same contract, typed: download the OpenAPI 3.1 spec.
Authentication, four flavors
| Label | What it is |
|---|---|
none | The public door surface (/agent/**). Agents call with no credential; every rate ceiling is published in the manifest. |
bbl_ key | Authorization: Bearer bbl_… — the platform's own server calling /v1/**. Never valid from a browser. |
signed push | API key plus X-Bubblio-Signature-V2/X-Bubblio-Timestamp — HMAC-SHA256 of ${timestamp}.${rawBody} with the platform's webhook secret, ±5 min. The key alone can never move a receipt. |
consent token | Bearer <JWT> from POST /approvals/session — the human's passkey session on the approvals surface. |
Two properties shape many responses: uniform misses (unknown, malformed, foreign, disabled, and migration-lagged resources answer the same 404 body per surface, so a prober can't tell closed from nonexistent — on the push endpoints even a failed signature answers that 404) and calm errors (one plain sentence; machine codes ride a code field — branch on that, never on message text).
Discovery
{ hub, feed, doors: [{ doorId, label, handle, blurb?, manifest, mcp, paid_actions? }] }. Owners may run unlisted doors — absence proves nothing. Transient read failures degrade to an honest empty doors list, never a 500.bubblio_* extensions; divergences declared in the body's mapping notes. is_eligible_checkout is false on every row — transacting is MCP quote → confirm. Cached per ttl_seconds; per-caller 429 with Retry-After.act only while ≥1 action is visible), endpoints, pricing + payment when actions are priced (publishable key only — facts, never credentials), and every rate ceiling. 404 {"error": "No agent door here."} for everything that isn't an open door.MCP endpoints
Minimal, hand-rolled, stateless Streamable-HTTP servers: POST one JSON-RPC 2.0 message per request, receive one application/json response — no SSE, no session ids. Methods: initialize, ping, tools/list, tools/call; notifications (no id) are acknowledged 202. Tool failures come back as isError results the calling model can read; JSON-RPC error objects are reserved for malformed requests, unknown methods, unknown tools. Bodies cap at 64KB (413); tripped ceilings answer 429 with the real limit in the message.
POST /agent/{doorId}/mcp
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"quote","arguments":{"tool":"generate_image","args":{"prompt":"…"}}}}
→ 200 {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{ …the quote JSON… }"}]}}door argument (handle or door id) resolved through the unchanged per-door machinery, plus find { query }. Custody, approvals, mandates, payments, and rate buckets all scope to the resolved door. GET answers 405 — there is nothing to GET on a stateless endpoint.about/ask always; list_actions/quote/confirm while actions are visible. The action lifecycle and result shapes are the actions guide; quotes needing a human are the consent guide.Approvals (the consent pages' API)
action/connect), platform, pinned action + args + amount, the agent's presented vs verified identity, mandate context. Renders facts and writes nothing — prefetchers are inert. Uniform 404: “No approval here.”mode: 'auth' (returning passkey) or 'register' (the first Face ID — enrollment IS the approval); both carry the same deterministic challenge: the SHA-256 of this approval's canonical terms. 409 when already decided/expired, or code: not_connected while a connect grant awaits its key.mandate: true additionally mints the offered standing mandate — one signature, indivisible. Idempotent: { ok, status: "approved", already: true } on repeats.{ token }, the Bearer for the endpoints below.mnd_…) — immediate at the door: the next quote skips it, and a confirm on a covered quote re-reads it pre-flight and refuses without consuming.Managed agent keys
{ ctx, label?, externalUserId?, userHint?, max_active_per_user? } — ctx is the platform-signed user JWT (shape-checked only; Bubblio can neither verify nor forge it). 201 answers the raw bak_ key exactly once; at cap: 409 code: key_cap_reached; mint ceiling: 429 (default 30/hour).{ grantId, key, id } — the raw key must hash to the named row (proof the caller holds the very key it connects); the write stamps the waiting quote and approval, then flips the grant last, winner-only. Idempotent for the same key; a different key against a granted grant is a 409 — one grant, one key, forever.Action-registry sync
door.sync. Shapes only, never capability: synced actions arrive with policy off; suggestions are displayed, never applied — which is exactly why API-key auth is allowed here. Idempotent; undeclared actions are orphaned (never deleted); destructive-looking names sync anyway with the suggestion dropped and a warning returned. connectPath sets the phone-connect page; payments sets the rails profile (publishable key only — sk_/rk_ refused by name). Answers { synced, orphaned, unchanged, pendingDecision, warnings }.Signed platform pushes
pushDoorResult; automatic in createBubblioToolRoute). Only unreachable → ok exists; the upgrade is append-shaped and the signed body must name this same quote. A pushed payment summary must match the pin exactly. Results cap at 32KB (refused, not truncated). 409: already_resolved (benign), not_confirmed, final_outcome.checkout.session.completed webhook (the SDK's completeBubblioCheckout / resumePaidQuote). Refused when args_hash or amount mismatches the pin (amount_mismatch, args_mismatch) — the money must buy exactly the pinned call, and every refund-worthy refusal means the payment bought nothing (the SDK auto-refunds it). 409: expired, already_settled, not_payable, conflict.The outbound half: the signed callback
What Bubblio POSTs to your callbackUrl — the contract createBubblioToolRoute verifies for you. Signed twice for a deprecation window: verify the replay-protected X-Bubblio-Signature-V2; the legacy body-only signature is replayable and refused by SDK ≥0.9. Delivered at most once per quote; Bubblio waits min(timeoutSeconds ?? 10, 20) seconds, then records unreachable until a signed results push resolves it. Body fields — tool, args, agent, ctx, idempotencyKey, callbackTimeoutSeconds, payment/payment_settled/paymentLinkRequest — are documented in the actions guide and the payments guide; the body is extensible, so never validate it with a closed schema. Answer 200 with JSON (outcome ok); any non-2xx finalizes the receipt as platform_rejected. For a paymentLinkRequest, answer { url, ref }.