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, andjqto 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.
| Credential | How you get it | What 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 key | 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. | Create, read, update and complete Sessions; read Events and earlier revisions; register a webhook endpoint. |
| Human link | Returned 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:
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.
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.
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):
{
"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"
}
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.
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
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:
{
"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:
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.
npm install jose
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.
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'
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));
}
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.
read -rs SESS_API_KEY && export SESS_API_KEY # paste the key, then press Enter
Round 1: create and read the answer
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:
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
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
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").
{ "payment_note": "Pay both in the next run." }
Round 3: one more revision, then complete
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:
{ "pay_on": "2026-11-09" }
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
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))'
{"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:
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
cat > hook.json <<'JSON'
{ "url": "https://hooks.example.com/sess" }
JSON
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.
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/webhookshows 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:
{
"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>overid.timestamp.body, one per secret in force) andsess-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
2xxanswer means your endpoint accepted the delivery, not that your workflow succeeded. Answer within 10 seconds; Sess follows no redirects. 5xx,429(withRetry-Afterhonored up to 1 hour),408,425, timeouts and disconnects are retried with jitter, up to 12 attempts within 24 hours. Other3xxand4xxanswers (including401and403) are permanent failures.
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
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:
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
| Status and code | Meaning | What to do |
|---|---|---|
400 INVALID_SCHEMA or UNKNOWN_COMPONENT | The 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 UNAUTHORIZED | Missing or wrong API key. | Check SESS_API_KEY and the Bearer prefix. |
404 NOT_FOUND on /v1/trial | The 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_FOUND | No Session with that ID for this key. | Check SESSION_ID. |
412 SESSION_REVISION_CONFLICT | Your 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_REQUIRED | No If-Match header on an update or completion. | Send the ETag from the latest read. |
409 IDEMPOTENCY_CONFLICT | The same Idempotency-Key was used with a different request. | Use a new key for a new request. |
409 SESSION_COMPLETED | The 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_CONFIGURED | The 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_LARGE | Request over 256 KiB or page content over 128 KiB. | See Limits. |
429 RATE_LIMITED | A 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, 503 | A 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 body | The 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
- Concepts: how revisions, results, receipts and credentials fit together.
- API reference: every field and response.
- Onboarding feedback: tell us where this quickstart slowed you down.