# Synorb agent guide: billing, usage, and pagination

This page is part of the Synorb agent guide at https://synorb.com/agent-guide.md.
What bills and what does not, how to read usage counters, paid upgrades, and how to follow pages and recover from errors.

## Billing rules

| Operation | Manifest usage allowance |
| --- | --- |
| `mode=count`, `mode=orient`, plan, Stream/catalog/details/tag pulls | Zero |
| Beacon create, list, archive, restore, delete | Zero |
| A topic-filtered execute | Only returned, on-topic Manifests |
| Duplicate Manifest returned by a repeated query | Not billed again in the same billing period (the ledger is keyed by organization, Manifest and period); billed again in a later period; inspect the usage receipt |
| Extraction failure or clean empty result | Zero unless a Manifest was actually returned |
| Invalid plan token, invalid scope, unknown Tag | Zero |

Always inspect the response `usage` block and `quota.manifests_billed_this_call`.
`target_count` is a ceiling, not a promise. Synorb does not pad a response with
unrelated Manifests only to fill it.

In `GET /account`, `usage.api_calls` is a legacy name. Despite the name, it is
the same billed-Manifest counter as `usage.manifests_used`. It is NOT a synonym
for `usage.manifests_remaining`. `manifests_remaining` is the inverse:
`manifests_per_month` minus the used count. Both `api_calls` and
`manifests_used` advance only on a billed Manifest, not on every request.

Zero-usage calls (catalog, profile, plan, Beacon CRUD) never change either
counter. For example, take a session of zero-usage catalog, profile and plan
calls followed by one billed pull. Both fields move from 0 straight to the
billed count. They do not count requests. Do not use either field to reconcile
raw request volume.

Prefer `usage.manifests_used`; it is the correctly named field for this number.
Synorb keeps `api_calls` for existing integrations, and it always mirrors
`manifests_used`. It is not a request-volume metric.

### Agent-initiated paid upgrades

Any persona agent (such as Muse or Instinct) can offer the same hosted payment
handoff to its user. With the user's Synorb Key, call
`POST https://api.synorb.com/api/billing/checkout-session` with
`{"target_plan":"individual|professional|startup","billing_cycle":"monthly|annual"}`.
Present the returned `checkout_url` and ask the account owner to confirm it in
Stripe.

The `status=checkout_ready` in the response means only that Synorb created a
hosted session. It is not payment confirmation. Access activates only after the
verified Stripe webhook. For cancellation or payment-method changes, use
`POST https://api.synorb.com/api/billing/portal-session`.

Never collect raw card data. Never put a Synorb Key in a URL. Never say that
payment succeeded based only on a checkout URL. The official CLI exposes the
same safe handoff as `synorb billing checkout --plan individual --open`,
`synorb billing portal --open`, and `synorb billing status`.

## Pagination, usage, and recovery

For GET lists, read a non-null fully-qualified `next` or `prev` URL verbatim,
with the same authentication, until it is `null`. The links are under
`data.pagination` for Streams and at the top level for `GET /v3/beacons`.

For `POST /manifests/query` with `mode` omitted or `"default"`,
`data.pagination.next` and `prev` are opaque, fully-qualified GET URLs. Send
the same Authorization header and GET the URL verbatim. Do not reconstruct the
body or copy scope into the query string. The server-held cursor is
short-lived and bound to the Synorb Key, endpoint, and query session. It is
`null` at the corresponding boundary. If a cursor has expired, repeat the
original POST query.

For `mode="count"` or `mode="orient"` (zero-usage previews), `next`/`prev`
carry no cursor. A bare GET of that URL returns 405, because these modes never
open a server-held query session. Instead, re-POST your ORIGINAL request body
(same filters, same `mode`) to that exact URL. Its `page_num` and `page_size`
query params override the paging of the body before the preview runs again. So
this always works, whatever the mode.

`POST /manifests/exec` remains POST-only because execution can bill Manifests.
Its `next` and `prev` values are POST paging hints. Copy `page_num` and
`page_size` into the original body, and keep Stream IDs, filters, and all other
fields.

Remove the old `plan_token`. POST the changed body to `/manifests/plan` for a
fresh zero-usage token. Then POST the newly planned body to `/manifests/exec`
with only that token added. An old token with changed paging fields returns 409.

When Synorb publishes an exact count, `data.pagination.count_as_of` is exact
within the published snapshot that `count_generation` names. `count_basis` is
`published_snapshot`. The count proof and the returned page use the same
authenticated request context. Writes after that snapshot appear on a later
refresh; they do not change the count retroactively. Keep the same
authentication while you follow a query cursor. Separate requests can
legitimately receive different `count_generation` values.

Synorb refuses some requests before execution (`executed=false` with
`planning_failed`, `needs_scope`, or `invalid_request`). These contain no
Manifest rows and consume no Manifest usage allowance. Use their message and
scope diagnostics before you retry. An executed partial response can contain
and bill authorized candidate rows even when `ok=false`. Respect its coverage
and relevance warnings. Partial candidates do not guarantee complete coverage
or independently proven relevance.

Read `data.quota.manifests_billed_this_call`; do not infer billing from `ok`.
Legacy page/query and Beacon `limit`/`offset`/`total` fields remain supported.
Use `usage.quota_limit`, `usage.quota_used`, `usage.quota_remaining`, and
`data.quota.manifests_billed_this_call`. Manifest-query responses also expose
settled `data.quota.quota_used` and `data.quota.quota_remaining`.

For an actionable `401`, use `/connect`. For a `404`, consult `/docs` and
`/openapi.json`. The `404` body names them in `action_url`
(`https://synorb.com/docs`) and in the `openapi` field
(`https://synorb.com/openapi.json`).

For `429`, branch on `detail.error_code`.
Honor `Retry-After` for `rate_limited`. For terminal demo `quota_exhausted`, use `key_expires_at`
and `starter_bind` (there is no `Retry-After`). For ordinary monthly
exhaustion, wait for `quota_resets_on`.
