/api/v1 is in developer preview, so breaking revisions still land on the current version rather than opening a new one. See versioning for the rules that apply, and read this page before each upgrade.
2026-08-02 (spec 1.5.0)
Additive. No existing operation changed.- Usage is readable from a key, under a new
usage:readscope that nothing implies and the mint default does not include. Three operations: the session usage series over time, the spend cap with credits drawn against it, and the rate-limit budgets as an on-demand read (answered from the counter without charging it). - The series can be split per environment or per session status, and deliberately not per API key. A key cannot enumerate its siblings; that split stays with a signed-in member in the app.
- Window percentiles are computed over the window, not averaged from the buckets, because percentiles do not compose. A per-bucket
latency_p95_msisnullbelow 20 turns rather than reported, since under that it sits within a hair of the slowest single turn.turnssits beside it so “not enough data” and “no data” stay distinguishable. - A window too wide to represent is refused, not silently shortened. Previously a long enough daily range returned a truncated series while still echoing the full date range back, so the totals beside it belonged to a shorter window. It now returns
422naming the limit. - Filler buckets carry zeros and
nulllatency, never0ms. The last bucket in a series is usually partial, so divide by elapsed time rather than by a whole day.
2026-08-02 (spec 1.4.0)
Additive. No existing operation, status code or body changed.- Every response now tells you what budget it just spent. Requests made with an API key carry
x-ratelimit-limit,x-ratelimit-remainingandx-ratelimit-reset, so you can pace offremaininginstead of discovering the ceiling by tripping it. The429carries them too, besideRetry-After. x-ratelimit-resetis seconds from now, not a Unix timestamp. It is measured when the response is written, so it decays across successive calls. Read it as a delay, never as a clock reading.- The headers are optional and their absence is meaningful. A request authenticated with a user access token carries none, because those are deliberately unlimited. A response refused before the limiter runs, such as a
401, carries none either. Treat a missing header as “no budget was charged”, not as zero remaining. - One request can spend two budgets (a session create spends both the per-request limit and the daily session quota). There is one set of header names, so the triple always describes whichever budget is closest to refusing you.
- A deployment that cannot count honestly says nothing. If the rate-limit counters are not shared across servers, the headers are omitted rather than published with a number that describes one server instead of your actual budget. Enforcement is unaffected either way.
2026-08-01 (spec 1.3.0)
Additive. Nothing existing changes shape, and no operation refuses anything it previously accepted.- You can now manage your own people, not just read them. Create, update, archive and restore, under a new
people:writescope:POST,PATCHandDELETEon/v1/workspaces/{workspace_id}/people, plusPOST .../people/{person_id}/restore.people:writedoes not implypeople:read: the two are granted independently, so a key that only feeds people in never gains the ability to read the pool back out. - Delete archives; it does not erase.
DELETEsetsarchived_atand the row stops appearing in listings unless you pass?archived=true. Restore brings it back. Sessions that already ran keep the person snapshot they froze at open, byte for byte, across both. Permanent erasure stays inside the ish app. - The write contract is narrower than the read shape.
product_id,type,visibility,custom_fieldsand the simulation config are not writable through this API: a key cannot move a person between workspaces or change what kind of thing it is. Send a field explicitly asnullto clear it; omit it to leave it alone. external_idfor your own identifiers. Workspace-unique, and looked up with?external_id=rather than in the path, so your id space never has to collide with ours. Create acceptsIdempotency-Key.- Generation is an API operation, and it is metered.
POST .../people/generation-jobswithGETpolling andPOST .../cancel. The job reports a livecountsobject as people land. Billing draws 2 credits per person that actually succeeds, plus 1 more when avatars are generated. The number you watched climb is the number you are charged for. Cancelling keeps whatever was already produced and you pay only for that.Prefer: wait=n(1-60s) holds the create open for a short job rather than making you poll. - An unknown interest id is now a
422instead of an empty page. Aqualification/tagfilter naming an id outside the taxonomy used to expand to itself and match nothing, so a typo returned a clean, well-formed, empty result indistinguishable from a real audience of zero. It now names the offending id.
2026-07-28 (spec 1.2.0)
Mostly additive. One existing operation changes what it refuses, and only for aperson_id no integration should have been sending.
Fixed
-
A session can no longer be opened for a real person.
POST /v1/workspaces/{workspace_id}/sessionsnow answers404person_not_foundwhenperson_idnames a real person who participates in ish under consent, rather than opening the session. This lane has always been documented as simulated people only, but the check tested ownership and library membership without testing which kind of person the record was, so some real-person records satisfied it. The answer is404, identical to an id that does not exist, rather than the403person_out_of_scopeused for another workspace’s persona: that403confirms a person exists and says they are not yours, which is the wrong thing to say about a human being. If this changes an answer you were getting, theperson_idwas reaching a record it should never have reached. Every simulated person is unaffected, andGET .../peoplelists exactly what a session create accepts.
- You can read the people you run sessions for. Five new operations under a new
people:readscope:listPeople,searchPeople,getPerson,getPeopleCounts, andgetPeopleSchema, all under/v1/workspaces/{workspace_id}/people. Until now aperson_idhad to come out of the ish app and be pasted into your code; an integration can now find its own participants. See people. people:readis not in the mint default. A person record carries demographics and a written background, so the scope is granted deliberately rather than by default. Existing keys gain nothing: scopes are fixed at mint, so mint a new key, or rotate, to pick it up.- Ownership is an
owneralias, not a visibility enum. Every person carriesowner:selffor one your workspace owns,platformfor one from the shared ish library. Both are runnable, and the two together are exactly what a session create accepts, which is why the unfiltered list returns the union. That equality holds in both directions as of the fix above: the two surfaces now apply the same rules, so a person the list omits is a person a session create refuses.workspace_idis present only onselfrows. People that other workspaces have published are deliberately absent: they are discoverable in the app but not runnable through the API, and listing ids you cannot use would be worse than omitting them. - Search takes the same rule tree the ish app builds.
POST .../people/searchaccepts a boolean tree of attribute filters, capped at 2 levels of nesting, 15 conditions per group, and 25 in total. Saved segments are not exposed as objects, because a segment is a rule: send its rule. See search grammar for the semantics and a worked example. GET .../people/schemapublishes the attribute vocabulary. Every attribute with its type, its allowed values, whether it is filterable and under which key, and awritabilityblock. It is authoritative: a rule naming an attribute the schema does not publish is refused rather than quietly matching nothing. Cache it, since it is the same for every workspace.- Two new error kinds, both
422:search_rule_invalidandsearch_rule_too_complex. They are the first handler-raised422s to carry anerror_kind, so a422is no longer safe to treat as “malformed body” without reading the envelope.
type is always ai: real people who participate in ish under consent are not reachable with any key you can mint.
2026-07-28 (spec 1.1.0)
A breaking pre-release revision that renames the intent noun. Everything below this entry shipped under spec1.0.0; from here on each entry names the info.version of the OpenAPI spec it shipped in, so you can correlate a spec diff with the entry that explains it.
Breaking
-
The intent primitive is a
task, not anassignment.assignmentnow names only the act of assigning a task to someone, which is not something the API exposes, so the word has left the public surface entirely. -
source_assignment_idis gone from the task response. It was a lineage pointer nothing ever wrote, so it servednullon every row for every caller. Removed rather than kept as a permanent null, because a field you can never receive a value in is one you should not write a branch for.
- API keys now reach the task registry. All five task operations previously refused keys outright with a
403api_key_not_permittedand could only be called with a user access token. They now take the same scope pair every other registry uses:tasks:readfor list and read,tasks:writefor register, update, and delete. An under-scoped key gets403insufficient_scopewithdetail.required_scope. tasks:readjoins the mint default. A newly minted key carriessessions:run,sessions:read, andtasks:read, so a key that runs sessions can also resolve a task it binds by id. Keys minted before this date are not changed. Scopes are fixed at mint, so an existing key gains nothing: mint a new key, or rotate, to pick up the default. No:writescope is granted by default.- Session list rows carry
participant.type. It distinguishes a simulated participant from a real one. Present onlistSessionsrows; absent on older rows, which carry no type.
- The
409refused when you retire a task an active recruitment listing still references now says so in those terms.detail.error_kindstaystask_in_published_listinganddetail.listing_idstill names the listing. - Task name uniqueness is described as per workspace, which is what it means for an API caller. The rule itself did not change.
backgroundandstepson a task are documented as accepted and stored but not yet consumed by API sessions. They always behaved this way; only the description was unclear.- No documented operation returns
api_key_not_permittedany more. The kind stays in the catalog because it still applies elsewhere in the product.
- Repoint the four registry paths from
/assignmentsto/tasks. - Rename the session-create
assignmentblock totask, andgoaltoinstructionsinside it. A leftoverassignmentor a top-levelgoalis a422naming the stray key, never a silent partial accept. - Read
task(withtask.instructions) where you readassignment(withassignment.goal) on session responses. - Switch any
assignment_idfilter onlistSessionstotask_id, and branch on the four renamedtask_*error kinds. - Drop
source_assignment_idfrom your task model.
2026-07-28
A breaking pre-release revision. It renames the public actor noun, renames one environment field, moves the close endpoint, and widens what create and close return. If you integrated before this date, three edits are required. Breaking
The old
DELETE /v1/sessions/{session_id} is gone rather than deprecated, and a request to it is a 405. The new close takes no request body and requires the same sessions:run scope.
Additive
- Create and close return the full session. Both used to return a five-field stub (
id,status,decision_mode,turn_count,max_turns). They now return the same 23-field object a read returns, including the frozenassignment, the frozenenvironmentand its fourenvironment_*pointers,person_snapshot,accrued_credits,ended_reason,ended_at, and the timestamps. Nothing was removed, so a client reading only the original five fields keeps working. listSessionsgained two filters:environment_version_idpins the list to sessions run against one declared environment version, andsearchmatches a session id by prefix or the frozen goal case-insensitively. The existingstatus,api_key_id,environment_id,assignment_id,created_from, andcreated_tofilters are unchanged.Idempotency-Keynow covers every create. It was accepted on create-session only; it is now accepted on create-environment, create-environment-version, and create-assignment on the same terms. A retry with the same key and body replays the existing row; the same key with a different body is a409idempotency_conflict.- Observation retention is now a session and environment setting.
observation_retention(none,30d,90d, orindefinite) can be set on an environment as its default and overridden per session on create. It is served back on the session. An environment described inline with no override resolves tonone. - Turns can carry the frame back. A turn in the trace now carries
observation_url, a short-lived signed URL for the retained frame, plusobservation_widthandobservation_height.observation_urlis null when nothing was retained, which is what anonesession always reports. Read the URL, do not store it. - List rows carry
ended_at.ended_atmoved onto the shared session base, so alistSessionsrow now serves it alongsidesettled_atwithout a second read. - Every error kind is now documented and stable. Responses across all four surfaces carry a machine-readable
detail.error_kind, and the full catalog of 33 public kinds is published at errors. Branch on the kind rather than the status: several kinds share a status and want different remedies. - The registry list envelopes reserve pagination. The environment, environment-version, and assignment lists now carry
has_moreandnext_cursoralongsideitems. Both are inert today (falseandnull) because those lists return the whole collection, and they are present so pagination can land later without a wire break. Read them rather than assuming a single page.
- Rename
persona_idtoperson_idin your create body, and readperson_id(orparticipant.person_idon list rows) where you readpersona_id. - Rename
platformtokindin any inlineenvironmentdescriptor. - Change your close call from
DELETE /v1/sessions/{id}toPOST /v1/sessions/{id}/close.
2026-07-27
A breaking pre-release revision to how a session carries its intent. Breaking- Session create bodies carry the intent as a required
assignmentobject with exactly one shape:{"goal": ...}describes it inline, or{"id": ...}binds a registered assignment and freezes its goal and revision onto the session. - A top-level
goalfield is rejected with a422. - Session reads return an
assignmentobject in place of the flatgoal. Idempotency-Keyvalues stored before this revision no longer replay. A retried old-shape body is a422, and reusing an old key with a new-shape body is a409idempotency_conflict. Mint fresh keys after migrating.