Skip to content

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.
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.

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:

Terminal window
curl https://api.tanzanite.ai/v1/connection \
-H "Authorization: Bearer tanzanite_live_xxxxxxxxxxxx"
# equivalent
curl 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.

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:

  1. 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.
  2. 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.

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.

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 429 with { "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.

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 — 402 with { "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 — 402 with { "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.

  • 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 /v1 route, field by field.
  • SDK — the typed TypeScript client for all of the above.