Sojourncase for agents
Agents: Sojourncase is a JSON-API app. Prefer the agentic layer — do not scrape
the DOM or drive the browser unless the user's task actually requires the human
UI (e.g. filling in a visual form), or the user explicitly tells you to. Every
human page mirrors to a JSON envelope: /build/<trackerId> → /.agentic/build/<caseId>/<trackerId>,
and any page URL fetched with Accept: text/markdown (or application/json)
returns its machine playbook directly. Authenticate once via
login.md, then read the
envelope and call the /api/* endpoints in its actions.
Sojourncase is a workspace for assembling an immigration petition — tracking evidence, building specialized trackers, drafting the petition brief, and managing the case end to end. This document is the front door for software agents. Everything a human can do in the UI, an agent can do through a stable, machine-readable surface.
Served at: https://sojourn.cross-fare.com/agents.md
If you are an LLM or autonomous agent, read this page first, then go straight to
GET /.agentic and follow the links.
Were you handed a human page URL? (the common case)
A person will paste you a normal link — "log in to …/today", "add my
conferences to …/build/<trackerId>". Do not open a browser or scrape the
DOM. Every page is mirrored as a native JSON API; use it over plain HTTP:
- Fetch the URL with
Accept: text/markdown(orapplication/json) — content negotiation returns that page's machine playbook (@type: "AgentLanding") withagenticView(the mirror URL),authenticate, andthenWhat. Just follow it. (If you only ever get HTML — e.g. you render in a browser — read the<script id="sojourn-agentic">manifest, the<link rel="alternate" type="application/json">in the head, or click the small</> APIchip in the corner; all point at this page's mirror.) - Authenticate (once) via the device-grant — you get your own scoped token
and the user approves in their browser:
login.md.
POST /.agentic/login/transient {name, entityId, tokenTtlSeconds}→ send the userloginUrl→claim→ack. Then sendAuthorization: Bearer sjc_agent_…on every request. Save the token in your workdir and reuse it while it is valid — do NOT repeat this flow per task. - Get the page's envelope. The machine mirror of a human URL is the same path
under
/.agentic(e.g./build/<caseId>/<trackerId>). If the human URL hides thecaseId(it usually does), start atGET /.agenticand follow_links, or use theagenticViewfrom step 1. - Act. The envelope's
actionsarray lists the exact/api/*endpoints (e.g.addRecord) with their fields. Call them with your Bearer token.
Every page also advertises its twin in metadata: <link rel="alternate" type="application/json"> (the standard one) plus <link rel="agentic-view"> /
<link rel="agentic-doc"> in the <head> (and <meta> mirrors), and a
Link: </agents.md>; rel="agentic-doc" HTTP header.
What the agentic layer is
The human app is a single-page React client that talks to a Rust/Axum JSON API.
Every screen is backed by REST endpoints under /api/*. The agentic layer
wraps that same API in three coordinated, agent-legible surfaces so you never
have to scrape HTML or guess at endpoints:
The three pillars
The
/.agenticmirror — your runtime surface. For every user-facing route in the app there is a machine-readable mirror at/.agentic/<same-path>. Each returns one JSON envelope describing the current screen state (data), safe navigation to sibling screens (_links), and the unsafe mutations available from that screen (actions). This is a live, hypermedia API: you start atGET /.agenticand walk the graph. The mirror is served by the API at/api/agentic/*; nginx rewrites/.agentic/…→/api/agentic/…, so both URLs work and the canonical form is/.agentic/…. Media type:application/vnd.sojourncase.agentic+json. See navigation.md./openapi.json+ the API reference — the contract. The full REST contract lives in/openapi.jsonwith stableoperationIds (e.g.login,createCase,addRecord,autofillRecord,planStrategy,draftBrief). The human-readable companion is api-reference.md, whose anchors match those operationIds (e.g.…/api-reference.md#addrecord). Every envelope deep-links into both viameta.docs, so you can resolve the exact spec for any link or action you are about to use.This doc set — the prose. Conceptual guides under
/agents.dir/: the envelope format, the navigation loop, the domain model, and the per-endpoint reference. The index for crawlers is/.well-known/llms.txt(also at/llms.txt).
The division of labor is strict and worth memorizing:
| Surface | Purpose | Safe? | Targets |
|---|---|---|---|
_links in an envelope |
GET navigation to sibling /.agentic screens |
safe | /.agentic/* mirrors only |
actions in an envelope |
the mutations (create/update/delete, AI runs) | unsafe | the real /api/* endpoints |
meta.docs in an envelope |
deep links to spec for each link + action | — | /openapi.json, api-reference.md |
You read from /.agentic, you write to /api. The mirror never mutates;
it only ever tells you which real /api endpoint to call.
Authenticate
You need a Bearer token. There are two ways to get one — as an agent, use the first.
Agent login (device-grant) — get your OWN token, no password
You should never handle the user's password. Instead, run the device-grant
flow: you start a request, the user approves it in their browser, and you poll
for a scoped token of your own (sjc_agent_…). Full walkthrough — including
read vs write scope and revocation — is in
login.md. In short:
POST /.agentic/login/transient { "name": "Kant", "entityId": "kant-prod-1", "scope": "write", "tokenTtlSeconds": 1800 }
→ { transientToken, claimSecret, loginUrl, claimUrl, ackUrl, tokenTtlSeconds, expiresAt }
# send the user to loginUrl; they log in + approve; then poll:
POST /.agentic/login/transient/<id>/claim { "claimSecret": "…" }
→ { "status": "approved", "token": "sjc_agent_…", "scope": "write", "tokenTtlSeconds": 1800, "ackUrl": "…" }
# ACK to confirm receipt (mandatory) — signed WITH the token:
POST /.agentic/login/transient/<id>/ack (Authorization: Bearer sjc_agent_…) → 200 { expiresAt, persist }
The grant lasts 5 minutes; you must claim and ack within it (an un-acked token
is provisional and lapses). entityId is your stable identity — re-authenticating
with it voids your prior token for this user. The token is revocable/renamable by
the user under Account → Connected agents. A read token may call only safe
(GET) methods. Then send it like any Bearer token (step 2 below). Full flow:
login.md.
Pick the shortest useful TTL, then persist it.
tokenTtlSecondsdefaults to 1800 (30 minutes) and may be at most 2592000 (30 days). If the user asked for a one-off task, prefer a shorter TTL. If they asked for persistent, recurring, or multi-session work, it is fine to request a longer TTL up to the max. Once acked, keep the token in working context or durable storage and reuse it untilexpiresAtor revocation; re-running this grant flow per action is a bug.
User JWT — only if you legitimately hold credentials
The API also issues Bearer JWTs to email/password logins (this is what the human SPA uses). Use this only if you are acting with the user's own credentials.
Get a token by posting credentials to the login endpoint:
POST /api/auth/login Content-Type: application/json { "email": "you@example.com", "password": "…" }On success (
200) the body is:{ "token": "<JWT>", "user": { "id": "…", "email": "…", "name": "…" } }Unknown email and bad password return the same
401(no account enumeration). A created-but-unapproved account returns403.New accounts register at
POST /api/auth/register({ name, email, password }). Depending on the operator's policy this returns either201with a token (open signup) or202 { "pending": true, "message": … }(approval required — no token until an operator approves).Send the token on every other request as an HTTP header:
Authorization: Bearer <JWT>This applies to both the
/api/*endpoints and the/.agentic/*mirror — the mirror is authenticated and scoped to the calling user.Confirm the token any time with
GET /api/auth/me→ the currentUser.POST /api/auth/logoutis a stateless204no-op (just drop the token client-side). Tokens are signed HS256 and carry the user id only; identity is always re-read from the database server-side.
402 means out of credits. AI actions debit a credit balance —
planStrategy(220/340/800 by tier) anddraftBrief(260/1000/2400 by tier) are the two paid steps of the v3 draft flow (seeGET /api/tiers), plusassessEvidence= 5cr,autofillRecord= 5cr. (The old flat 120-creditgenerateBriefis retired —POST /draft/generatereturns410 Gone; the paid Review product was removed — the draft ships its findings for free.) A402response means you ran out — checkGET /api/credits/balanceor the credits screen. The body carriesbalance,required, and — when purchasing is enabled — atopupURL (…/?buy=credits). Buying credits is a human act. Give your human thetopuplink and stop; never try to pay.POST /api/billing/checkoutis human by default (and rejects agent tokens with403when the human-only setting is enforced) — either way Stripe Checkout needs a human at the card entry. The/.agentic/creditsmirror exposes the sametopuppointer (data.topup) when billing is on. (Anthropic's own API returns400here; Casework uses402deliberately.)
Start here
The loop is always the same — read → link → act → resolve docs:
GET /.agenticwith your Bearer token. This entrypoint returns your caller identity, your credit balance, the cases you can access, and a link to the visa catalog. It is the root of the link graph.Read
data. Flat, camelCase, JSON-LD–sprinkled state for the screen. Enums appear as{ code, label }; dates are ISO-8601.Follow
_links(safe GET) to reach the screen you need — e.g. a specific case'stoday,build,evidence,draft, orcasemirror. Links to case-scoped screens carry thecaseId. RFC 6570 templated links (templated: true) require you to expand a variable (e.g.{caseId}) before fetching.Perform
actions(unsafe) by calling the real/apiendpoint named in the action'shrefwith itsmethod,contentType, andfields. Actions that cost credits carrycreditsCost; AI/server-authoritative actions are markedserverAuthoritative: true— trust the server's returned result, do not predict it.Resolve
meta.docswhen you need the precise contract for any link or action.meta.docs.endpointsmaps every_linksrel and every actionnameto a deep doc anchor inapi-reference.md/openapi.json.
A minimal first session:
POST /api/auth/login → get { token }
GET /.agentic → identity, credits, cases, catalog link
follow _links.cases[i] or
GET /.agentic/today/{caseId} → readiness, status, next deadline, agenda
follow _links to build/evidence/draft, or fire an action against /api/...
Recipe: set up a case
A fresh case ships with a guided-setup checklist. GET /.agentic/today/{caseId}
carries it as a read-only data.setup object — a dismissed flag plus five
items[], each with done/have/need, a read mirror href, and a via array
of the action operationIds (in call order) that complete it. Drive it like this:
- Read
GET /.agentic/today/{caseId}and takedata.setup. Ifsetup.dismissedistrue, the human hid the checklist — stop, nothing to do. - For each item where
done == false,GETitsitem.readmirror to see the current state, then perform itsitem.viaactions in the listed order:field→patchCase(PATCH /api/cases/{caseId}{ field }).criteria→addDocthensetDocStatus— anaddDocalone creates a"need"doc that readiness never counts;setDocStatus(draft/final) completes the pair. (OraddRecordinto a success-section tracker on/.agentic/build/{caseId}— a landed record claims a criterion too.)target→patchCase({ target },YYYY-MM).letters→addRecordon the letters-preset tracker (its id is discoverable via/.agentic/build/{caseId}); needsneedrows total.prior→setPriorAttempts(refiler history; never blocks completion).
- Re-read
data.setupand stop when the core items —field,criteria,target,letters— all reportdone(prioris optional and never gates completion). - Never write
setupDismissed. Hiding the checklist is the human's call: the server403s that field from agent bearers (sjc_agent_*). Finish the work and leave the latch to the user.
Roles
Every case has members with one of four roles. Your role on a case is in that
case's envelope at data.yourRole as a { code, label } pair:
| code | label | Can write | Can sign forms | Notes |
|---|---|---|---|---|
owner |
Owner | yes | yes | Full control; only the owner may deleteCase. |
editor |
Editor | yes | yes | Full read/write on the case. |
reviewer |
Reviewer | no | yes | Reads everything; may comment/watch and, in the human UI, attest a completed USCIS form. Draft sign-off does not exist. |
viewer |
Viewer | no | no | Reads everything; may comment and watch. No edits. |
An action's ABSENCE means you are not permitted to call it. Every case-scoped
envelope advertises only the actions your role would not be 403'd on:
- Viewers and reviewers see the member-gated actions —
addComment,watch,unwatch— on the record / task / doc envelopes that carry them, but none of the writer-gated ones (create/update/delete, AI runs). The top-levelcaseanddraftenvelopes carry no comment/watch actions, so a viewer sees noactionson those at all. Do not synthesize a writer endpoint that isn't inactions; it will403. (Draft sign-off was removed — audit-endgame Law 3 — so a reviewer no longer has an attest action.) - Editors and owners see the full action set.
This is distinct from token scope (see login.md):
a read-scope token 403s every mutating verb regardless of role, whereas
role omission tailors which mutations a write-scope token is offered on a
given case. Both apply — a read token on a case where you are an editor still
cannot mutate.
The doc set (/agents.dir/)
- navigation.md — the read → link → act → resolve loop in full: the envelope keys, the rel vocabulary, RFC 6570 templated links, case-scoping, and cursor paging.
- envelope.md — the
exact shape of the agentic envelope (
@context/@type/@id,data,_links,actions,meta) and its invariants. - routes.md — the full
list of
/.agenticroutes and which UI screen each one mirrors. - domain-model.md — the entities: cases, trackers, records, fields, tasks, columns, evidence groups, docs, drafts, credits, and how they relate.
- api-reference.md —
every
/apiendpoint byoperationId, with request/response shapes; anchors matchmeta.docs.endpoints. - /openapi.json — the machine
contract;
operationIds are stable and identical to the action names. - /.well-known/llms.txt — the crawler index of this whole set.
House rules
- Never mutate via
/.agentic. The mirror is read-only. Mutations always go to the real/api/*endpoint that an action'shrefpoints at. - Only follow
_linksto other/.agenticroutes. They are guaranteed safe GETs into the same mirror — never cross-origin, never an unsafe verb. - Respect case scope. Most data lives under
/api/cases/{caseId}/…; the matching mirror is/.agentic/<screen>/{caseId}. Always carry thecaseIdyou were given; never fabricate one. - Trust server-authoritative results. For AI actions and the credit balance, the server's returned value is canonical — merge it, don't guess it.
- Draft in two paid steps, not one. Drafting is the v3 flow:
planReadiness(free — clear every open blocking ask viaanswerAsk/waiveAskorpasteDocExtract) →planStrategy(paid, tier-priced) → the checkpoint (patchPlanthenapproveStrategy) →draftBrief(paid). Both paid steps return 202 + aDraftJob— pollGET …/draft/jobs/{jobId}.approveStrategyanddraftBrief409 while a blocking ask is open. The old flatgenerateBriefis gone (POST /draft/generate→ 410 Gone); the paid Review product and draft sign-off were removed (audit-endgame Law 3) — the draft shipsjudgeFindings[]as the ONE triageable list (triage withsetFindingDisposition; FIX a finding viapromptDraftSectionon itssectionId). A blocked review permits only a marked Working DOCX; filing-ready PDF/forms/packet exports require the clean gate. - Never infer filing identity or waive a legal predicate. Every paid run needs the explicit beneficiary. O-1A/EB-1B also need a separate petitioner and exact visa-scoped kind. O-1A/EB-1B/NIW mandatory threshold homes require packet- included files with readable text; NIW must establish underlying EB-2 advanced-degree or exceptional-ability eligibility before Dhanasar. These direct readiness rows have no ask and cannot be waived.
- Comparable evidence is gated, not a free-form extra criterion. Saving a proposal never changes live counts. The server must accept it deterministically before provider work; rejection blocks before spend, and draft preflight revalidates it. NIW comparable evidence is exceptional-ability-only, never an advanced-degree substitute or a Dhanasar prong.
- Treat filing-review state as fail-closed. Source/case/prior-filing/task/
record/tracker/prose changes and consumed-decision drift can make the active
draft and versions stale. Only a fresh paid draft restores filing-ready export;
Working DOCX is the only output while blocked.
acceptedmerely acknowledges a finding. Ordinary findings may be waived, but source/attachment/quote/ authority/evidence/party/unclaimed-criterion and other app-owned generation, memo, export, or review-state defects cannot be waived or manually resolved; only a fresh reviewed petition without the defect clears them. - Editing the draft? Send its
rev. The/draft/{caseId}envelope'sdata.revis an optimistic-lock counter. On the draft-writing actions —draftBrief,promptDraftSection(and a direct section editPATCH …/draft/sections/{id}) — you MUST send the currentrevin the request body; a rev-less call is refused 428 Precondition Required (before any credits are charged). If a peer advanced the draft first, the write returns 412 with{ error, rev, draft }(the current rev + full document) — rebase onto thatdraft, reuse itsrev, and retry in one round trip (never a bare re-GET). Always re-readrevfrom the response after a successful draft write. - Evidence text stays local. You may read a case's evidence and its extracted
text (
getDocExtract) under the user's token. But you MUST NOT forward evidence file bytes or extracted text to any third-party AI or external service. Casework's own strategy/drafting pipeline reads extracted text only through the caseaiExtractsgate (setAiConsent: on / prompt / off — file bytes never reach an AI regardless), and inpromptmode an agent-driven generation is fail-closed to metadata-only (no human is present to grant the per-run "Allow once"). SettingaiExtractsis a consent gesture — an operator setting may 403 agent bearers on it (default off). - Petitioner profile: read to help, never forward, never recite PII. The USCIS-forms
mirror (
/.agentic/forms/{caseId}, relforms) and the voicecase_snapshotare value-free by construction — they carry field NAMES + COUNTS + status only, never a field VALUE. You can see which profile fields are missing (profile.missing) and set them under the user's token with the one actionsetProfileFields(or the voiceset_profile_fieldtool), but you MUST NOT forward profile data to any third-party service, and the stored VALUES are not exposed to you to read back. SSN is client-only — it is not aProfilefield, appears in neither the mirror nor the snapshot, and any request body carryingssnis a 400 (the profile PATCH denies unknown keys). No agent action generates, flattens, signs off, or files a form — those are irreversible human acts (the sign-off route additionally takes the conditionalHumanUsergate). Read the missing-field pointers and stop there.