Private beta. Cerebro isn't open yet: the commands and keys on these pages need access. Request access
Connect what you sell, or the agent you build
Cerebro is the transaction layer between sellers and AI agents. A seller connects its system once; an agent connects once; Cerebro carries each sale between them, from discovery to the seller's confirmation.
Concepts
| Term | Meaning |
|---|---|
| Platform | Software a business sells through: a table system, a tee sheet, a ticketing tool, a shop. |
| Offering | Something for sale: a VIP table, a tee time, a ticket, a product. |
| Window | A time something is free, live from the platform, with what's left and its price. |
| Hold | A short reservation at the platform while the person decides. Only where the platform can hold. |
| Transaction | The sale. It goes from held or confirmed through to fulfilled; the platform's system decides every step. |
| Agent | Any AI acting for a person: an assistant, a personal agent, a concierge. |
Levels and capabilities
A platform's level comes from the functions it connects. Every result and every transaction an agent receives carries can: exactly what can be done with it right now. Nothing is offered that the platform can't do.
A hold needs availability(); confirm() doesn't. A shop selling from stock, or anything without times, connects confirm() alone: agents buy straight from the offering, and your confirm is the final check.
In live, agents see only what they can buy: a connection needs confirm() (level 4 or 5) to be shown. Levels 1 to 3 work in test while you integrate.
In can | Means | Guarantee |
|---|---|---|
availability | Live windows can be asked | Asked from the platform at that moment, never from a copy. A window can still be gone by the time you hold or confirm. |
hold | The platform can reserve for a few minutes | While held, the platform keeps it for you until hold_expires_at. |
confirm | The sale can be made through Cerebro | The platform's own system accepts or declines. Nothing is sold until it does. |
release | A hold can be let go | Always possible on a live hold. See hold and release for when the platform frees it. |
cancel | The sale can be cancelled | Under the platform's own terms. The platform can refuse. |
status | Where it stands | Always. This is the source of truth after a missed webhook. |
Environments and keys
There are two environments, test and live, and nothing crosses between them. A test key only sees test connections and test sales; a live key only live ones. Test sales never earn or cost a fee.
| Key | Who | Used for |
|---|---|---|
ck_test_… · ck_live_… | Platforms | The SDK and the npx @cerebro-one/connect commands, as CEREBRO_KEY |
ak_test_… · ak_live_… | Agents | HTTPS and MCP, as Authorization: Bearer |
Keys are shown once and stored only as a hash. A platform key can't call agent endpoints, and the other way round. An agent key can be limited to some platforms or categories; anything outside them looks like it doesn't exist.
An agent key has scopes: read (find, availability, status, events), transact (hold, release, confirm, cancel), and customers (issue and revoke people's tokens). Keys administer nothing: webhooks, keys and approvals are a person's in your organisation. A new key gets read and transact unless you choose others. A key without a scope gets not_allowed, and over MCP it doesn't see those tools. Give customers only to the key on your own server that registers your users, never to one in an AI model's reach or an MCP setup; give an analytics tool a read-only key.
Rotating a key makes a new one with the same scopes; the old one keeps working for the overlap you choose (up to 30 days), then stops. Revoking stops a key at once. On a platform, pass key as a function (key: () => secrets.get('CEREBRO_KEY')) and the SDK reads it before every request, so a rotated key takes over without restarting your application. The SDK keeps retrying when Cerebro can't be reached, at start or later, and stops only when Cerebro refuses the key.
Terminal and approvals
Everything is set up from the terminal, by you or your coding agent: npx @cerebro-one/connect setup (--agent for an agent product, --personal for your own agent). There is no dashboard. The first time, it gives you a link (opened in your browser where there is one, and always printed): you sign in, check that the code matches the one in your terminal, and confirm. The terminal then acts as you for 30 days (logout ends it at once), but never with your second factor.
So every step only a person may take comes to you as a link: you see exactly what is asked and from which terminal, and confirm with your authenticator, or refuse. Cerebro then does that one thing, as you, and nothing else. A link works once, for 30 minutes, and fails if what it names changed in the meantime. Secrets it makes (a live key, a live webhook secret, your agent's setup, an invitation link) are shown once, in your browser, never to the terminal or a coding agent.
Setup asks for these when it reaches them. Anything else: npx @cerebro-one/connect ask <action> '<json>' (run it alone for the list).
- Organisation: create it (accepting the terms), accept new terms, request going live, accept fee terms, request deletion or cancel it.
- Live: put a mapping version live, approve checkout hosts, make or replace a live key, add a live webhook or roll its secret, waive the verified-contact requirement.
- Re-opening live sales (
reopen_live): resume after a pause, switch a business or live offering back on, allow or unblock an agent, open your policy to every agent, raise the hold limit. Closing is never an approval: pause, switch off, block and lower limits from the terminal at once. - Team: invite someone, change a role, remove a member, cancel an invitation.
- Connecting with the other side (sellers): invite an agent to sell your offerings (
invite_agent), and accept an agent's invitation (accept_pair). Both let an agent in, so both are yours to confirm; the invitation link is shown to you in the browser. An agent inviting or accepting, and anyone ending a connection or withdrawing an invitation, needs no confirmation. - Money: set up payouts (Stripe-hosted).
- Connections: disconnect and reconnect a system or an agent; your own agent's setup.
- You: close your account.
What a signed-in terminal can do
It acts as you, without your second factor: it sets up and tests (connections, test keys, mappings for test, test webhooks), reads (team, keys, ready, webhooks), and takes the safe direction at once (pause, switch off, block an agent, lower a limit, revoke a key, stop a webhook, end a waiver). It can ask for anything above, never confirm it. A stolen session is not harmless: it can read everything you can, and start mapping readings, whose AI cost is capped by your organisation's spend ceiling. Its session is kept in ~/.config/cerebro/session.json, readable only by your user on that machine; a coding agent running as you can read it too, so treat it like a signed-in browser. npx @cerebro-one/connect terminals lists every terminal signed in as you, terminals revoke <id> signs one out at once (its links you haven't confirmed stop working too), and terminals revoke --all signs out every one. A session lasts 30 days from sign-in, whatever its use, and is never extended: then you sign in again. It reaches every organisation you're a member of, with your role in each, checked on every request: removed from one, it loses it at once. Closing your account signs them all out.
Limits
- 10 sign-in codes a minute from one network address; a terminal checks for its code at most 30 times a minute (it's told to every 3 seconds). A code works for 10 minutes, once.
- A person looks up or confirms at most 20 sign-in codes a minute.
- At most 10 approvals asked a minute, and 20 waiting at once. An approval works once, for 30 minutes; it fails if what it names changed (the mapping version's reading, the terms in force) or if the terminal that asked was signed out.
- Over a limit:
rate_limited(429) withRetry-After. The first refusal each minute is recorded in the audit, without the address.
Connect your system
Keep your system. Connect it once. Cerebro does everything agents see.
You install the SDK in your backend and point a few functions at what you already have. The SDK only connects out to Cerebro: it asks for work, runs your function, and sends the answer back, so you open no ports. Return your data exactly as you store it, in your own field names and your own words; Cerebro maps it once, with you, and you approve.
confirm() under your own rules, and returning a reference for each one. Cerebro does everything agents see, the mapping, testing, and keeping agents in step with your answers. Your system stays the authority on what's free and what's sold.Quickstart
Install the SDK
npm install @cerebro-one/connect
Sign in and get a test key
# Signs this terminal in (you confirm a link in your browser), creates your organisation # and saves a test key to .cerebro/test-key, kept out of git. Then it walks you through the rest. npx @cerebro-one/connect setup export CEREBRO_KEY=$(cat .cerebro/test-key)
Connect your functions
Only offerings is required. Each function you add raises your level.
import { Cerebro } from '@cerebro-one/connect'; import { shop } from './your-system.js'; // your own code export const connection = Cerebro.connect({ key: process.env.CEREBRO_KEY, // Every status and payment word your system uses, in your own words. statuses: ['retenida', 'confirmada', 'pendiente', 'cancelada', 'asistio', 'no_vino', 'pagado', 'sin_pagar'], checkoutDomains: ['pay.yourshop.com'], // where your checkout pages live offerings: () => shop.listAll(), // required: everything you sell, as you store it availability: (id, query) => shop.free(id, query), // what's free between query.from and query.to hold: (id, order) => shop.hold(id, order), // optional: { ref, expires_at } release: (ref) => shop.unhold(ref), // optional: frees a hold early confirm: (id, order) => shop.sell(id, order), // { ref, status, checkout?: { url, amount, currency, expires_at } } checkout: (ref) => shop.newPaymentPage(ref), // optional: a fresh page for the same order cancel: (ref) => shop.cancel(ref), // optional: under your own terms status: (ref) => shop.status(ref), // optional: { status, payment? } in your words }); const { level, can } = await connection.ready; console.log(`Connected at level ${level}: ${can.join(', ')}`);
Map, approve and publish
Cerebro reads your data, shows you how agents will see it, and asks about anything it can't tell from the data. See mapping and going live.
npx @cerebro-one/connect preview
Test the whole purchase
npx @cerebro-one/connect test
Functions
Each function can return a value or a promise. Cerebro waits for each answer up to the time shown; after that the agent gets an error and your late answer is ignored.
| Function | Return | Time |
|---|---|---|
offerings() required | Everything you sell, as you store it. Give each record an id: Cerebro passes it back to your other functions. Up to 100,000 records per connection; the SDK sends a large catalogue in parts by itself. | 120 s |
availability(id, query) | What's free between query.from and query.to (ISO 8601), in your own format. query.partySize (people) or query.itemQuantity (items) is set when the agent knows it, as the offering is bought. | 3 s |
checkout(ref) | Optional. A fresh checkout page for a sale you already made (the first expired or was lost): { url, amount, currency, expires_at? } for the same order and the same amount, never a new order. | 5 s |
quote(order) | Optional. Prices a cart with your own rules: { ref?, currency, lines: [{ line, amount }], fees?, taxes?, discounts?, total, due_now?, valid_until }, amounts in major units. The parts must add up to the total exactly; a tax already inside your prices (gross prices with VAT) is sent with included: true and isn't added. order.answers carries what the buyer answered so far (a delivery country) when your price depends on it. Without it, Cerebro prices carts from your mapped fixed prices and says so on the quote. | 5 s |
hold(id, order) | { ref, expires_at }: your hold's reference and when it lapses (ISO 8601). | 5 s |
release(ref) | { status: 'released' } once the hold is freed. | 5 s |
confirm(id, order) | { ref, status, payment_url?, due?, delivery? }. status is confirmed, pending (you confirm by hand later) or declined. due is { amount, currency } in major units: what the customer pays now through payment_url. Better: return checkout: { url, amount, currency, expires_at }, your own checkout page for this sale. The page must be on a host you list in checkoutDomains (in live, also approved by an owner or admin through a link, with their authenticator; a shared host such as github.io itself is refused, your own you.github.io is fine), https on its default port, and its amount must be what the buyer accepted: for a cart, the quote's due-now (or its total); for a single offering, its price or the deposit your offering publishes, in its currency, to the cent. A link with another amount or currency, with no amount, or for an offering whose price Cerebro doesn't know is withheld from agents, and the sale waits for a person (review_reason checkout_mismatch or checkout_unverified). For any other up-front amount, use a quote. A checkout page is not a payment: report the payment when it's made. For a cart, order.lines lists every part and order.quote.total is what the buyer accepted: make all of it for exactly that total, or refuse with unavailable. Never a part of a cart, never another total; return total with the sale. order.answers holds the buyer's answers to your questions; an eligibility answer is what the buyer stated, not a verified fact. | 10 s |
cancel(ref) | { status: 'cancelled' }, or throw not_allowed when your terms don't allow it. | 10 s |
status(ref) | { status, payment? } in your own words. | 5 s |
The order
hold and confirm receive an order:
| Field | Meaning |
|---|---|
start | The window's start, exactly as your availability gave it (after mapping). |
partySize or itemQuantity | One of them, as the offering is bought: people taking part, or items bought. |
units | Inventory units the order takes, when Cerebro worked them out (one unit, or a pool of identical units: party size divided by one unit's capacity, rounded up). Absent when you allocate; your answer is the authority. |
customer | { name, email?, phone? }. A hold may carry a placeholder name until confirm. |
holdRef | On confirm after a hold: your ref from hold. |
idempotencyKey | The same for every retry of the same request. See below. |
Idempotency: required
If your answer to hold or confirm gets lost on the way back, Cerebro sends the same request again with the same order.idempotencyKey. Your system must recognise the key and return the result it already made, not make a second one. Without this, a lost answer could sell the same table twice.
cancel and release must be safe to call twice for the same ref.
Saying no
Throw an error with one of these codes, and the agent gets exactly that answer. In hold, confirm, release and cancel, only unavailable, hold_expired, not_allowed, invalid and rate_limited say that nothing happened. Any other error there (an exception in your code, a timeout to your payment provider, your database down) can't prove the order wasn't made, so the agent gets outcome_unknown and Cerebro keeps sending the same request under the same idempotencyKey until you answer it. In the other functions, anything else reaches the agent as source_unavailable.
Sellers that require payment up front. Cerebro then commits only what ends by itself if unpaid. That needs your cancel and status, and either hold or a provisional confirm. Pass provisionalConfirm: true only if your confirm makes a purchase that you cancel when asked, if unpaid by its checkout's deadline, and your checkout always returns expires_at. A checkout without a deadline, or one that outlasts the hold, is withheld and the sale is cancelled. At the deadline Cerebro calls your status first: a purchase you report paid (payment mapped to paid) is kept, even if your payment notice never reached Cerebro. Otherwise it calls your cancel. If your system can't be asked three times running, or refuses the cancel, a person at Cerebro takes it over.
The refusal contract. Throw one of those five codes only when your system made nothing: no order, no hold, no charge. Cerebro takes a refusal as proof, and acts on it: the buyer's request behind it is closed (see authorizations). If anything may have been written before the error, throw without a code. Cerebro then keeps asking under the same idempotencyKey, and your idempotency makes sure it never becomes a second order. Breaking this contract is the one way a buyer could end up with two orders; a request that had a confirm refused is never used for another attempt, so even then it can't happen twice from the same request.
| Code | When |
|---|---|
unavailable | It's sold or taken. |
hold_expired | The hold lapsed before confirm. |
not_allowed | Your terms don't allow it (a cancellation inside the window, a party too large). |
invalid | Something in the order is wrong. |
not_found | No such offering or reference. |
For example: throw new CerebroError('unavailable', 'table taken'), with CerebroError from @cerebro-one/connect. A plain error with a code property works too.
Reporting outcomes
Tell Cerebro the moment you know what happened: paid, attended, used, cancelled at your side, a no-show. Agents see it at once, and fees depend on it. Use your own status and payment words; the mapping translates them.
import { connection } from './platform-connect.js'; // Call these the moment you know: a check-in scan, a payment, a no-show. export async function onCheckIn(ref: string) { await connection.report(ref, { status: 'asistio' }); } export async function onPayment(ref: string, amount: number, paymentId: string) { await connection.report(ref, { payment: 'pagado', amount: { amount, currency: 'EUR' }, paymentId }); } // Tickets, codes or downloads: everything the buyer needs to use the sale, sent together. export async function onTicketsIssued(ref: string, tickets: Array<{ seat: string; code: string }>) { await connection.report(ref, { delivery: tickets.map((t) => ({ kind: 'ticket' as const, label: t.seat, code: t.code, format: 'qr' as const })) }); }
For a payment, send the amount (major units) and your own payment id. Each payment then counts once, however often it's reported, and a deposit is never mistaken for the whole sale. Without an amount, a payment means what was due now through your payment link.
Payment must fit the sale. A payment in another currency than the sale's never makes it paid, and one that comes after the sale ended (cancelled, expired) never brings it back. Both, and a payment above what the sale is worth, are kept and go to Cerebro's staff until you report the refund.
An order on its way: report your word for shipped, ready to collect, or out for delivery, and for a shipped order add tracking: { url, carrier, number } (the url must be https). The sale stays confirmed with its progress; delivered or collected is fulfilled.
Tickets, codes and downloads: send what the buyer needs to use the sale as delivery, a list of items, with your confirm answer when you have it at once, or later with report(ref, { delivery }) (for example with the payment). Each item is { kind, label?, code?, format?, url?, line?, expires_at? }: kind is ticket, code, pass, download or document; code is what is scanned or typed (up to 4,096 characters, kept exactly as you send it) and format how it is shown (qr, barcode or text); url is an https page or file on one of your approved hosts, the same list as your checkout pages; line names the cart line it belongs to. Every item needs a code or a url. Send all of a sale's items together, at most 100: each list replaces the one before. A report whose delivery is wrong is refused whole and says why; in a confirm answer the sale still stands without it, and you send it again with report. Cerebro keeps items encrypted, shows them only to the agent that made the sale, removes them when the sale is cancelled, and deletes them with the buyer's details.
A word your mapping doesn't know is refused, never guessed: it means your system changed, so run preview again.
Mapping and going live
preview reads your offerings, a few availability answers and your status words, and Cerebro's AI proposes how each field maps to Cerebro's standard: titles, times in your time zone, prices with what they mean (a minimum spend, a deposit and what it's a percentage of), statuses. It runs twice independently; where the two readings disagree, you're asked. The mapping is stored as data and applied by plain code; no AI runs when agents buy.
A reading takes a few minutes, so preview starts a mapping run and waits for it (--no-wait starts it and returns; status shows where it stands and its preview once it's done). Running preview again while a run is going waits for that same run, and the same data with the same answers as a finished run gets that run's result: nothing is read or charged twice. If Cerebro restarts during a run, the run is marked interrupted and you start it again; a reading that finishes after its run was interrupted is discarded, never published.
In a terminal, preview asks its questions one by one. Questions marked needed must be answered; important ones you can accept as read. Each question has an id that names the fact it's about in your data (the currency of consumo_minimo, what the status no_vino means), not its wording. Answer once: your answer is kept with your name and goes into every later reading. Values changing don't affect it (a minimum spend of 50 becoming 75 is still a minimum spend). It no longer stands, and preview tells you why and asks again, when the field disappears or changes type, a field related to it by name appears beside it (minimum_spend_scope next to minimum_spend), a field of a few repeating words (a unit, a category) gets a new word, a status word goes away, or Cerebro's mapping standard changes. A change of meaning that leaves no trace in your data can't be seen by Cerebro: answer the question again, or take your answer back: answers lists everything Cerebro goes by from your answers (and what no longer stands, with why), and withdraw <id> takes one back at no cost; the next preview asks it again. A question Cerebro can't tie to a field, status or record in your data is marked as such; its answer applies to the next reading only. For CI, answer reads answers.json: [{ "id": "…", "answer": "…" }]. A choice's own price (a ticket type, an extra) must say what it counts: per person, item, unit, transaction, or each one chosen; until that's known, Cerebro asks and won't price it. Approve only when every golden example is right.
npx @cerebro-one/connect approve 1
npx @cerebro-one/connect sync
sync publishes your offerings with the approved mapping. In live, Cerebro also reads them again by itself every 15 minutes, so a new price or item reaches agents without you running anything; if Cerebro can't read them for an hour (your live system is down), they stop selling until the next good reading, so nothing is sold at a price Cerebro can't vouch for. Run sync when you want a change to show at once. If your data changes shape, the affected kind of offering is switched off until you approve again, so agents never see something mapped wrong.
Going live: once test passes, put your company's legal details in .cerebro/company.json and run npx @cerebro-one/connect setup again. Each step only a person may take prints a link: you open it, see exactly what is asked, and confirm with your authenticator (approvals). Cerebro checks your identity, the company registry, the sanctions lists and your website; after approval, an owner or admin confirms a live key, which is shown once in their browser, never to the terminal. Set it as CEREBRO_KEY in production and run sync. Putting the mapping live is a person's decision too: an owner or admin of your organisation confirms that version through a link. A key, or a coding agent holding one, can approve for test only, so nothing you try in test reaches live agents. Test and live keep separate offerings, sales and connected functions.
Are you live?
npx @cerebro-one/connect ready (with your test or live key; --json for a coding agent) answers from the same rules Cerebro uses to decide whether an agent may find and buy your offerings, so it can't say you're live when agents can't buy. It gives three answers. Connected: your organisation is approved for live and on the current terms, your fee terms are in force, your connection is active with a working live key, your live system connects confirm, your live mapping is approved, Cerebro read your live offerings within the hour, and you haven't paused. Commercially live: connected, at least one offering any live agent can buy, and a sandbox test that passed under the mapping version you put live. Per offering: eligible, or why not (switched off by you, held by screening or a price jump, its kind of data changed shape, a paid offering without an approved checkout host, or requirements of agents that can't be met). Every unmet condition says what's missing, who acts (you, your system, or Cerebro) and how it resolves.
Screening
Every text agents could read (title, business name, city, terms, details) is checked at each sync. Text that reads like an instruction to an AI, hidden characters, or images loaded from elsewhere hold that offering back from agents entirely. sync tells you which offering, which field and why; change the text and sync again.
Test with the sandbox
npx @cerebro-one/connect test
A built-in test agent runs the whole purchase against your test connection, through the same engine real agents use: find, availability, hold, release, confirm, the same confirm again (it must return the same sale), status and cancel. Each step prints ✓, ✗, or – when you didn't connect that function.
- It runs with a test key only. A live key is refused, because it makes real holds and sales in your system.
- The test agent reaches your platform and nothing else.
- If a step fails, it releases what it held and cancels what it sold, where you allow it, and says what it couldn't undo.
Give your agent things to buy
One connection across the sellers connected to Cerebro, with live answers from each seller's own system.
Use MCP if your agent speaks it; the same operations are on HTTPS. Both use the same key, permissions, idempotency and errors.
Quickstart (MCP)
Add Cerebro as a remote MCP server (Streamable HTTP, stateless) with your agent key and the customer token of the person the session is for:
{
"mcpServers": {
"cerebro": {
"type": "http",
"url": "https://api.cerebro.one/mcp",
"headers": {
"Authorization": "Bearer ${CEREBRO_AGENT_KEY}",
"Cerebro-Customer": "${CEREBRO_CUSTOMER}"
}
}
}
}Your own agent, for yourself (Claude Desktop or any MCP client, no server): run npx @cerebro-one/connect setup --personal (add --max "50 EUR" for a standing permission, below). You confirm a link with your authenticator and get the config above filled in, shown once in your browser: a key that can find and buy (never manage anything), and your own customer token. You can also give your agent a standing permission to book for you without asking each time, within limits you set: a maximum per purchase, a number of purchases every so many days, and an end date. Within it, each purchase gets its own authorization, which Cerebro makes and binds to exactly that purchase. Your agent can't create one, raise the limits or extend them, and you can end the permission at once. Without a permission, live purchases need an authorization (below), as for any agent.
An agent product for many people: your server opens each person's MCP session with their Cerebro-Customer token (customers), and registers the buyer's authorization when they ask for a purchase. Your model passes only its authorization_id.
Tools: find, availability, quote, hold, release, confirm, status, checkout, cancel. They take and return the same fields as the HTTPS endpoints below.
Test sellers
With a test key you can buy from Cerebro's own test sellers straight away, with nothing to set up: Noche (nightclub tables: a table per party, a deposit up front, holds), BaanVrij (padel courts: time slots, identical courts, a checkout page) and Hofladen Berg (a farm shop: items in stock, a quote priced for the delivery country). They connect through the same SDK as any seller. Their stock is shared by every builder, so another agent can take the table you wanted, as in real life. A seller's own test connection is closed to other organisations' agents unless that seller opens it.
Use these buyer names on a hold or confirm to see the hard cases:
| Name | What happens |
|---|---|
Test Unavailable | The seller refuses before making anything: unavailable. |
Test Unknown | The seller makes the order and its answer is lost. Cerebro asks again under the same key and you get the same order, never two. |
Test Slow | The seller's first answer comes after Cerebro stopped waiting; asked again, it gives the same order. |
Test Short Hold | The hold lapses after 30 seconds. |
Test Price Change | The seller confirms another total than the one priced: the sale waits for a person (review_reason: total_mismatch). |
Releases your open holds and cancels your sales at Cerebro's test sellers, with a test key; other builders' holds and sales are untouched. Answers { "released", "cancelled", "failed" }. The sellers' own stock starts fresh whenever Cerebro restarts.
find
Searches what's connected and asks each source for live windows. text matches words in titles, business names, categories, details and terms; it is not read by AI. Windows cover from to to, or the next 14 days without them; give party_size (people) or item_quantity (items) to keep only what fits; each offering is filtered by its own one.
curl -sS https://api.cerebro.one/v1/find \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "VIP", "city": "Marbella", "from": "2027-03-06T20:00:00+01:00", "to": "2027-03-07T06:00:00+01:00", "party_size": 8 }'
{
"request_id": "7c0d5a52-3f0e-4c8e-9a51-2b1f6d0e8a11",
"results": [
{
"offering_id": "b6f1c2d4-9e3a-4f7b-8c21-5a0e7d9f3b62",
"business": "Club Uno",
"city": "Marbella",
"title": "Mesa VIP 12",
"category": "nightlife.table",
"inventory": "unit",
"capacity": 8,
"capacity_unit": "people",
"purchase_dimension": "party_size",
"unit_allocation": "one_unit",
"tax": "unknown",
"amounts": [
{
"kind": "minimum_spend",
"amount": {
"amountMinor": 100000,
"currency": "EUR"
},
"scope": "per_unit",
"timing": "at_venue",
"refundable": "unknown"
},
{
"kind": "deposit",
"amount": {
"amountMinor": 30000,
"currency": "EUR"
},
"scope": "per_unit",
"timing": "upfront",
"refundable": "unknown",
"basis": {
"percent": 30,
"of": "minimum_spend"
}
}
],
"choices": [],
"inputs": [],
"buyer_amount": null,
"delivery": "unknown",
"terms": "Señal del 30% al confirmar. Cancelación gratuita hasta 48 h antes; después la señal no se devuelve. Consumo mínimo a pagar en el local.",
"details": {
"zona": "Planta 2 · vista DJ",
"deposit_percent": 30
},
"can": [
"availability",
"hold",
"confirm",
"cancel"
],
"windows": [
{
"start": "2027-03-06T23:00:00+01:00",
"remaining": null,
"amounts": [
{
"kind": "minimum_spend",
"amount": {
"amountMinor": 100000,
"currency": "EUR"
},
"scope": "per_unit",
"timing": "at_venue",
"refundable": "unknown"
},
{
"kind": "deposit",
"amount": {
"amountMinor": 30000,
"currency": "EUR"
},
"scope": "per_unit",
"timing": "upfront",
"refundable": "unknown",
"basis": {
"percent": 30,
"of": "minimum_spend"
}
}
]
}
]
}
],
"unavailable_sources": 0
}connected_only: true keeps only sellers you are connected with (below).
purchase_dimension says what to send for this result when you quote, hold or confirm: party_size (people taking part: a court, a table, a tour) or item_quantity (items bought: tickets, products); the other is refused, and so is the old quantity. unit_allocation says how many units an order takes: one_unit (the party must fit its capacity), identical_pool (party size divided by one unit's capacity, rounded up: 6 players on courts of 4 take 2), platform (the platform decides) or none. Units are worked out by Cerebro or the platform and shown on quotes and sales; you never send them. tax says whether listed prices include tax; the platform's quote has the actual tax. Amounts are in minor units (cents), each with its kind, scope, timing and whether it's refundable; a deposit says what it's a percentage of. windows is null when the source only lets agents discover. unavailable_sources counts sources that didn't answer in time: their offerings are left out, never guessed.
availability
Live windows for one offering, asked from its source right now. In a query string, write times in UTC (Z): a + in an offset would be read as a space.
curl -sS "https://api.cerebro.one/v1/offerings/$OFFERING_ID/availability?from=2027-03-06T19:00:00Z&to=2027-03-07T05:00:00Z&party_size=8" \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY"
{
"offering_id": "b6f1c2d4-9e3a-4f7b-8c21-5a0e7d9f3b62",
"can": [
"availability",
"hold",
"confirm",
"cancel"
],
"windows": [
{
"start": "2027-03-06T23:00:00+01:00",
"remaining": null,
"amounts": [
{
"kind": "minimum_spend",
"amount": {
"amountMinor": 100000,
"currency": "EUR"
},
"scope": "per_unit",
"timing": "at_venue",
"refundable": "unknown"
},
{
"kind": "deposit",
"amount": {
"amountMinor": 30000,
"currency": "EUR"
},
"scope": "per_unit",
"timing": "upfront",
"refundable": "unknown",
"basis": {
"percent": 30,
"of": "minimum_spend"
}
}
]
}
]
}customers
One key serves all your users; the customer token tells them apart, so one person never sees another's sales. From your server, register each person once with your own id for them (never their name or email) and keep the token with their account. Send it as the Cerebro-Customer header on every hold, confirm, status, release and cancel. A sale belongs to the person whose token made it: with anyone else's token it doesn't exist (not_found).
The token never goes where your AI model can read it: your server adds the header. Over MCP, open each person's session with the header on the connection; without it, find and availability work and sales don't. Registering needs a key with the customers scope and isn't offered as a tool. A token lasts a year (expires_at); calling this again for the same person issues a fresh one, and the old one keeps working until it expires, so renew before then. POST /v1/customers/{ref}/revoke ends all of theirs.
# From your server, once per person (when they sign up with you). Keep the token with their account; # never put it where your AI model can read it. curl -sS https://api.cerebro.one/v1/customers \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "ref": "user_8f3a2c" }'
{
"customer_id": "4c0e2b9a-3f1d-4e8a-9b7c-2d5f6a8e1c3b",
"ref": "user_8f3a2c",
"token": "ct_test_Jx9vQ2mR7tLp4WnB8kYc3HdF6sZa1EoU",
"expires_at": "2027-11-08T10:00:00.000Z",
"created": true
}Ends every token of that person, for a lost device or a closed account. Answers { "revoked": 2 }.
carts and quotes
When a quote is required. An offering that asks for a choice (a ticket type), has choices with their own prices, takes an amount the buyer sets, or shows its prices before tax, is bought through a quote: hold or confirm its quote_id. By offering_id alone it's refused with reason: quote_required, because only the seller can price exactly what is bought. Cerebro's fee is worked out on the value of the sale as recorded: the quote's total for a cart, the offering's own price for a single sale, or the seller's confirmed total when Cerebro had no price.
Everything a person wants from one business, as lines: { "lines": [{ "offering_id", "party_size" or "item_quantity", "start"?, "variant"?, "options"?: [{ "id", "quantity" }], "pick"?, "buyer_amount"?: { "amount_minor", "currency" } }] }. Use the ids from the result's choices: a variant is a ticket type with its own price (adult, child), options are add-ons, pick is a specific item from an availability window (a seat, a stylist), buyer_amount is what the person adds within the offering's limits (a tip).
Cerebro checks every choice first, then the platform prices the cart with its own rules; a platform without a price engine is priced from its fixed prices, and price_source says which. The answer is a quote: quote_id, each line's amount, fees, taxes, discounts, total, due_now (null when the platform didn't say) and valid_until, plus the questions to ask (inputs). A quote never changes: a different number of people or items, choice or time is a new quote. One quote is one currency and one business.
When the price depends on an answer (shipping to a country), send those answers with the cart; the platform prices with them, and priced_for lists them. The sale must give the same answers, or it's refused and you ask for a new quote. Questions appear once in inputs, however many lines ask them. A tax marked included is already inside the prices: shown, not added.
Show the person the total, then hold or confirm with quote_id alone; confirm takes the person's answers by the questions' keys. The platform makes the whole cart for exactly that total or nothing: never a part, never another price. An expired quote is refused with unavailable; ask for a new one. If a platform ever confirms another total, the sale stays pending with review_reason: "total_mismatch", with no payment link and nothing due, until Cerebro's staff settle it on written evidence; no report from the platform changes it meanwhile.
Every amount is an integer in the currency's minor unit (cents), with currency and currency_decimals (2 for EUR, 0 for JPY). Format them for the person's language yourself: 125050 EUR is €1,250.50 or 1.250,50 €, the same money.
hold and release
Only where can includes hold. Pass the window's start exactly as you received it. Every hold needs an Idempotency-Key header (1 to 255 printable characters); use a new one per hold and the same one only to retry. Keys are yours alone: another agent using the same key never meets your transaction.
curl -sS https://api.cerebro.one/v1/holds \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY" \ -H "Cerebro-Customer: $CEREBRO_CUSTOMER" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d @- <<JSON { "offering_id": "$OFFERING_ID", "start": "2027-03-06T23:00:00+01:00", "party_size": 8, "customer": { "name": "Ana García", "email": "ana@example.com" }, "authorization_id": "$AUTHORIZATION_ID" } JSON
{
"id": "e2a7c9d1-5b34-4f8e-a6c0-91d3b7f5e208",
"status": "held",
"can": [
"status",
"confirm",
"release"
],
"payment": "none",
"offering_id": "b6f1c2d4-9e3a-4f7b-8c21-5a0e7d9f3b62",
"start": "2027-03-06T22:00:00.000Z",
"party_size": 8,
"item_quantity": null,
"units": 1,
"value": {
"amountMinor": 100000,
"currency": "EUR"
},
"source_ref": null,
"hold_expires_at": "2027-03-02T18:14:11.000Z",
"hold_expires_in_seconds": 599,
"payment_url": null,
"checkout_expires_at": null,
"due_now": null,
"paid": null,
"outstanding": null,
"payment_route": "platform",
"quote_id": null,
"review_reason": null,
"progress": null,
"source_hold_until": null,
"delegation": {
"enforced": false,
"requirements": [
"request"
],
"missing": [],
"authorization_id": "7d2e4f6a-1b3c-4d5e-8f9a-0b1c2d3e4f5a"
},
"payment_deadline": null,
"delivery": null
}curl -sS -X POST https://api.cerebro.one/v1/holds/$HOLD_ID/release \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY" \ -H "Cerebro-Customer: $CEREBRO_CUSTOMER"
Where the platform connected release, it frees the hold at once. Where it didn't, the hold is released for you but the platform keeps it until it lapses: the transaction then shows source_hold_until, the time it's free again there.
confirm
Makes the sale, only after the person has said yes: from a hold (hold_id) or directly (offering_id, start, and party_size or item_quantity as its purchase_dimension says) where there is no hold. The sale shows party_size, item_quantity and the units it takes. customer.name is required.
# Keep the key: retrying with it returns the same sale and never makes a second one. IDEMPOTENCY_KEY=$(uuidgen) curl -sS https://api.cerebro.one/v1/transactions \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY" \ -H "Cerebro-Customer: $CEREBRO_CUSTOMER" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d @- <<JSON { "hold_id": "$HOLD_ID", "customer": { "name": "Ana García", "email": "ana@example.com" } } JSON
{
"id": "e2a7c9d1-5b34-4f8e-a6c0-91d3b7f5e208",
"status": "confirmed",
"can": [
"status",
"cancel"
],
"payment": "unpaid",
"offering_id": "b6f1c2d4-9e3a-4f7b-8c21-5a0e7d9f3b62",
"start": "2027-03-06T22:00:00.000Z",
"party_size": 8,
"item_quantity": null,
"units": 1,
"value": {
"amountMinor": 100000,
"currency": "EUR"
},
"source_ref": "NO-8f3a1c2e",
"hold_expires_at": null,
"hold_expires_in_seconds": null,
"payment_url": "https://pay.yourshop.com/r/NO-8f3a1c2e",
"checkout_expires_at": "2027-03-06T22:40:00.000Z",
"due_now": {
"amountMinor": 30000,
"currency": "EUR"
},
"paid": null,
"outstanding": {
"amountMinor": 100000,
"currency": "EUR"
},
"payment_route": "platform",
"quote_id": null,
"review_reason": null,
"progress": null,
"source_hold_until": null,
"delegation": {
"enforced": false,
"requirements": [
"request"
],
"missing": [],
"authorization_id": "7d2e4f6a-1b3c-4d5e-8f9a-0b1c2d3e4f5a"
},
"payment_deadline": null,
"delivery": null
}The answer is the platform's: confirmed, pending (the business confirms by hand; you get an event) or declined. Confirmed is not paid. When the platform takes payment at checkout, give the person payment_url; due_now is what they pay there.
status and cancel
curl -sS https://api.cerebro.one/v1/transactions/$TRANSACTION_ID \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY" \ -H "Cerebro-Customer: $CEREBRO_CUSTOMER"
curl -sS -X POST https://api.cerebro.one/v1/transactions/$TRANSACTION_ID/cancel \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY" \ -H "Cerebro-Customer: $CEREBRO_CUSTOMER"
Only where can includes cancel, under the platform's terms; it can refuse with not_allowed. Whether money comes back is the platform's rule, shown in the offering's terms and each amount's refundable.
Payment happens on the platform's own checkout page: give the person payment_url. It works until checkout_expires_at; after that, or if it was lost, this asks the platform for a new page for the same sale, never a second order. A payment page is not a payment: payment changes only when the platform reports one, and you get transaction.payment. An expired page never cancels the sale; only the platform's own terms can. If the platform's notice of a payment never arrives, Cerebro asks the platform; never ask the person to pay again.
An order on its way shows progress: { stage, at, tracking }: stage is preparing, ready (to collect or to start), shipped or out_for_delivery, and tracking the carrier's page, name and parcel number when the platform gave them. You get transaction.progress. The sale stays confirmed until it's delivered (fulfilled).
Tickets, codes and downloads arrive on the sale as delivery: { at, items }, null until the seller sends any. Each item is { kind, label?, code?, format?, url?, line?, expires_at? }: show a code as its format says (qr, barcode or text), exactly as given, and open or hand over a url (always https, on a host the seller's owner approved). You get transaction.delivery with the number of items, never the items themselves: read the sale. The list is always the whole, current one; it stays after the sale is used and is null again once the sale is cancelled, declined or failed. Items are the buyer's way in: give them to the person the sale is for, and don't keep them longer than they need them.
Payment fields
A transaction keeps what the sale is worth apart from what was paid:
| Field | Meaning |
|---|---|
value | The full commercial value, from the offering's amounts (for a table, its minimum spend). |
due_now | What the person pays now through payment_url, as the platform computed it. |
paid | Paid so far, net of refunds, as the platform reported it. |
outstanding | Still to pay, for example the rest of a minimum spend at the venue. |
payment | none, unpaid, partially_paid, paid, partially_refunded, refunded. |
payment_route | platform: the platform's own checkout took the money. Cerebro never sees card data. |
Example: a €1,000 table with a 30% deposit, deposit paid: value €1,000, paid €300, outstanding €700, payment partially_paid.
Retries and idempotency
Every hold and confirm carries an Idempotency-Key. Sending the same key again returns the same transaction and makes nothing twice. The same key with different details is refused with invalid. Two identical requests at the same moment make one sale.
| You get | It means | Do this |
|---|---|---|
source_unavailable (503) | The platform never received the request. Nothing happened there. | Try again later. Tell the person nothing was made. |
outcome_unknown (504) | The platform received it but its answer was lost. It may have gone through. | Retry with the same Idempotency-Key: you get the sale if it was made, and nothing is made twice. Don't tell the person it failed. |
unavailable (409) | Taken since you looked. | Offer another window. |
| A network error, no answer | Unknown | Retry with the same key, or read GET /v1/transactions/{id} if you have the id. |
Cerebro already retries once with the same key before it answers outcome_unknown. After that, Cerebro keeps sending the request to the platform under the same key until it answers, and tells you the result by webhook. Until then the transaction shows status requested (or stays held for a confirm on a hold), can offers only status, and a release or cancel is refused with unavailable.
Every field is checked before anything reaches a platform: a request that fails the check is answered invalid, naming the field, and nothing is held or sold.
Events and webhooks
Every change to your transactions is an event: transaction.failed, transaction.held, .confirmed, .pending, .declined, .released, .expired, .cancelled, .fulfilled, .no_show, .payment. When a change on an existing transaction (a confirm on a hold, a release, a cancel) is established not to have happened, you get .confirm_failed, .release_failed or .cancel_failed and the transaction keeps its status.
curl -sS "https://api.cerebro.one/v1/events?after=0" \ -H "Authorization: Bearer $CEREBRO_AGENT_KEY"
{
"events": [
{
"id": "21abfcb8-dde3-4390-b39a-5f68aa5978ef",
"seq": 41,
"transaction_id": "3f1dfbab-15bb-4956-8204-6fbd44332aa9",
"version": 1,
"type": "transaction.held",
"data": {
"transaction_id": "3f1dfbab-15bb-4956-8204-6fbd44332aa9",
"from": null,
"to": "held",
"actor": "source"
},
"correlation_id": "d950d77b-b507-4257-88dd-e61221bad8ec",
"created_at": "2027-03-02T18:04:11.562Z"
}
]
}version counts the changes to one transaction (1, 2, 3…) and seq orders all your events. Pass the last seq you saw as after; up to 500 per page with limit.
To get events pushed instead, add a webhook: npx @cerebro-one/connect webhooks add https://you.example/cerebro. Where Cerebro sends your events is a person's decision in your organisation, never your key's: a key that leaked from your server can't redirect them. A live endpoint (--live) is confirmed by a person through a link, with their authenticator.
Each event is POSTed to your HTTPS URL with the event as the body, Cerebro-Event-Id, and Cerebro-Signature: t=<unix time>,v1=<hex>. The secret is shown once. Verify every delivery:
import { createHmac, timingSafeEqual } from 'node:crypto'; // Check every delivery before trusting it: one signature must match the raw // body, and the timestamp must be recent, so an old delivery can't be replayed. // While a rolled secret's overlap lasts there are two signatures; either may match. export function verifyCerebroSignature(secret: string, header: string | undefined, rawBody: string, toleranceSec = 300): boolean { const [t, ...signatures] = (header ?? '').split(','); const timestamp = Number(/^t=(\d+)$/.exec(t ?? '')?.[1]); if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false; const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest(); return signatures.some((s) => { const received = Buffer.from(/^v1=([0-9a-f]{64})$/.exec(s)?.[1] ?? '', 'hex'); return received.length === expected.length && timingSafeEqual(expected, received); }); }
- Answer with any 2xx. Otherwise the delivery is retried up to 10 times, from 30 seconds apart up to 6 hours apart.
- A retry carries the same event id: use it to ignore duplicates.
- Deliveries can arrive out of order. Use
versionto order a transaction's events, andGET /v1/transactions/{id}when in doubt.
Managing webhooks
From a terminal signed in as a developer, admin or owner of your organisation (npx @cerebro-one/connect webhooks …):
- Add an endpoint (
add <url>). Its secret is shown once. - Stop one (
stop <id>): nothing more is sent to it, pending deliveries included. - Roll its secret (
roll <id> --overlap-hours 24): a new secret, shown once. For the hours you choose (default 24, at most 168) every delivery carries two signatures, one per secret, so you can switch without missing anything. - Send a test (
test <id>): a signedwebhook.testevent at once, with what your endpoint answered. - See deliveries (
deliveries <id>): the last 100 with their attempts and last error, and send one again (resend <id> <delivery>, same event id).
A live endpoint and a live secret are confirmed by a person through a link, with their authenticator, and the live secret is shown once in their browser, never to the terminal. Agent keys can't manage webhooks.
Limits and tracing
- 120 requests per minute per key. Beyond that:
rate_limited(429) withRetry-After. - 1,000 searches (find and availability) per key per hour, and at most 25 results per search.
- At most 10 holds open at once on one platform and 50 in total. Release a hold you won't use.
- If, over 24 hours and past 30 holds, fewer than 1 in 20 holds become sales, new holds are refused with
rate_limited; confirming directly still works. Write to Cerebro if your agent needs higher limits. - An agent that cancels more than half of its own sales (past 10 cancellations in 24 hours) can't make new ones until Cerebro reviews it.
- A platform can set how many holds one agent may keep open on its supply.
- Every answer carries
Cerebro-Version, the API version it speaks: the one your key was made with, or the one you ask for with the same header. Changes that would break your integration only come with a new version. - Send
X-Correlation-Id(up to 100 letters, digits,.,_,-) to follow a request through Cerebro and the platform; every answer returns one.
Sellers you are connected with
A seller and an agent can choose each other: one sends an invitation, the other accepts, and from then on the agent finds and buys that seller's offerings even where the seller lets in only the agents it chose. The answer lists them: { sellers: [{ seller, organisation, since, limits }] }. limits are the seller's own for your agent, in one currency: max_sale_minor (one sale), open_minor (held or sold and not yet used, at once) and sales_per_day (sales and holds in 24 hours); null when it set none. Over a limit, a hold or a sale is refused with rate_limited and a reason: limit_pair_sale, limit_pair_open, limit_pair_day, or limit_pair_unmeasured when the sale's value isn't known or is in another currency than the limits'. Only the seller can raise them.
To invite a seller: npx @cerebro-one/connect invite --email owner@seller.example prints a link that works once, for 14 days (for that address only when you name one). Nothing is connected until the seller accepts, which it confirms with its second factor. To accept a seller's invitation: npx @cerebro-one/connect accept <link>. connected lists them, connected end <id> ends one, and invite list and invite revoke <id> manage what you sent. Either side can end a connection at any time; a sale already made keeps its status and cancellation.
Are you live?
From the rules your key and your new sales are checked against: connected when your organisation is active, approved for live and on the current terms, your agent is active and has a working live key; commercially_live when, as well, your agent completed a test purchase and, if a fee agreement pays agents a share, your payouts are set up. unmet lists each missing condition with who acts and how it resolves. Any key with the read scope.
Fees and settlement
For business and platform operators: what a sale through Cerebro costs and how it's settled.
- The money goes to you. The customer pays through your own checkout. Cerebro never holds the money for platform sales.
- A fee only on fulfilled sales. Cancelled, declined, expired and no-show sales carry no fee. No subscriptions, set-up fees or minimums.
- Terms are agreed with each platform: a percentage of the sale's value, with a minimum and a maximum, fixed per agreement. The agreement in force on the day of the sale applies.
- Refunds lower the fee. After a partial refund, the fee is recalculated on what was kept; after a full refund, it's zero. Corrections are new entries; nothing is rewritten.
- One invoice a month, listing every fee and correction of that month, in the sale's currency.
- Agents earn a share of Cerebro's fee on the sales they made, paid only after the platform has paid the invoice. Finding an offering earns nothing; making the sale does.
Control and data
- Your system decides. Every hold, sale and cancellation is your system's answer; Cerebro never sells on its own.
- You choose what agents can do by the functions you connect, and what they see by what you return in
offerings()and approve in the mapping. - You choose which agents. Every agent with live access can find your offerings unless you block it, or you let in only the ones you chose. To connect with one in particular, invite it:
npx @cerebro-one/connect invite --email them@agent.example(add--currency EUR --max-sale 1500 --open 5000 --per-day 20to set your own limits for that agent: the largest sale, the value open at once, sales and holds in 24 hours). You confirm in the browser; they get a link that works once, for 14 days, for that address only (or, without--email, for whoever you give it to). When they accept with one of their agents, that agent can find and buy from you. An agent can invite you the same way:npx @cerebro-one/connect accept <link>shows who asks, and you confirm in the browser, with limits of your own if you want them.connectedlists who you are connected with;connected end <id>ends one at once: nothing new is sold between the two, and sales already made keep their status and cancellation. A block still wins over a connection, and Cerebro's own limits for every agent apply as well. - You see every step in the terminal:
setup,preview,sync,testandready. There is no dashboard: what only a person may decide comes to you as a link. - Data: Cerebro keeps the offerings you publish and, for each sale, the customer's name and the contact details the agent sent, to pass them to you and settle the sale. They are removed 90 days after the sale ended; the sale itself, its history and its money stay. Availability is never kept as a catalogue: it's asked from you live, every time. Card data never passes through Cerebro.
- Audit: every change is logged by the database itself, without customer data.
Statuses
Every status comes from what the platform answered or reported. Cerebro's code moves a transaction only along these paths; no AI ever changes a status.
| Status | Meaning | Next |
|---|---|---|
requested | Recorded; the platform hasn't answered yet. The same request again finishes it | held, pending, confirmed, declined, failed |
failed | The platform didn't make it: nothing was held or sold. The same key gets the same answer, except when the platform couldn't be reached, when it tries again | final |
held | Reserved until hold_expires_at | released, expired, pending, confirmed, declined |
pending | The business is confirming by hand | confirmed, declined, cancelled |
confirmed | Accepted by the platform's system | cancelled, fulfilled, no_show |
released | You let the hold go | final |
expired | The hold ran out | final |
declined | Not accepted; nothing was sold | final |
cancelled | Cancelled under the platform's terms | final |
fulfilled | It happened: attended, used, delivered | final |
no_show | Confirmed, but nobody came | final |
Errors
Every error has the same shape, on HTTPS and in MCP tool results:
{
"error": "not_allowed",
"detail": "a fulfilled transaction cannot be cancelled",
"correlation_id": "5d3f9a1e-2b7c-4e60-8f14-9a0c6e2d7b35"
}| Code | HTTP | Meaning |
|---|---|---|
unavailable | 409 | Sold or taken since you looked. |
hold_expired | 410 | The hold ran out before confirm. |
not_allowed | 403 | Outside the platform's terms, or not possible in this state. |
invalid | 422 | A required field is missing or wrong, or an idempotency key was reused with different details. |
not_found | 404 | No such offering or transaction for your key. |
unauthorized | 401 | Missing, wrong or revoked key. |
rate_limited | 429 | Too many requests; see Retry-After. |
source_unavailable | 503 | The platform never received the request. Nothing happened there; try again later. |
outcome_unknown | 504 | The platform received the request but its answer was lost, so it may have gone through. Retry with the same Idempotency-Key: you get the sale if it was made, and nothing is made twice. |
What isn't supported yet
Don't promise these to your users or your customers:
- Holds, cancellations and payment links everywhere. Each depends on the platform. Always read
canandpayment_url; never assume. - Payment through Cerebro. The platform's own checkout takes the money. Agent payment tokens and payment through Cerebro come later.
- Refunds started by an agent. Cancel where allowed; whether money comes back is the platform's rule, and refunds are reported by the platform.
- Changing a sale (another time, another party size). Cancel where allowed and make a new one.
- Carts across businesses (one quote is one business), waiting lists and partial cancellations.
- Reading plain-language requests.
findmatches words and filters; it doesn't interpret sentences. - Other ways to connect a platform. The SDK is the way: no API authorisation, feeds or MCP servers on the platform side.
- SDKs in other languages. The platform SDK is for Node.js. Agents use plain HTTPS or MCP from any language.
Changelog
- 2026-11-10 (connecting): a seller and an agent can choose each other:
npx @cerebro-one/connect invite,acceptandconnected; a seller's own limits for one agent (limit_pair_*reasons);GET /v1/connectedandconnected_onlyonfind. The terminal now opens the link you must confirm in your browser where there is one (--no-browserorCEREBRO_NO_BROWSER=1to only print it). - 2026-11-10 (delivery): a sale carries what the buyer needs to use it: sellers send
delivery(tickets, codes, passes, downloads, documents) withconfirmorreport; agents readdeliveryon the sale and gettransaction.delivery. Links must be on the seller's approved hosts. - 2026-11-09 (terminal-first): no dashboard.
npx @cerebro-one/connect setupsigns a terminal in with a code you confirm in your browser and does the whole connection; each step only a person may take is a link confirmed with their authenticator (approvals). New commands:login,logout,setup,ask,webhooks,team,keys,terminals. - 2026-11-09 (breaking, quotes): an offering with a required or priced choice, a buyer-set amount, or prices before tax can't be held or confirmed by
offering_idalone: quote it first (reason: quote_required). - 2026-11-09 (test sellers): Cerebro runs three test sellers every test key can buy from, with test buyer names for the hard cases and
POST /v1/test/reset. A seller's own test connection is now closed to other organisations' agents unless it opens it. Personal setup for your own agent over MCP. - 2026-11-09 (prepaid): sellers can require payment up front: such a sale carries
payment_deadlineand is cancelled if unpaid by then (Cerebro asks the seller first). Platforms declare a provisional confirm withprovisionalConfirm: true; without it, these offerings are committed through a hold. New refusal reasonprepaid_unsupported. - 2026-11-09 (breaking, authorizations): every live hold, and every live confirm without a hold, names an
authorization_idyour server registered for that purchase (POST /v1/customers/{ref}/authorizations); without one it's refused (reason: authorization_required). Test shows what's missing indelegationand doesn't refuse. Error bodies now carry areasonwhen there is one. - 2026-11-09 (breaking, webhooks): agent keys no longer manage webhooks and the
adminscope is gone (keys that had only it are revoked): a person in your organisation adds, stops, tests and rolls them in the dashboard; live ones need a recent second factor./v1/webhooksanswers 404. Deliveries and their signatures are unchanged. - 2026-11-09 (breaking): live offerings are read again every 15 minutes and stop selling after an hour without a good reading; live checkout hosts must be approved in the dashboard (the SDK's
checkoutDomainsonly proposes them); agent keys getreadandtransactunless more are chosen, and issuing customer tokens needs the newcustomersscope (no longeradmin); a single offering's payment link must ask its price or its published deposit, to the cent, or it is withheld and the sale waits for a person; inhold,confirm,releaseandcancel, an error other than an explicit refusal isoutcome_unknown, never a failure. - 2026-11-08 (mapping, later):
GET /connect/v1/mapping/answersandPOST /connect/v1/mapping/answers/withdraw(CLIanswers,withdraw); an answer to a question not tied to your data applies to one reading; a reading that outlives its run is discarded.npx @cerebro-one/connect clean <file>pseudonymizes an export on your machine before you share it (customers replaced by tokens that still link their records; not anonymous: read the free-text fields it lists). - 2026-11-08 (mapping):
previewstarts a mapping run and checks on it (POST /connect/v1/mapping/proposeanswers 202 with a run,GET /connect/v1/mapping/runs/{id}); a retry joins the running run and the same input reuses a finished one. Questions have ids naming the fact they're about; answers are given by id withanswered_by, kept, and carried into later readings until the data under them changes. A choice's price states what it counts (price_scope). MCP tools refuse fields they don't take, naming what to send instead. - 2026-11-08 (breaking):
quantityis replaced byparty_sizeoritem_quantity, as each offering'spurchase_dimensionsays; results carryunit_allocationandtax; quotes and sales show theunitstaken; the platform's order carriespartySize,itemQuantityandunits. A per-unit price now means per court, table or room taken, everywhere. - 2026-11-07 (any model):
confirm()no longer needsavailability()(stock, queues); a direct sale at a time is priced from that time's window; carts sendanswersfor prices that depend on them (priced_for); taxes can beincluded; questions listed once. - 2026-11-07 (progress):
progresson a sale (preparing, ready, shipped, out for delivery) withtracking, andtransaction.progress. Payments in another currency, after a sale ended or above its worth are kept and go to staff until refunded. - 2026-11-07 (checkout):
checkouton confirm withcheckout_expires_at,POST /v1/transactions/{id}/checkout, the platform'scheckoutDomainsand optionalcheckout(ref). - 2026-11-07 (newest): carts and quotes:
POST /v1/quotes, hold and confirm byquote_id, answers to the offering's questions, and the platform's optionalquote(order). - 2026-11-07 (latest): customers: every hold, confirm, status, release and cancel carries the
Cerebro-Customerheader with the token your server got fromPOST /v1/customers. - 2026-11-07 (later): a mapping approved with a test key applies to test only; live takes an approval with the live key. Functions your SDK connects count for its own environment. Statuses
requestedandfailed; requests are checked before they reach a platform; afteroutcome_unknownCerebro finishes the request and sends a webhook; idempotency keys are per agent and environment. - 2026-11-07: first published version: SDK, sandbox, HTTPS and MCP, events and webhooks, payment fields,
outcome_unknown,release.