Documentation menu

Quickstart

Get a typed result from a person in a few minutes with curl and no signup. Then verify its signed receipt. With an API key you can run three rounds on one Session.

On this page

Prerequisites

  • A terminal with curl, and jq to pick fields out of JSON (or copy them by hand).
  • A browser, on a phone if you have one nearby: you will answer as the person.
  • Node.js 20 or later for receipt verification (step 4).
  • The API base URL: https://api.sess.si.

Which credential?

Every credential is a secret sent as Authorization: Bearer …. None ever goes in a URL.

Credentials used in this quickstart
CredentialHow you get itWhat it can do
Trial management credential (sess_mg_…)Returned once, in the body of a trial create. No signup.Read the Sessions and typed results of its own trial, and create more Sessions in that trial. It cannot publish a next revision, complete a Session or set up webhooks, and it is not an API key.
API keyCreated 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.Create, read, update and complete Sessions; read Events and earlier revisions; register a webhook endpoint.
Human linkReturned by every create as human_url.Lets one person open that Session's page and answer. Send it only to them.

1. Try it: create a trial Session

Set the API address, and make new files readable only by you, since one of them will hold the management credential:

Shell
export SESS_API="https://api.sess.si"
umask 077

Save the review your agent wants: three synthetic invoices, a proposed amount to correct, and a list to select from.

Shell: write review.json
cat > review.json <<'JSON'
{
  "interaction": {
    "mode": "interactive",
    "title": "Review three synthetic invoices",
    "description": "Synthetic data. Correct the amount if needed and select the invoices to approve.",
    "source": {
      "kind": "sess_surface",
      "components": [
        { "type": "record", "label": "INV-1", "fields": { "id": "INV-1", "vendor": "Acme", "amount": 120 } },
        { "type": "record", "label": "INV-2", "fields": { "id": "INV-2", "vendor": "Globex", "amount": 75.5 } },
        { "type": "record", "label": "INV-3", "fields": { "id": "INV-3", "vendor": "Initech", "amount": 990 } },
        {
          "type": "field",
          "name": "inv_3_amount",
          "label": "INV-3 amount",
          "required": true,
          "schema": { "type": "number", "minimum": 0, "maximum": 100000 },
          "value": 990
        },
        {
          "type": "field",
          "name": "approved",
          "label": "Invoices to approve",
          "schema": {
            "type": "array",
            "items": { "type": "string", "enum": ["INV-1", "INV-2", "INV-3"] },
            "maxItems": 3
          },
          "value": []
        }
      ]
    },
    "actions": [{ "id": "submit", "label": "Submit review", "intent": "submit" }]
  }
}
JSON

Create the trial. No Authorization header: this request starts a new trial.

Shell: create a trial Session
http_status="$(curl -sS -X POST "$SESS_API/v1/trial/sessions" \
  -H "Content-Type: application/json" \
  --data-binary @review.json \
  -D headers.txt -o trial.json -w '%{http_code}')"
echo "$http_status"

Expected result: 201. trial.json now holds (your values differ):

trial.json
{
  "id": "ses_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "revision": 1,
  "human_url": "https://sess.si/s/{token}",
  "workspace_id": "wks_0123456789abcdef0123",
  "management_credential": "{management credential, shown only in this response}",
  "action_expires_at": "2026-01-01T01:00:00.000Z",
  "result_available_until": "2026-01-02T01:00:00.000Z",
  "trial_expires_at": "2026-01-02T00:00:00.000Z"
}
Shell: keep the ID and the management credential
if [ "$http_status" = "201" ]; then
  export SESSION_ID="$(jq -r .id trial.json)"
  export SESS_TRIAL_KEY="$(jq -r .management_credential trial.json)"
else
  echo "No trial was created (HTTP $http_status): $(jq -r '.error.message // empty' trial.json)" >&2
  grep -i '^retry-after:' headers.txt >&2   # on 429: seconds until you can retry
fi

Stop here unless the status was 201: the next steps need SESSION_ID and SESS_TRIAL_KEY, and without them they fail with a misleading 404.

Next: the management credential is never shown again. Keep trial.json private and delete it when you are done. The person can answer until action_expires_at (1 hour); you can read the result until result_available_until (24 hours after that).

A trial can hold up to 3 active Sessions and create 10 in its 24 hours: send Authorization: Bearer $SESS_TRIAL_KEY with the same create request to add one.

If the create answers 429 RATE_LIMITED, no trial was created. The error's message says which limit was reached, and Retry-After (in headers.txt) says how many seconds until a retry can succeed:

  • This network's allowance: each network address can start 3 trials per rolling 24 hours. Everyone behind one office network, VPN or mobile carrier shares it, so it can be used up before your first request. Wait for Retry-After, or try from a different network.
  • The overall daily allowance: all trials for the day have been used, on every network. Wait for Retry-After.

The “See it work” demo starts a trial too, so it shares these limits. While you wait, the request and the expected responses on this page (in this step and in step 3) show the real exchange without using a trial.

2. Answer as the person

Print the human link and open it in a browser within the hour. In real use you would send this link only to the reviewer; never post it publicly or log it.

Shell
jq -r .human_url trial.json

On the page, change INV-3 amount to 909, tick INV-1 and INV-3, and choose Submit review. The page confirms that the response was accepted. Submitting twice, or from two devices, still records one response.

3. Read the typed result

Shell: read the trial Session
curl -sS "$SESS_API/v1/trial/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $SESS_TRIAL_KEY" \
  | jq '{status, responses: [.responses[] | {revision, action_id, data}]}'

Expected result:

Response (selected fields)
{
  "status": "responded",
  "responses": [
    { "revision": 1, "action_id": "submit", "data": { "inv_3_amount": 909, "approved": ["INV-1", "INV-3"] } }
  ]
}

The data object is the typed result, validated against the response_schema that Sess derived from your fields (also in the response). Reading never changes the Session, so polling is safe. To wait for the answer in a script:

Shell: poll until the person has answered
until curl -sS "$SESS_API/v1/trial/sessions/$SESSION_ID" \
    -H "Authorization: Bearer $SESS_TRIAL_KEY" | jq -e '.status == "responded"' > /dev/null; do
  sleep 5
done

Sess records the choice; it does not pay the invoice or verify the person’s identity. What happens next is up to your agent.

That is the whole first loop: one create, one submission, one read. Next: check the receipt, or read on for more rounds. A trial Session answers one round; publishing a next revision needs an API key.

4. Verify the signed receipt

Each accepted response carries a receipt: a compact JWS signed with Ed25519 (alg EdDSA, typ sess-receipt+jwt, and a kid). The public keys are at /.well-known/sess-receipt-keys.json. The issuer is the API origin (the scheme and host of SESS_API, which stays the issuer wherever human links are served), and the audience is the Session's workspace (for a trial, its workspace_id). This uses the maintained jose library; no Sess SDK is needed.

Shell
npm install jose
JavaScript, verify-receipt.mjs
import { readFile } from 'node:fs/promises';
import { createRemoteJWKSet, jwtVerify } from 'jose';

const api = process.env.SESS_API;
const trial = JSON.parse(await readFile('trial.json', 'utf8'));
const res = await fetch(`${api}/v1/trial/sessions/${trial.id}`, {
  headers: { Authorization: `Bearer ${process.env.SESS_TRIAL_KEY}` },
});
const session = await res.json();
const keys = createRemoteJWKSet(new URL(`${api}/.well-known/sess-receipt-keys.json`));

for (const response of session.responses) {
  const { payload } = await jwtVerify(response.receipt, keys, {
    algorithms: ['EdDSA'],
    typ: 'sess-receipt+jwt',
    issuer: new URL(api).origin,
    audience: trial.workspace_id,
  });
  console.log(payload.event_id === response.event_id, payload.surface.revision, payload.action.id);
}

Expected output: true 1 submit. The receipt records what Sess accepted: the Session, Event, revision and its hash, the action, a hash of the input, the responder's assurance (link_holder) and the time. It does not prove that the person understood the page or that anything happened afterwards.

The whole trial in one program

Plain HTTP; no SDK is needed (and none is published yet). Each version reads SESS_API and review.json, creates the trial, waits for the answer and prints the typed result.

Example language
Shell (curl and jq), trial.sh
http_status="$(curl -sS -X POST "$SESS_API/v1/trial/sessions" \
  -H "Content-Type: application/json" \
  --data-binary @review.json -D headers.txt -o trial.json -w '%{http_code}')"
if [ "$http_status" != "201" ]; then
  echo "Create failed: HTTP $http_status $(jq -r '.error.code // empty' trial.json 2>/dev/null)" >&2
  grep -i '^retry-after:' headers.txt >&2   # present on 429 RATE_LIMITED
  exit 1
fi
SESSION_ID="$(jq -r .id trial.json)"
SESS_TRIAL_KEY="$(jq -r .management_credential trial.json)"
echo "Open this link and answer: $(jq -r .human_url trial.json)" >&2

until curl -sS "$SESS_API/v1/trial/sessions/$SESSION_ID" \
    -H "Authorization: Bearer $SESS_TRIAL_KEY" | jq -e '.status == "responded"' > /dev/null; do
  sleep 5
done
curl -sS "$SESS_API/v1/trial/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $SESS_TRIAL_KEY" | jq '.responses[-1].data'
JavaScript (Node.js 20 or later), trial.mjs
import { readFile } from 'node:fs/promises';

const api = process.env.SESS_API;
const res = await fetch(`${api}/v1/trial/sessions`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: await readFile('review.json', 'utf8'),
});
const created = await res.json();
if (res.status !== 201) {
  throw new Error(`${res.status} ${created.error.code} (request ${created.error.request_id})`);
}
// Show the link only to yourself (you are the reviewer here); never log it in real use.
console.error('Open this link and answer:', created.human_url);

const auth = { Authorization: `Bearer ${created.management_credential}` };
for (;;) {
  const session = await (await fetch(`${api}/v1/trial/sessions/${created.id}`, { headers: auth })).json();
  if (session.status === 'responded') {
    console.log(session.responses.at(-1).data);
    break;
  }
  await new Promise((resolve) => setTimeout(resolve, 5000));
}
Python 3 (standard library), trial.py
import json
import os
import sys
import time
import urllib.request

api = os.environ["SESS_API"]
with open("review.json", "rb") as f:
    payload = f.read()

req = urllib.request.Request(
    api + "/v1/trial/sessions",
    data=payload,
    method="POST",
    headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
    created = json.load(resp)
# Show the link only to yourself (you are the reviewer here); never log it in real use.
print("Open this link and answer:", created["human_url"], file=sys.stderr)

auth = {"Authorization": "Bearer " + created["management_credential"]}
while True:
    poll = urllib.request.Request(api + "/v1/trial/sessions/" + created["id"], headers=auth)
    with urllib.request.urlopen(poll, timeout=30) as resp:
        session = json.load(resp)
    if session["status"] == "responded":
        print(session["responses"][-1]["data"])
        break
    time.sleep(5)

With an API key: rounds, history and webhooks

Draft — lands with P1 increment 2 Planned; not available on the current API.

An API key unlocks the full owner API for your workspace: more than one round per Session, the Event history, earlier revisions and webhooks. Create one in your account at https://app.sess.si and copy it once.

5. Run three rounds

An API key can take the same Session through several rounds: the person answers, your agent publishes the next revision, the person answers again on the same link, and your agent completes the Session.

Shell: read the API key without echoing it
read -rs SESS_API_KEY && export SESS_API_KEY   # paste the key, then press Enter

Round 1: create and read the answer

Shell: create the Session with the same review.json
curl -sS -X POST "$SESS_API/v1/sessions" \
  -H "Authorization: Bearer $SESS_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @review.json \
  -o session.json -w '%{http_code}\n'
export SESSION_ID="$(jq -r .id session.json)"
jq -r .human_url session.json   # send this link to the person

Expected result: 201. After the person submits, read the Session and keep its ETag:

Shell: read the Session and its ETag
curl -sS -D headers.txt -o current.json \
  "$SESS_API/v1/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $SESS_API_KEY"
jq '{status, revision, result: .responses[-1].data}' current.json
export ETAG="$(grep -i '^etag:' headers.txt | cut -d' ' -f2- | tr -d '\r')"

Expected result: "status": "responded", revision 1, the typed result, and an ETag such as "1.1" (revision 1, one Event so far). Use the ETag exactly as returned; it is a strong validator and changes whenever the Session does.

Round 2: publish the next revision

Shell: write round2.json
cat > round2.json <<'JSON'
{
  "interaction": {
    "mode": "interactive",
    "title": "Confirm the approved invoices",
    "description": "Synthetic data. INV-1 and INV-3 are approved; INV-3 was corrected to 909.",
    "source": {
      "kind": "sess_surface",
      "components": [
        { "type": "record", "label": "INV-1", "fields": { "id": "INV-1", "vendor": "Acme", "amount": 120 } },
        { "type": "record", "label": "INV-3", "fields": { "id": "INV-3", "vendor": "Initech", "amount": 909 } },
        {
          "type": "field",
          "name": "payment_note",
          "label": "Note for the payment run",
          "schema": { "type": "string", "maxLength": 200 }
        }
      ]
    },
    "actions": [
      { "id": "confirm", "label": "Confirm", "intent": "approve" },
      { "id": "change", "label": "Request a change", "intent": "request_change" }
    ]
  }
}
JSON
Shell: publish revision 2
curl -sS -X PATCH "$SESS_API/v1/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $SESS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "If-Match: $ETAG" \
  -H "Idempotency-Key: round-2-$SESSION_ID" \
  --data-binary @round2.json \
  -o current.json -w '%{http_code}\n'

Expected result: 200, with "status": "active" and "revision": 2. The same link now shows revision 2; the person reloads it, adds a note and chooses Confirm. Repeating this request with the same Idempotency-Key returns the same result instead of publishing twice. Read the Session again as in round 1 to get the result and the new ETag (for example "2.3").

Round 2 typed result (example)
{ "payment_note": "Pay both in the next run." }

Round 3: one more revision, then complete

Shell: write round3.json
cat > round3.json <<'JSON'
{
  "interaction": {
    "mode": "interactive",
    "title": "Choose the payment date",
    "description": "Synthetic data. Two approved invoices, 1,029 in total.",
    "source": {
      "kind": "sess_surface",
      "components": [
        { "type": "text", "text": "Pick the run in which INV-1 and INV-3 are paid." },
        {
          "type": "field",
          "name": "pay_on",
          "label": "Payment run",
          "required": true,
          "schema": { "type": "string", "enum": ["2026-11-02", "2026-11-09"] },
          "option_labels": { "2026-11-02": "Monday 2 November", "2026-11-09": "Monday 9 November" }
        }
      ]
    },
    "actions": [{ "id": "schedule", "label": "Schedule payment", "intent": "submit" }]
  }
}
JSON

Publish it with the same PATCH command (using round3.json, the latest ETAG and a new key such as round-3-$SESSION_ID). After the person answers, read the Session again for the result and the ETag (for example "3.5"), then complete the Session:

Round 3 typed result (example)
{ "pay_on": "2026-11-09" }
Shell: complete the Session
curl -sS -X POST "$SESS_API/v1/sessions/$SESSION_ID/complete" \
  -H "Authorization: Bearer $SESS_API_KEY" \
  -H "If-Match: $ETAG" \
  -o current.json -w '%{http_code}\n'
jq .status current.json

Expected result: 200 and "completed". A completed Session accepts no further answer or revision.

Read the whole history

Shell: list the Events
curl -sS "$SESS_API/v1/sessions/$SESSION_ID/events" \
  -H "Authorization: Bearer $SESS_API_KEY" \
  | jq -c '.events[] | {sequence, type, revision, action_id} | with_entries(select(.value != null))'
Expected output
{"sequence":1,"type":"response.accepted","revision":1,"action_id":"submit"}
{"sequence":2,"type":"revision.published","revision":2}
{"sequence":3,"type":"response.accepted","revision":2,"action_id":"confirm"}
{"sequence":4,"type":"revision.published","revision":3}
{"sequence":5,"type":"response.accepted","revision":3,"action_id":"schedule"}
{"sequence":6,"type":"session.completed","revision":3}

Each response.accepted Event also carries its typed data and receipt. To see exactly what the person saw in a round, read that revision: GET /v1/sessions/$SESSION_ID/surfaces/1.

6. Receive results by webhook

Instead of polling (or as well), Sess can POST every accepted response to one HTTPS endpoint for your workspace, signed with a per-workspace secret. This needs an API key; trials never send webhooks. You need an endpoint reachable at a public https:// URL on port 443.

Run a receiver

The receiver checks the Standard Webhooks v1 signature over the raw body before trusting anything, answers the verification challenge, and ignores repeats of the same webhook-id. Node.js, no dependencies:

JavaScript, receiver.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';

// whsec_… secrets in force: add the new one when you rotate, remove the old one after retire_at.
const secrets = [process.env.SESS_WEBHOOK_SECRET].filter(Boolean);
if (secrets.length === 0) throw new Error('Set SESS_WEBHOOK_SECRET to your whsec_… signing secret');
const seen = new Set(); // use your database in production

function verified(id, ts, body, header) {
  if (!id || !(Math.abs(Date.now() / 1000 - Number(ts)) < 300)) return false;
  const signatures = String(header ?? '').split(' ').map((s) => s.slice(3)); // drop "v1,"
  return secrets.some((secret) => {
    const mac = createHmac('sha256', Buffer.from(secret.slice(6), 'base64')) // drop "whsec_"
      .update(`${id}.${ts}.${body}`)
      .digest();
    return signatures.some((s) => {
      const given = Buffer.from(s, 'base64');
      return given.length === mac.length && timingSafeEqual(given, mac);
    });
  });
}

createServer((req, res) => {
  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const body = Buffer.concat(chunks).toString('utf8'); // the exact bytes that were signed
    const id = req.headers['webhook-id'];
    const ts = req.headers['webhook-timestamp'];
    if (!verified(id, ts, body, req.headers['webhook-signature'])) return res.writeHead(401).end();
    const msg = JSON.parse(body);
    if (msg.type === 'sess.webhook.verification') {
      res.writeHead(200, { 'content-type': 'application/json' });
      return res.end(JSON.stringify({ challenge: msg.challenge }));
    }
    if (!seen.has(id)) {
      seen.add(id);
      console.log(msg.session_id, msg.revision, msg.action_id, msg.data); // msg.receipt: the signed receipt
    }
    res.writeHead(200).end();
  });
}).listen(8443); // behind your HTTPS terminator, e.g. https://hooks.example.com/sess

Register and verify the endpoint

Shell: write hook.json
cat > hook.json <<'JSON'
{ "url": "https://hooks.example.com/sess" }
JSON
Shell: register the endpoint
curl -sS -X PUT "$SESS_API/v1/webhook" \
  -H "Authorization: Bearer $SESS_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @hook.json \
  -o webhook.json -w '%{http_code}\n'
jq '.endpoint | {id, status}' webhook.json

Expected result: 201 and "status": "pending". The first registration for your workspace also returns the signing secret, once, as signing_secret.secret (whsec_…); later registrations return null there. Give it to your receiver as SESS_WEBHOOK_SECRET (for example jq -r .signing_secret.secret webhook.json), then delete webhook.json. Sess keeps the secret only encrypted.

Shell: verify the endpoint
curl -sS -X POST "$SESS_API/v1/webhook/verify" \
  -H "Authorization: Bearer $SESS_API_KEY" -w '\n%{http_code}\n'

Sess POSTs {"type": "sess.webhook.verification", "challenge": "…"}, signed like a delivery, to the exact URL; your receiver echoes the challenge and the call returns 200. If it fails you get 422 WEBHOOK_VERIFICATION_FAILED with a reason such as egress_refused, redirect, bad_answer or timeout. Nothing is delivered until the endpoint is verified.

  • A verification holds for 30 days (verification_expires_at); verify again before then to renew it. After it lapses, nothing is delivered until you verify again.
  • Registering again, even with the same URL, replaces the endpoint: deliveries stop at once until the new registration is verified.
  • When an endpoint is replaced or its verification lapses, queued deliveries and retries end as endpoint_unavailable. The responses stay readable by polling; after verifying, redeliver them.
  • GET /v1/webhook shows the endpoint, the key IDs of the secrets in force and delivery counts with the age of the oldest pending delivery.

What arrives

Every response accepted while your endpoint is verified is POSTed as JSON:

Delivery body (example)
{
  "type": "response.accepted",
  "event_id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ1",
  "session_id": "ses_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "sequence": 1,
  "revision": 1,
  "action_id": "submit",
  "data": { "inv_3_amount": 909, "approved": ["INV-1", "INV-3"] },
  "accepted_at": "2026-01-01T00:00:00.000Z",
  "receipt": "{the signed decision receipt}"
}
  • Headers: webhook-id (the Event ID), webhook-timestamp (seconds), webhook-signature (v1,<base64 HMAC-SHA256> over id.timestamp.body, one per secret in force) and sess-webhook-key-ids (their key IDs).
  • Delivery is at least once: deduplicate on webhook-id, which is the same on every retry and redelivery. The body and the receipt are identical each time.
  • A 2xx answer means your endpoint accepted the delivery, not that your workflow succeeded. Answer within 10 seconds; Sess follows no redirects.
  • 5xx, 429 (with Retry-After honored up to 1 hour), 408, 425, timeouts and disconnects are retried with jitter, up to 12 attempts within 24 hours. Other 3xx and 4xx answers (including 401 and 403) are permanent failures.
Shell: delivery log and redelivery
curl -sS "$SESS_API/v1/sessions/$SESSION_ID/deliveries" \
  -H "Authorization: Bearer $SESS_API_KEY" | jq '.deliveries[] | {event_id, status, last_outcome}'

export EVENT_ID="evt_01JABCDEFGHJKMNPQRSTVWXYZ1"   # an event_id from the log or the Events list
curl -sS -X POST "$SESS_API/v1/sessions/$SESSION_ID/events/$EVENT_ID/redeliver" \
  -H "Authorization: Bearer $SESS_API_KEY" -w '\n%{http_code}\n'

Redelivery answers 202 and sends the same bytes, with the same webhook-id and receipt, to the current verified endpoint. If a delivery of that Event is already pending to the current endpoint, that pending job is returned in the delivery field and keeps its own retry budget; otherwise a new job starts with a fresh one.

Rotate the signing secret

Shell: rotate
curl -sS -X POST "$SESS_API/v1/webhook/secrets/rotate" \
  -H "Authorization: Bearer $SESS_API_KEY" -o rotated.json -w '%{http_code}\n'

Expected result: 201. The new secret is in signing_secret.secret, shown once. The previous secret keeps signing for 24 hours (previous.retire_at), so deliveries carry two signatures meanwhile: add the new secret to your receiver, then remove the old one after it retires. Delete rotated.json afterwards.

7. Claim a trial into your account

Claiming links a trial you started to your account, so that your API keys can read its Sessions. On your account's API keys page, under Trials, paste the trial's management credential ($SESS_TRIAL_KEY, from trial.json) and choose Claim trial. A claim needs both the management credential and your signed-in account. The human link can never claim a trial.

A claim changes nothing else. The trial's Sessions stay in the trial with the same limits, deadlines and deletion, their receipts stay as issued, and the management credential keeps working. One account can claim a trial; claiming it again from the same account changes nothing, and another account cannot claim it.

After the claim, read a trial Session with your API key, until that Session's result_available_until:

Shell: read a claimed trial Session
curl -sS "$SESS_API/v1/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $SESS_API_KEY" -w '\n%{http_code}\n'

Expected result: 200 and the Session, as with the management credential. Its Events and revisions are readable the same way. These reads are read-only: publishing a revision, completing the Session or redelivering an Event answers 403 ACTION_NOT_ALLOWED. From that Session's result_available_until on, it reads as not found, exactly as with the management credential.

If something fails

Common errors
Status and codeMeaningWhat to do
400 INVALID_SCHEMA or UNKNOWN_COMPONENTThe body does not match the contract, or uses a component or schema keyword outside the supported subset.Compare it with the schemas. Create requests refuse an Idempotency-Key header.
401 UNAUTHORIZEDMissing or wrong API key.Check SESS_API_KEY and the Bearer prefix.
404 NOT_FOUND on /v1/trialThe management credential is wrong, expired, or not for this Session; or the result is past result_available_until.Check SESS_TRIAL_KEY and SESSION_ID. A lost management credential cannot be recovered: start a new trial.
404 SESSION_NOT_FOUNDNo Session with that ID for this key.Check SESSION_ID.
412 SESSION_REVISION_CONFLICTYour If-Match ETag is not the current one: the person answered, or another update happened.Read the Session again and retry with the new ETag.
428 PRECONDITION_REQUIREDNo If-Match header on an update or completion.Send the ETag from the latest read.
409 IDEMPOTENCY_CONFLICTThe same Idempotency-Key was used with a different request.Use a new key for a new request.
409 SESSION_COMPLETEDThe Session is completed.Create a new Session. If you were retrying a completion whose response you never saw, the first attempt completed it.
400 WEBHOOK_INVALID_URL, 422 WEBHOOK_VERIFICATION_FAILED, 409 WEBHOOK_NOT_CONFIGUREDThe webhook URL is not a public https:// URL on port 443, your endpoint did not echo the challenge, or no endpoint is registered and verified yet.Check the reason in the error, fix the URL or the receiver, and verify again.
413 PAYLOAD_TOO_LARGERequest over 256 KiB or page content over 128 KiB.See Limits.
429 RATE_LIMITEDA trial limit is reached; the error's message says which. Before your first trial: this network's allowance (shared by everyone on an office network, VPN or mobile carrier) or the overall daily allowance. With a trial: its active-Session or creation limit.Wait for the time in Retry-After (or, for this network's allowance, try another network). With a trial, use its existing Sessions. See if the create is rate limited.
500 INTERNAL, 503A server error, or the trial is not available on this deployment (TRIAL_UNAVAILABLE).Retry a read later. Contact support with the request_id.
502, 504 or another 5xx with an HTML bodyThe hosting platform's gateway answered, not Sess (it may also show a Sess 503 as an HTML 504). The request may or may not have taken effect.Retry it as described in gateway errors and unseen outcomes.

On the person's page, an answer to a revision that has since been replaced is not accepted: the page shows the new revision with their safe draft values filled in and asks them to submit again.

If a create request times out or gets a gateway error, you cannot tell whether the Session was created: a retry may create a second Session (and, for a trial, a second trial). Updates and completion are safe to retry with the same Idempotency-Key.

Where next