Billing & limits
Per-seat pricing
Aibo is billed per seat. An end user counts as a seat once they send a message in a billing period — creating a user, minting tokens, or listing agents doesn't consume a seat.
Each seat comes with a fixed token cap per billing period (monthly), set on your tenant as seatTokenCap. Prompt and completion tokens both count.
const tenant = await server.tenant.get();
console.log(tenant.seatTokenCap);
await server.tenant.update({ seatTokenCap: 500_000 });curl -sS "$AIBO_URL/v1/tenant" -H "Authorization: Bearer $AIBO_API_KEY"
curl -sS -X PATCH "$AIBO_URL/v1/tenant" \
-H "Authorization: Bearer $AIBO_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"seatTokenCap":500000}'Buying seats
Billing is managed from the console session: POST /v1/billing/checkout starts a Stripe Checkout session for a number of seats (default 10) and returns its url. It returns 501 when billing isn't configured on the deployment (for example, local development).
const { url } = await admin.billing.createCheckout({
seats: 25,
successUrl: 'https://console.acme.test/billing?ok=1',
cancelUrl: 'https://console.acme.test/billing',
});curl -sS -X POST "$AIBO_URL/v1/billing/checkout" \
-H "Authorization: Bearer $AIBO_CONSOLE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"seats":25,"successUrl":"https://console.acme.test/billing?ok=1","cancelUrl":"https://console.acme.test/billing"}'
# → 200 { "url": "https://checkout.stripe.com/…" }Usage
GET /v1/usage returns token usage per user per day, along with the current cap. from and to are required dates; externalId narrows to one user.
const usage = await server.usage.get({ from: '2026-10-01', to: '2026-10-31', externalId: 'cust_42' });
// { seatTokenCap, data: [{ externalId, day, promptTokens, completionTokens }] }curl -sS "$AIBO_URL/v1/usage?from=2026-10-01&to=2026-10-31&externalId=cust_42" \
-H "Authorization: Bearer $AIBO_API_KEY"The token cap: 402 payment_required
The cap is a hard block. When a user's seat has used its seatTokenCap for the period, sending a message fails with 402:
{ "error": { "code": "payment_required", "message": "Seat token cap reached for this billing period." } }In a stream, the same condition may arrive as an error event with code: "payment_required". Handle it in your UI (e.g. "You've reached this month's limit") rather than retrying — retries will keep failing until the period rolls over or you raise seatTokenCap.
Rate limits: 429 rate_limited
Requests are rate limited. When you exceed a limit you get 429 with code: "rate_limited". Back off exponentially with jitter and retry; if the response includes a Retry-After header, wait at least that long. Re-send message posts with the same Idempotency-Key so a retry can never double-send.
async function withRetry<T>(fn: () => Promise<T>, attempts = 5): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err: any) {
// SDK errors carry the envelope's `code`
if (err?.code !== 'rate_limited' || i >= attempts - 1) throw err;
await new Promise((r) => setTimeout(r, 2 ** i * 250 + Math.random() * 250));
}
}
}Errors
Every error uses one envelope:
{
"error": {
"code": "not_found",
"message": "Chat not found.",
"details": {}
}
}code is stable and machine-readable; message is for humans and may change; details is optional extra context (e.g. validation issues).
| HTTP | code |
Meaning | What to do |
|---|---|---|---|
| 400 | bad_request |
Invalid body, query or headers (e.g. a DM with two agents, a group with 8 agents) | Fix the request; see details |
| 401 | unauthorized |
Missing, invalid, expired or revoked credential | Re-mint the user token / check the API key |
| 402 | payment_required |
The user's seat reached the tenant's token cap for the period | Tell the user; raise seatTokenCap or wait for the next period |
| 403 | forbidden |
Authenticated, but this principal can't do this (e.g. a user editing a tenant agent, a user token on a tenant-only endpoint) | Use the right principal |
| 404 | not_found |
The resource doesn't exist in your tenant (or isn't visible to this user) | Check ids and the acting user |
| 409 | conflict |
The request conflicts with existing state (e.g. a tenant slug already taken at signup) | Change the conflicting value |
| 429 | rate_limited |
Too many requests | Back off and retry |
| 500 | internal |
Something went wrong on our side | Retry with backoff; contact support if it persists |