Conventions
Errors, replaying a write, pagination, and the one visible difference between the two transports.
Read this once and the rest of the reference reads itself.
The two transports refuse differently
Same cause, two shapes — and this trips up anyone who reads the MCP guide and then writes REST.
| MCP | REST | |
|---|---|---|
| Transport-level refusal | 401 with WWW-Authenticate | 401 with WWW-Authenticate |
| Capability-level refusal | 200 with a JSON-RPC error envelope | Real HTTP status code |
Past the transport guard, the MCP layer wraps every error in a 200 JSON-RPC response — that is the
protocol, not a choice. Do not branch on the HTTP status alone when speaking MCP: read the envelope.
One deliberate exception: a capability outside your granted scope is refused before the transport,
as a real 403 insufficient_scope with a WWW-Authenticate challenge. It has to be, because a
refusal buried in a 200 is one no client can read as an invitation to re-authorise.
Refusals are typed
A refusal tells you which of four things went wrong: the capability does not exist, your credential may not use it, the request is malformed, or a precondition failed. Refusals are stable and enumerable — the full list is generated from the registry itself, so it cannot drift from what the server does.
Identifiers
Capability identifiers are dotted, lowercase, and stable: audit.overview.get,
content.items.list, brand-voice.define. The last segment is the verb. Resource identifiers are
UUIDs. Neither is ever recycled.
Pagination
Two reads paginate, and they do not use the same envelope. We would rather tell you than let you find out from a second parser.
GET /v1/projects/{projectId}/topics | GET /v1/projects/{projectId}/contents | |
|---|---|---|
| The page | items | contents |
| Where the counts live | at the root: total, page, pageSize, totalPages | in a pagination object: page, pageSize, totalPages, totalItems |
| The total is called | total | totalItems |
| Is there a next page? | hasNextPage and hasPreviousPage | not given — compare page with totalPages |
Both take the same two query parameters. page starts at 1 and defaults to 1. pageSize defaults
to 20 and is capped at 50: ask for more and you get 50, not a refusal. Every other read returns its
whole document and takes neither.
To walk a list, request page 1, then keep going until hasNextPage is false on topics, or until
page reaches totalPages on contents. Do not assume the last page is full.
Replaying a write
There is no Idempotency-Key header, and we will not pretend otherwise. What the API guarantees
instead is narrower and lives in the state of the change itself: a proposal is applied once.
Apply it a second time — a retry after a timeout, a double click — and you get 409 with
not_pending, which means nothing to retry: the proposal
was already applied, or it was discarded. Read the current state before proposing again.
Reads are safe to repeat. For the 5xx failures, retry with backoff, as the
error catalogue says on each of those entries.
Timestamps
Every timestamp is UTC, ISO 8601, with an explicit offset. There are no naive timestamps in the API surface.
Rate limits
Requests are bounded per credential and per project. We publish no numbers here — a limit written in
documentation becomes a promise, and we only promise what the code enforces. When limits are
enforced at the edge you will see them announced in standard RateLimit-* response headers, and
this page will say so.
One project per connection
Not a convention — an invariant. A session carries exactly one project, resolved from the credential. No parameter can change it, and passing a project identifier in the body of a call is refused rather than ignored. An agency opens one connection per client.