Overview
Tanzanite’s integration API lets an external product — a game editor, a document tool, a CAD package, a CRM — put a Tanzanite agent inside its own interface. Your users connect their own Tanzanite account once, and from then on your backend can create agent sessions, generate images, ask vision questions and make model calls on their credits, inside a workspace scoped to your app.
A product that integrates this way is called a host app: your app hosts the agent, rather than the agent hosting your app. That word is used consistently throughout these pages.
Three things are worth understanding before you write any code, because each one prevents a class of mistake later:
- There are two API surfaces, and only one of them is for you.
- An integration is two records, not one — a definition and a connection — and they have different lifetimes.
- Every session is confined to a project, and a project belongs to one user of one host app.
The two API surfaces
Section titled “The two API surfaces”| Path | Auth | For | Versioned |
|---|---|---|---|
/api/* |
Firebase ID token (session auth) | Tanzanite’s own first-party app backend | No — may change without notice |
/v1/* |
Tenant key, or a session-scoped embed ticket | Third-party host apps (this API, and the SDK) | Yes |
Everything on this site’s developer pages is on the API origin,
https://api.tanzanite.ai.
/api/* is the first-party app talking to its own backend. It is not a public
contract, it carries no compatibility promise, and it is documented here only
where the Connect flow crosses it. /v1 does not accept Firebase ID tokens —
if you have one, you are on the wrong surface.
/v1 is the surface for host apps. Its promise is narrow and therefore keepable:
changes within /v1 are additive only — new endpoints, new optional request
fields, new response fields. Anything that would break an existing caller gets a
/v2 instead. Write your clients so an unknown response field is ignored rather
than rejected, and you will not have to touch them.
There is deliberately no OpenAPI document: the route count is well below the point at which a generated spec pays for its own drift, so these pages plus the typed SDK are the contract.
Authentication
Section titled “Authentication”A tenant key is minted for one user’s connection to one host app at the end of
the Connect flow. It looks like tanzanite_… and it is a
backend credential. Send it either way:
curl https://api.tanzanite.ai/v1/connection \ -H "Authorization: Bearer tanzanite_live_xxxxxxxxxxxx"
# equivalentcurl https://api.tanzanite.ai/v1/connection \ -H "X-Tanzanite-Key: tanzanite_live_xxxxxxxxxxxx"The key is returned exactly once, by the token exchange, and is never retrievable again. Store it server-side, encrypted at rest, keyed by your own user id. It must never reach a browser, a mobile binary, or a log line.
Browsers get tickets instead. A ticket is short-lived, bound to one session,
and reaches session-scoped routes only — never a connection-scoped one. That is
what lets an embedded widget stream an agent’s output without ever holding a
credential that could create sessions or spend money on its own. See
POST /v1/stream-ticket and POST /v1/sessions/:id/embed-ticket.
The two records
Section titled “The two records”An integration is a definition plus a connection, and conflating them is the most common early mistake.
The host-app definition is authored once on a Tanzanite account and describes
your app for every user who will ever connect: its name, description and icon,
its baseUrl and toolsUrl, its tool catalog, the prompt context its agents
carry, the harness tool groups its sessions may use, the scopes it requests, its
redirect URI allow-list, and a version. Its id doubles as your OAuth
client_id.
Authoring is self-serve. A new definition is private: connectable by your own account only, so you can connect it, run it and iterate with nobody else exposed. Being connectable by other accounts (published) is a review:
- You ask — Request publication in Tanzanite → Host apps. The platform checks a short list first: the app is private (not a draft), has at least one redirect URI, a tool catalog, a consent-screen description, no reserved word in its name or id, and a connection from your own account — an app its author never ran is not ready for anyone else. Anything missing is listed in one response.
- Tanzanite decides — approve (optionally marking the app verified, which changes what the consent screen says about it) or reject with a reason you see in the dashboard. Withdraw a pending request at any time.
A published app is published at the shape a reviewer saw. An edit that widens
its reach — a new tool group, a new scope, a higher dangerLevel ceiling —
bumps its version and returns it to review. It stays connectable by your
account meanwhile; other accounts can connect again once the new version is
approved, and existing connections must re-consent to it.
Names are yours to choose except the platform’s own: tanzanite, tanz,
official and verified are refused as whole words in a name or id, in either
case, so a consent screen cannot be dressed as first-party.
A connection is one user’s grant to one host app: the tenant key, the scopes
they consented to, the definition version they consented at, a daily spend cap
and an in-flight session ceiling. One user connecting to your app twice does not
produce two live connections — reconnecting mints a new one and the platform
revokes the previous one for that same user and app.
The division exists because the alternative failed in a specific way: when the app’s identity travelled with each user’s token exchange, the last caller silently redefined the app for everyone, and the tool catalog was authored once per connection instead of once per app. Now the exchange carries only your own per-user credential, and nothing a caller sends can change what the app is.
Reads are keyed on the host app, not the connection
Section titled “Reads are keyed on the host app, not the connection”Every /v1 read is scoped by (user, host app) — never by connection id. This
matters the first time a user reconnects: their projects, their session history
and their spend all stay exactly where they were, because none of it was filed
under the connection that was just replaced. A user disconnecting and
reconnecting your app is a routine, non-destructive act.
Scopes
Section titled “Scopes”A connection holds a subset of the scopes its definition requests. Every /v1
route names the one scope it needs; a route called without it is refused with
403 and { "error": "insufficient_scope", "scope": "…" }.
| Scope | Grants | Spends credits |
|---|---|---|
agent.sessions |
Create and drive agent sessions in this app’s projects | Yes |
projects |
Create, list and read this app’s projects and their sessions | No |
media.generate |
POST /v1/media/generate (images, speech, and 3D models as a polled job), GET /v1/media/jobs/:id |
Yes |
media.vision |
POST /v1/media/vision |
Yes |
completions |
POST /v1/completions |
Yes |
decisions |
POST /v1/decisions |
Yes |
On the wire, scopes are space-separated, in vocabulary order (RFC 6749 §3.3):
agent.sessions projects media.generate. The consent screen marks the
credit-spending ones explicitly, so ask only for what you use — a scope list that
includes three spend scopes for a feature that generates one image is a consent
screen users abandon.
Rate limits
Section titled “Rate limits”Three limits apply, and they exist for different reasons:
- 1 200 requests per minute per IP, applied before authentication and failing closed. This one protects the front door, so an unauthenticated flood is rejected without ever costing a database read. A request refused here has not been attributed to your connection.
- 300 requests per minute per connection, applied after authentication.
- A ceiling on concurrent sessions per connection — sessions that are still
running, not requests. The default is 3 and the maximum a user can raise it
to at consent is 20. Exceeding it is
429with{ "error": "too_many_sessions", "limit": 3 }, which is retryable the moment one of your sessions finishes.
The concurrency ceiling is separate from the request rate limit on purpose: they have different remedies. A rate limit is answered by slowing down; a session ceiling is answered by finishing or cancelling work, or by asking the user to raise it.
Spending
Section titled “Spending”Everything a host app does that costs money is charged to the connected user’s credit balance, never to yours. Two things can stop a charge:
- The user has no credits —
402with{ "error": "insufficient_credits" }. Retrying changes nothing; they must add credits. - The daily cap the user set on this connection at consent is spent for the UTC
day —
402with{ "error": "host_app_spend_cap" }. The wallet has money; this connection may not spend more of it today.
Render those two differently. Showing “add credits” for a spend-cap refusal sends the user to the wrong screen.
To check before you start, GET /v1/wallet (any spend scope, free) returns what
this connection can spend right now — the wallet, this connection’s daily cap, the
account’s caps and auto-reload — and spendableUSD, the least of them.
Each spend request may carry a tag and a label the user sees on their Usage
tab and CSV: tag is 1–64 letters, digits, _ - : . (bot-live, or one per
thing you serve, like game:abc123), label a line of up to 120 characters.
Where to go next
Section titled “Where to go next”- Connect — the OAuth 2.0 + PKCE flow that mints a connection, and what the consent screen shows the user.
- Projects & sessions — the unit of work, and exactly what a session can and cannot reach.
- Tool definitions — publishing the tools your app exposes to the agent.
- Media — the synchronous image and vision endpoints, the asynchronous 3D model endpoint, and why they differ.
- API reference — every
/v1route, field by field. - SDK — the typed TypeScript client for all of the above.