Aibo Platform docs

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.

ts
const tenant = await server.tenant.get();
console.log(tenant.seatTokenCap);

await server.tenant.update({ seatTokenCap: 500_000 });
bash
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).

ts
const { url } = await admin.billing.createCheckout({
  seats: 25,
  successUrl: 'https://console.acme.test/billing?ok=1',
  cancelUrl: 'https://console.acme.test/billing',
});
bash
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.

ts
const usage = await server.usage.get({ from: '2026-10-01', to: '2026-10-31', externalId: 'cust_42' });
// { seatTokenCap, data: [{ externalId, day, promptTokens, completionTokens }] }
bash
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:

json
{ "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.

ts
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:

json
{
  "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