The base URL
The base URL is the host alone. Every path starts at/v1/…, so a base URL that already carries /v1 doubles the segment and answers 404.
Stage is the only non-production environment: the same platform, released first. No
dev host exists.
The Console web application is https://app.capcells.com; the console door it calls is /console on the platform’s own host. GET /livez and GET /readyz answer on both environments without a credential.
Authentication
Every request is signed. The credential is acapsk_ key pair minted at the Console, and the request carries an Ed25519 signature over what it does, presented as Authorization: Capcells-Ed25519 fp=…,ts=…,sig=…. API keys carries the whole scheme — the canonical string, test vectors, and capcells_sign, the reference signer every example in this tab calls.
A key acts for exactly one Tenancy and carries a scope: read covers GET and HEAD, write covers every method and permits reads. Every unusable presentation — no header, a bearer token, a signature that does not verify, a timestamp more than 5 minutes out, a revoked key — receives one identical 401, deliberately. The first thing to check is your clock. A key that verifies and is still refused answers 403 under not_permitted_tenant or not_permitted_scope, which are different problems with different remedies.
Request conventions
The wire forms below hold on every route; Model defines each in full.The error shape
Every refusal is one envelope.A refusal
code, and choose your next act from retry — as_is, after_record, after_change or never — and from details where the kind of refusal carries them. The sentence is written for a person and is reworded without notice; nothing else in the envelope ever is. At 500 and above the message is replaced with the request could not be served and the code is internal_error — a failure of the Platform’s never carries a code naming a fault in your request, and its request_id is how you name it to an operator. Errors states every field.
Every response carries Request-Id, the Platform’s identifier for the request and the one its own log records. Send your own in Request-Id and it comes back as Caller-Request-Id, recorded beside the Platform’s and never in its place.
Every response also carries Contract-Version, the version of the contract it was rendered under. Send the version your client was built against in the same header; without one you are served the baseline, 2026-10-15. See the changelog.
The catalogue of codes is append-only: a published code is never renamed and never returns for a different kind of refusal. Treat a code you do not recognise as an unfamiliar refusal, not as a fault in your client.
Reading Server-Timing
Every response carriesServer-Timing: the time the Platform spent on the request, in milliseconds.
A signed read
The four parts come once your credential is proved, on answers, refusals and failures alike. A part the request did not incur is
0.0, never missing. A request refused as unauthenticated carries total alone.
The parts are parts, not a split of total: a statement runs while a connection is held, and reading the request and writing the answer belong to no part. What you measure less total is time outside the Platform — the network and your own side of it. The Platform serves both doors from Google Cloud’s northamerica-south1 region (Querétaro, Mexico), so callers in or near it pay the least distance.
The Platform’s request log records the same five metrics under the response’s Request-Id. Quote it with the header when a request was slow.
Versioning
The path carries the version,/v1, and every route in this reference lives under it. A /v2 is reserved for a redesign of the whole API. Within /v1 the contract is versioned by date in the Contract-Version header: what counts as breaking, the window a breaking change passes through, and every version are on the changelog. Objects version themselves where a version is the object’s own fact: a Configuration by version, a policy version by version and digest, and a Loan keeps the Configuration version it was issued under.
Bounds
The defaults every Tenancy starts on, counted per serving instance. Every refusal by a bound reports itself to the operator, so a Tenancy that keeps meeting one is seen rather than silently throttled.
Both
429s carry Retry-After in whole seconds, never zero, computed from the bound’s own arithmetic — a caller that honours it spreads out instead of synchronising with every other retry. Their details.bound is the number in force for you.
A request within these numbers is never refused for a bound. Where the Platform runs more than one instance you may be admitted more; that is never promised. Read the bounds in force for you — a Tenancy may be given its own.
What is safe to retry
Reads always. Mutations by their own idempotency, and every mutating route has one.
The answers that say send it again carry
retry: as_is: 409 contended — another write for the same client held its turn past two seconds, and nothing was written; a 429, after its Retry-After; and 503 unavailable from the door, after its Retry-After of 5 seconds. retry_after in the envelope is the same number as the header.
The Idempotency-Key you send is the fifth line of the signed canonical string, so a keyed retry re-signs with the same key, the same body and a fresh timestamp.
The key in detail
Two routes read it — creating a Configuration and writing a new version of one — and they are the only mutations the Platform serves that carry no reference of your own: it composes the identifier and takes the version number. Send it on any other route and it is signed, ignored, and the reference governs the repeat; you never have to decide per route whether to key.
A refusal is a recorded outcome. If your first attempt was refused
422 and you retry under the same key with a corrected body, you get idempotency_mismatch — the key already names the request you sent first. A corrected request is a new request: give it a new key.
A 5xx is recorded too, and replayed for the window. That is deliberate: a commit whose answer was lost is indistinguishable from no act at all, and replaying what happened is safer than performing it again. If you meet one, read the listing — if the act is absent, send it under a new key.
Two first attempts under one key that overlap in time are both performed. Both callers receive whichever outcome was recorded first, and where the act had an effect the second one stands as an ordinary record. The Platform promises once-only for attempts that follow one another, not for attempts that race, and says so here rather than holding a connection open across the act to make it true.