Connected applications
A registered partner application calls Tessera keylessly. It authenticates with a Google-signed OIDC ID token minted by its own workload identity, and it acts only on the organizations and projects it created. This page is the integrator's reference (WP-A9).
Authentication
- Mint an ID token for the environment's exact audience (no trailing
slash, no
/v1), withformat=full. - Send it as
Authorization: Bearer <id token>to the API host or the ingest host. Both accept the same audience. - Tessera pins the token on the service account's email AND unique id
(
sub), requiresemail_verified: true(the boolean), and refuses tokens older than one hour. - A registered application never holds an API key.
Scoping
- Project operations need
X-Tessera-ProjectandX-Tessera-Environment. - Add
X-Tessera-Project-RefandX-Tessera-Org-Ref(yourexternal_refvalues) for defence in depth. A mismatch is the same 404 as a project that does not exist. - An environment outside your registry entry is 403
environment_not_allowed. - Human context goes in
X-Tessera-On-Behalf-Of: an opaque reference matching^[A-Za-z0-9._:-]{1,128}$, never an email or a name. It is recorded only asdetails.unverified_annotationon audit events.X-Tessera-Actor-Labelis refused.
Provisioning
- Create orgs and projects with
POST /application/orgsandPOST /application/orgs/{org_id}/projects. - The
external_refis the idempotency key. A repeat with the same body returns 200; a different body is 409 withresource_id. - Refs are retired on delete and never reused (409
external_ref_retired). AnIdempotency-Keyheader is ignored on these creates. A delete that Tessera operations discards as forged (see "While your application is disabled" below) retires nothing: the org or project returns with its ref, and a create with that ref is again a repeat of it. - Managed orgs start with zero members, on your configured plan.
GET /credential/ownedlists everything you created in one place: every org, a page at a time (cursor,limit, ordered byorg_id), each with every project you created in it. Every status is listed,deletinganddeletedincluded. Each org and project reads exactly asGET /application/orgs/{org_id}andGET /application/projects/{project_id}return it. Sessions and API keys get 403.
Members
People reach a managed org only when your user opts in and you provision them. Tessera sends no email or invitation.
- Add the person with
POST /application/orgs/{org_id}/members({email, role}, roleadmin,memberorviewer; neverowner). A new member is alwayspending, and the answer never says whether the email already has a Tessera account. The normalized email (trimmed, lowercased; ASCII or IDNA only) is the idempotency key: a repeat with the same role returns 200 with the member as it is, and another role is 409 withresource_id. - Mint a claim code with
POST /application/orgs/{org_id}/members/{member_id}:claim-code. It is returned once, valid for at most 15 minutes, single use, and replaced by the next code you mint. Show it to the person in your own product. - The person signs in to the Tessera console with the same, verified email
(email and password, or Google; not an SSO tenant) and enters the code on
the console's claim page. Link the person to
<console>/claim#code=<claim_code>(the console URL is in the per-environment constants): the code travels in the URL fragment, which the browser never sends to a server, and the page removes it from the address bar; the person still confirms with one click. Only then does the member showactive. Your product never handles the person's Tessera sign-in or session: it shows the code and sends the person to the console.
A pending member expires 72 hours after the add; add them again for a new
member_id. You list (GET .../members), re-role (PATCH .../members/{member_id})
and remove (DELETE .../members/{member_id}) only people you provisioned;
people added any other way are only counted, in unmanaged.members. A role
change or removal takes effect on the person's next request. Removal, or a
role below admin, revokes every API key that person created in the org.
A removed member's row keeps no email address; an expired one is scrubbed
by a scheduled pass, normally within minutes of its expiry (at most 25 a
pass every 5 minutes across the platform, oldest first). The org's audit
log is immutable: it keeps the redeeming person's email as the display name
of the actor that redeemed.
Once a person redeems, the org's audit log (which you read with
GET /application/orgs/{org_id}/audit-events) names them by their internal
Tessera user id as the actor of application_member.materialize and
role.assign.
Limits: members_per_org live members, and members_churn_per_day adds plus
removals in a rolling 24 hours (429 application_quota with quota; the
churn refusal also carries Retry-After). Adding or minting a code in an org
that is not active is 400 failed_precondition (org_not_active); a code
for a member who already redeemed is 400 failed_precondition
(member_already_active). Removal works in an org of any status, but it
counts toward the churn limit.
Once an org has used its members_churn_per_day (adds and removals together),
a removal is refused with 429 until the oldest counted add or removal leaves
the 24-hour window, so for up to 24 hours. During that time the person keeps
their access. Keep headroom for urgent removals: don't spend the whole budget
on adds. Removal is the only per-person cut-off. If an urgent removal is
blocked, contact Tessera operations: they can set your application's kill
switch, which stops every call of your application and every claim-code
redemption at once, but does not end that person's existing access by itself.
The person's redemption is refused:
- while your application is disabled (either kill-switch layer): 403
permission_denied(reason: application_disabled), or 503unavailablewithRetry-Afterwhen Tessera cannot read the kill-switch state; - while the org is suspended or being deleted: 400
failed_precondition(org_not_active); - for a code minted before your application's tokens were revoked as of an instant: the uniform 404.
A refusal does not use up the code; it stays valid until it expires.
Lifecycle
- Rename with
PATCH /application/orgs/{org_id}orPATCH /application/projects/{project_id}({name}). The onboarding mode is fixed at creation. - Suspend with
POST /application/orgs/{org_id}:suspendorPOST /application/projects/{project_id}:suspend, and lift it with:resume. The data is kept.- What it refuses. Within 5 seconds, every data-plane request to the
project is refused with 403
org_suspendedorproject_suspended, from your application, its people and any API key. Work started through the API is refused with it. - What it does not do. It freezes the project data plane only. Org-level reads continue for the org's people: the org, its project list, its members and, for admins, its audit log and model inventory. It removes nobody and revokes no key; to cut a person off, remove them. Nobody can redeem a claim code into a suspended org. Scheduled work (syncs, refreshes, scheduled exports, webhooks, alert sweeps) keeps running until slice 6b skips it.
- You keep the provisioning operations and the event-feed reads.
- What it refuses. Within 5 seconds, every data-plane request to the
project is refused with 403
- Delete with
DELETE /application/projects/{project_id}: the project readsdeletingwith itspurge_after(a 7-day grace), its API keys stop at once, and the purge then leaves adeletedtombstone. You cannot restore it: suspend first and keep your own grace window while suspended. (The one exception is a forged delete Tessera operations discards while your application is disabled, below.) DELETE /application/orgs/{org_id}deletes every project you created in the org the same way, and the org's people lose access at once. Once every project is purged the org readsdeleted: its people, SSO, domains, SCIM tokens and invites are removed, by a scheduled pass every 5 minutes after the last purge.- It is refused with 400
org_has_unowned_projectswhile the org holds a project you did not create, which only an org created before the managed-org guard can. You cannot see or delete that project; ask the org's owner to delete it (or Tessera operations), then retry.
- It is refused with 400
- Lifecycle changes on a
deletingordeletedobject are 400org_not_activeorproject_not_active. A repeat of a suspend, resume or delete answers 200 with the object as it is. - While Tessera operations has your application disabled with its database
kill switch, everything destructive it requested waits, and nothing is
canceled: project purges, org finalization, model-file and dataset-file
purges (including retention you shortened) and entity erasures. It resumes once the switch is
cleared. (The registry's
enabled: falsestops your calls but pauses nothing.) While the switch is set, nobody else changes the retention of the projects your application created: the people you provisioned keep their sessions, butputModelFileRetentionandputDatasetFileRetentionanswer them (and any API key) 403permission_denied(reason: managed_org). One exception: the event feed and event exports act on an entity erasure from the momentdeleteEntityaccepts it, and never read the switch. Within seconds the entity's feed items are redacted in storage, anentity_deleteditem is appended, and every existing export part holding the entity is rewritten without it. This also happens while the switch is set, whenever the feed or the export maintenance was still behind when the switch was set. These are copies derived from the event store; the stored events, serving and training keep the data until the erasure itself runs. - Before clearing it, Tessera operations reviews every paused item with you,
and you confirm, through your out-of-band contact, which requests were
yours. For the ones you confirm forged, while the switch stays set:
- A project or org delete whose purge has not started is discarded: the
project returns to
active(orsuspended) and its purge readscanceled, the org's delete is withdrawn, and their refs stay theirs. API keys, SCIM tokens and invites the delete revoked stay revoked. - An explicit model-file or dataset-file purge request is withdrawn, as
cancelModelFilePurgeandcancelDatasetFilePurgewithdraw one: the files return to the project's retention, which still applies. - A retention setting your application, or a person it provisioned,
changed last is restored (audited as an operator restore): by default
to the value it had before your application's changes (the value
before the oldest of its unbroken run of changes, as the audit
recorded them), or to another value you confirm (1 to 365 days, or
off). While the switch is set, Tessera can correct its own restore.
offmeans no retention: old dataset builds and archived models keep their files until someone requests their purge, and an old build keeps its snapshot, so an erasure waiting on it cannot complete. A dataset-file value that would leave a build your genuine erasures wait on unscheduled, or starting its purge after their 42-day bound, is refused. - A model your application, or a person it provisioned, archived is listed with who archived it: its retention purge waits until you confirm the archive (or it is treated like a forged item that cannot be withdrawn, below).
- A purge that has already started, or a forged archive, cannot be withdrawn. For such an item, you (as controller) and Tessera decide together whether the application stays disabled for good or the loss is accepted and the rest resumes. Kept disabled for good, nothing destructive it requested runs again, unless released: no project purge, org finalization, model-file or dataset-file purge of the projects it created (by request or by retention), and no erasure it requested that nobody released. Your genuine erasures still complete, once Tessera operations releases them and, while the switch stays set, what they wait on: the dataset builds whose snapshots they need, or, in a project being deleted, that project's purge (its deletion then runs, so the erasure completes with it). An erasure in a project whose delete you confirm forged completes once that delete is discarded, or, if its purge already started, once its loss is accepted and the purge released. An accepted loss takes two people at Tessera, each from an operator Build they submit themselves: one approves, the other accepts within a day, and either can revoke the approval before the switch is cleared (a purge released under it stops at its next step; an erasure Tessera already released, or a purge step already running, is not undone). Tessera's tooling checks that two different people submitted the two Builds and records both: the separation is checked and auditable, not enforced against an operator who alters the Build itself (a preventive control is planned before production). Everything else you confirm genuine runs once the switch is cleared.
- A project or org delete whose purge has not started is discarded: the
project returns to
- As the controller, keep your data subjects' erasure deadlines in view:
they keep running while your application is disabled, and meanwhile no
erasure can be requested in your managed orgs at all (
deleteEntityis yours alone there). Tessera operations can release specific paused erasures while the switch stays set (and the dataset builds or project purges a released erasure waits on, so it completes), and can discard the requests you confirm were forged. To tell them apart, check each pauseddeletion_idagainst the ids your own system received back fromdeleteEntity: any id you received is genuine for you, even if a forged request created the record first (a repeated request for the same entity is answered with the existing record). A discarded request never runs and no longer appears inlistEntityDeletionsorgetEntityDeletion. A request whose erasure has already started cannot be discarded. - What a discarded request's acceptance did stays, for the life of the
project: the entity's earlier feed items stay redacted (their payload was
removed at acceptance); every later event of the entity reaches your
event feed as
event_redactedand is left out of every later event export (serving and training still use those events); those redactions name the discarded deletion id, whichgetEntityDeletionanswers 404; every later export'sdeletionsobject lists that id withcompleted_atnull; and the entity's lines are gone for good from every export part that held them at acceptance (each part was rewritten without them and is downloadable again, with the other entities). The discard removes the raw id from the request's ownentity_deletedfeed item, which then reads like a completed deletion's (entity.idnull), although noentity_deletion_completeditem follows. A laterdeleteEntityof the same entity still starts a new erasure and erases it; when it completes, no feed item names the entity's raw id.
Exports
An application can export only to Tessera-managed storage:
projection_2dexports;- state snapshots to the default
gcsdestination, sent with{"destination": {"kind": "gcs"}}and nouri. The files land under the managed export prefix listed below and are served through signed URLs.
Any other destination is outbound delivery and returns 403
application_operation_not_allowed before anything is created:
- a
gcsdestination with auri, including a different prefix in the default bucket; bigquery,s3orsnowflake.
In a managed org, sessions and API keys cannot export at all (403
managed_org).
Leakage policy
The leakage policy is project-wide: one version governs both environments.
PUT /leakage-policy replaces it, and an application may only tighten it:
- keep every deny pattern and deny attribute of the current version (read it
with
GET /leakage-policy), and add your own; - never turn
identifier_value_scanorlabel_temporal_gateoff.
Anything else returns 403 permission_denied with the reason
application_policy_relaxation and the relaxed fields, and writes nothing.
Deny patterns Tessera registers for your label names count as current
patterns, so include them too.
File retention
Model file and dataset build retention (PUT /model-file-retention,
PUT /dataset-file-retention) are project-wide too. An API key or console
session acting in the test environment may only keep files longer, but an
application may turn retention on or shorten it in the projects it owns, also
when it acts in test: turning both on (for example at 6 days) is how the
partner flow bounds the time an erasure takes to complete.
Approvals and attribution
Contract approvals, onboarding approvals, domain-pack installs, alert rules
and every other write you make are recorded as application:<application_id>.
Send the reference to your own approval record in X-Tessera-On-Behalf-Of.
You keep the human approval evidence. See
Governance evidence.
Per-environment constants
| Constant | Our hosted deployment (test environment) |
|---|---|
| API and ingest audience | https://api.staging.tessera.zone |
Console (the claim page is /claim) | https://console.staging.tessera.zone |
Upload object path (signed upload URL host storage.googleapis.com) | gs://tessera-<env>-data/projects/<project_id>/uploads/<upload_id>/<filename> |
State-snapshot and projection_2d exports without a uri | gs://tessera-<env>-exports/projects/<project_id>/exports/<export_id>/, served through 7-day signed URLs |
Implicit ingest source for POST /events | app:<application_id>, one per project and environment (the app: source-name namespace is reserved: no caller can create or rename a source into it) |
Safe completeUpload object size | Pending (HD-114): measured on our hosted deployment with the first release of this slice (the whole object is hashed within the load balancer's 30 s window) |
| Connector egress IPs | Not applicable until the connector destination guard lands; applications are push-only |
Limits and errors
| Status | Code | details[0] |
|---|---|---|
| 400 | invalid_argument | field_violation, including the scoping headers and X-Tessera-On-Behalf-Of |
| 400 | failed_precondition | reason: org_not_active (an add, claim code or lifecycle change on an org that is not active), project_not_active (a lifecycle change on a deleting or deleted project), org_has_unowned_projects (an org delete while the org holds a project you did not create) or member_already_active (a claim code for a member who already redeemed), plus each operation's documented reasons |
| 401 | unauthenticated | none |
| 403 | permission_denied | reason: application_disabled, application_operation_not_allowed, application_source_kind_not_allowed, application_policy_relaxation, environment_not_allowed, project_suspended, org_suspended, managed_org or purpose_not_allowed |
| 404 | not_found | none: the uniform body for unknown, foreign and human-created objects |
| 409 | already_exists | reason: external_ref_retired or idempotency_key_reused, plus resource_id |
| 429 | resource_exhausted | rate_limit with bucket and class (always with Retry-After), or reason: application_quota with quota (with Retry-After for the per-day quotas) |
| 503 | unavailable | none, with Retry-After |
Your per-class limits (ingest, read, admin) are reported by
GET /credential.