Concepts
The handful of ideas behind every Sess integration. Terms in bold are used with exactly this meaning across the docs and the API.
On this page
Session
A Session is one handoff between your agent and a person. It has an ID such as ses_01JABCDEFGHJKMNPQRSTVWXYZ0, holds the current revision of what the person sees, and controls access and expiry. Every Session belongs to a workspace: a trial has its own, and owner-key Sessions in the preview belong to the operator workspace. Your agent creates a Session with POST /v1/sessions (API key) or POST /v1/trial/sessions (trial).
An interactive Session is active while it waits for an answer and responded once Sess accepted one. Your agent then either publishes the next revision, which makes it active again on the same ID and link, or completes it. completed is final: a completed Session never reopens. responded means one round was answered, not that the Session is finished.
A display Session (mode: display) only shows data and accepts no input.
Surface and immutable revisions
A Surface is what the person sees: a title, an optional description, and components. Each Surface is stored as a numbered revision that never changes once written. Read any earlier revision exactly as stored with GET /v1/sessions/{id}/surfaces/{revision}.
Components are a small documented set: heading, text, read-only record (key/value pairs) and field (an input). Everything is shown as text: Sess never renders HTML, scripts, styles or remote content from your data, and an unknown component is rejected with UNKNOWN_COMPONENT rather than rendered approximately.
Owner reads carry an ETag. For an interactive Session it combines the revision and the number of Events, such as "2.3", so it changes whenever the person answers or your agent updates the Session. Updates and completion require it in If-Match.
Inputs, actions and submissions
A field is a labelled input described by a small subset of JSON Schema 2020-12: a string (a text box, or a list to choose from with enum), a number or integer with bounds, a boolean checkbox, or an array of enum IDs (selection checkboxes). A correction is a field with a proposed value. Any other schema keyword fails with INVALID_SCHEMA.
An action is a button you declare on a revision, with an ID, a label and an optional intent: submit, continue, approve, reject or request_change. A revision has up to 5 actions.
The person's page submits exactly four things: a client event ID, the revision it was made against, the action ID and the field values. The browser can never choose where the result goes or change your policy: the server resolves the action from the exact stored revision and validates the values against that revision's schema. Submissions are accepted only from the page itself.
Events and typed results
When Sess accepts a submission it records an Event: a stable ID, its sequence within the Session, the time, the revision, the action and the validated input. The typed result is that input, valid against the response_schema Sess derived from your fields. Each revision accepts one response (response_policy: first); a second submission to the same revision, even from another device, is refused and does not replace it.
Your agent reads results by polling: the Session's responses list every accepted result with its revision, and GET /v1/sessions/{id}/events lists every Event in order (response.accepted, revision.published, session.completed). Events are append-only: earlier results stay listed after later revisions and completion. Repeated reads of an unchanged Session return identical bytes.
With an API key, your workspace can also register one HTTPS endpoint and receive each accepted response as a signed webhook (see Delivery). Trials never send webhooks.
Receipts
A receipt is a signed statement of what Sess accepted, issued once when the response is accepted and never re-signed. It is a compact JWS (Ed25519, alg EdDSA, typ sess-receipt+jwt) whose claims include the workspace, Session, Event and step IDs, the revision with a hash of its normalized content, the exact action ID, label and summary, a hash of the validated input, the responder's assurance and a workspace-scoped subject, the acceptance time, an expiry, and the return binding.
Verify it with the key named by its kid from GET /.well-known/sess-receipt-keys.json: the issuer is the API origin, the scheme and host you send API requests to, wherever human links are served and the audience is the Session's workspace. The key set keeps older keys during a rotation. See Verify the signed receipt.
A receipt does not prove that the person understood every field, that their device displayed the page correctly, or that your agent or any other system then did the work. It records what the server accepted. In the invoice example, Sess records the choice; it does not pay the invoice or verify the person’s identity (the responder's assurance is link_holder: whoever held the link).
Credentials and audiences
Sess uses separate credentials for separate audiences. They are never interchangeable, and none ever goes in a URL except the human link itself.
| Credential | Who holds it | What it allows |
|---|---|---|
| API key (owner credential) | Your agent or backend | Create, read, update and complete Sessions, read Events and revisions, and manage the webhook endpoint, sent as Authorization: Bearer …. Created and revoked in your developer account. Draft — lands with P1 increment 2 The deployment's own operator key is held by the Sess team and never issued to developers. |
Trial management credential (sess_mg_…) | Whoever started a trial | Returned once, when the trial is created. Reads that trial's Sessions and results until their retrieval deadline and creates more Sessions in the trial. It cannot publish revisions, complete Sessions or configure webhooks, and it is not an API key. Anything else it is used for answers a generic 404 NOT_FOUND. |
| Human link | The person you send it to | Open that one Session's page and submit its declared actions. It can never call the owner or trial API, read your private metadata or change where results go. |
A human link carries a random 256-bit token. Sess stores only a hash of the token and finds the Session by that hash; the token itself is never stored. Trial management credentials are random secrets too, stored only as hashes. The deployment's operator key is a secret held by the Sess team and compared in constant time. API keys are random secrets with at least 128 bits of entropy, stored only as hashes. Draft — lands with P1 increment 2 A wrong, expired or revoked human link shows the same generic "not found" page as a link that never existed.
Draft — lands with P1 increment 2 Planned; not available on the current API.
Signing in to your account (with an email link) proves who you are as a developer; it says nothing about the person who opens a human link. A trial can be claimed into an account with its management credential, never with the human link.
Idempotency and retries
Reads are always safe to repeat.
Updates (PATCH) and completion accept an Idempotency-Key header, scoped to your credential, the operation and the Session. The same key with the same request (body and If-Match) returns the original result; the same key with a different request fails with 409 IDEMPOTENCY_CONFLICT. Sess never deduplicates by comparing content.
Creates refuse an Idempotency-Key header, because a replay would have to return the human link again and Sess keeps only its hash. If a create times out, a retry may create a second Session.
A person's repeated click carries the same client event ID and is accepted once, with the same result and receipt.
Revision conflicts
If your agent publishes revision 2 while the person still has revision 1 open, their submission names revision 1 and is refused with SESSION_REVISION_CONFLICT. It is never silently applied to revision 2: the page shows the new revision with the person's safe draft values filled in and asks them to submit again.
Your agent's own updates are protected the same way. PATCH and complete need the current ETag in If-Match: a stale one fails with 412 SESSION_REVISION_CONFLICT, a missing one with 428 PRECONDITION_REQUIRED, and a weak ETag (W/"…") never matches.
Delivery and uncertain outcomes
An accepted response, its Event, its receipt and, when your workspace has a verified webhook endpoint, its delivery job are written in one database transaction, so a crash cannot lose an accepted answer or its pending delivery, and polling always returns it.
Webhook delivery is at least once. Transient failures (5xx, 429, 408, 425, timeouts and disconnects) are retried with jitter, up to 12 attempts within 24 hours, always with the same body and receipt; other 3xx and 4xx answers end the delivery as failed. Your receiver should deduplicate on webhook-id, which is the Event ID. Every attempt is listed in GET /v1/sessions/{id}/deliveries, and any accepted Event can be redelivered. A 2xx means your endpoint accepted the delivery; a webhook signature proves the delivery came from Sess. Neither proves that any upstream work happened. A timeout or dropped connection is never recorded as delivered. See Receive results by webhook.
Errors
Every API error has the same JSON shape. Branch on code; the message is for people and may change. Errors on the person's page are HTML pages instead.
{
"error": {
"code": "SESSION_NOT_FOUND",
"message": "Session not found.",
"request_id": "req_00000000-0000-4000-8000-000000000000"
}
}
The API reference lists every code, and the quickstart explains the common ones.
Gateway errors and unseen outcomes
Not every error comes from Sess. The hosting platform's gateway may answer 502 or 504 with an HTML page instead of the JSON error above, and it may turn a Sess 503 into an HTML 504. Treat any 5xx response whose body is not JSON as retryable. A gateway error, a timeout or a dropped connection does not tell you whether Sess acted, so treat the outcome as unknown, not as failed:
- Updates and completion: send an
Idempotency-Keyon every update and completion, and retry with the same key, body andIf-Match. If the first attempt took effect, the retry returns its original result instead of acting twice. Without a key, a retry after an applied first attempt answers412(update) or409 SESSION_COMPLETED(completion), which does not mean your first attempt failed; read the Session to see its current revision, status and ETag. - Creates take no
Idempotency-Key, so a retry creates a new Session. A Session made by the unseen attempt cannot be looked up, because its ID and human link were only in the lost response; nobody receives its link, and it expires as described below. - The person's page: the form carries a client event ID, so submitting the same form again is accepted at most once, with the same result and receipt. Your agent reads the Session to learn whether an answer was accepted.
Draft — lands with P1 increment 2 Planned; not available on the current API.
Webhook setup: the signing secret appears only in the response that created it. If the response to your first PUT /v1/webhook is lost, a retry answers with signing_secret: null; rotate the secret (POST /v1/webhook/secrets/rotate) to get one you can see. A retried rotation rotates again, and the newest secret is the one returned.
Expiry and revocation
A human link for an owner-key Session expires 30 days after the Session is created. For a Session created with a developer account's API key, it expires 7 days after the Session is created. Draft — lands with P1 increment 2 A trial's human link accepts answers for 1 hour (action_expires_at), and its result stays readable for 24 hours after that (result_available_until). Expiry stops new input; it never discards a response that was already accepted. Sess can revoke a single Session's link immediately; after expiry or revocation the link shows the generic "not found" page and nothing else.
Draft — lands with P1 increment 2 Planned; not available on the current API.
Every Session gets documented deadlines for new input, for delivering an accepted result, and for deleting the data. See Retention and deletion.