Changelog
How v1 changes
Additive by default. New endpoints, new optional parameters and new response fields can appear at any time. Your client must ignore fields it does not recognise.
Error codes are stable strings. A code that appears in errors.md keeps its meaning for the life of v1. New codes may be added; existing ones are not renamed or repurposed.
Fields are never renamed within v1. If a field's name is wrong, a new one appears alongside it and the old one stays.
Removals are announced. Anything withdrawn gets an entry here first, with a notice period before it stops working. Nothing disappears from this pack without an entry below.
Breaking changes mean v2. A new major version would be served alongside v1, not in place of it.
Regenerate resync-api-v1.pdf whenever an entry is added here.
v1 · 2026-10-20 — the base URL and the docs site
Base URL. The API is served at https://api.limnal.dev. Every example in this pack now uses it; paths are
unchanged (they already carry /v1). HTTPS only.
Docs site. The current version of this pack is published at https://docs.limnal.dev.
v1 · 2026-10-20 — a limit per key, births recorded under your key, a whole-bid quickstart
Rate limit per key. A key may now carry its own limit, set by ReSync (an administrator act — a key cannot change its
own). Without one, the default stays 300 requests per minute. x-ratelimit-limit on every response, and the 429
message, say your key's own limit. Unknown and revoked keys always get the default.
Births carry your key's name. producer_id on resource.created and on a new resource's first status.changed is
now your key's name when your request created the resource — by find-or-create, or a person first named through
actorAlias. It used to read api. A resource ReSync creates in response to your write (the order a converted bid
produces) carries consequence.worker. Rows written before today keep the value they were written with.
Quickstart rewritten as one whole bid, from an empty start to a converted order, with a real response for every
door. examples.http follows the same walk. New captures in the pack: the type list and one type in
full, a member enrolment (201), a requirements read, a status change (200, 422 with the requirements, 409 for a
move the lifecycle does not declare), an attestation (201, 409 attestation_not_repeatable,
422 attestation_status_gate_unmet, 403 actor_not_readable), a converted bid's derivatives, 400 ports_required,
409 status_read_only, 409 unique_key_taken, a 429, and an event history with resource.captured.
Corrections — the pack described these wrongly; the API did not change:
- An unknown query parameter is ignored, not refused (a known one a door cannot apply to a key, such as
sort, is still refused). conventions.md, endpoints.md. - The rate-limit window is fixed (it starts with your first request), not rolling;
retry-afterandx-ratelimit-resetare the seconds left in it. GET /v1/resources/searchreturns at most 20 rows in all, not per requested type.GET /v1/aliases/resolve'sresourceTypeis the type's display name, not its code.GET /v1/resources/:id/eventsalso acceptsfilter=quiet.- Per-door error lists now include
409 status_read_only(PATCH, members, exit, move, connections),409 unique_key_taken(PATCH,find-or-create) and403 actor_not_readable(every door that takes an actor). PATCH /v1/resources/:id/statusdocuments its200body and its422body, and that its operate scope is the attest act.- Containers, cross-type search and move are open to keys (they have been since 2026-10-19); conventions.md still listed them as signed-in only.
external_systemnames a system ReSync already knows; it is not registered per organization, and identifiers are unique per system across every organization.- events.md lists
lifecycle.config_changedandconsequence_rule.changed(quietfilter), and says who may be named as an actor.
v1 · 2026-10-19 — key-lane completeness
New doors for keys:
POST /v1/resources/:id/move— move a resource from one container to another in one step. Needs operate scope on the resource and both containers; both halves are recorded together or not at all.GET /v1/resources/:id/containers— the containers a resource is inside (read scope; unreadable containers left out).GET /v1/resources/search— name search across types.enrollInto,moveOfandmoveFromlist exactly what the act would accept and need operate scope on the resource they name. A key never searches people (personis dropped) andrestrictToOrgIddoes not apply to it.POST /v1/identity-claims/redeem— documented for the first time. It now needsbind_systemsto include the account's system:403 alias_bind_refused, checked before the code.
POST /v1/resources/find-or-create: optional name and the actor fields. Creation is one step: the resource, your
identifier and resource.created are written together, and resource.created now carries the properties it was
created with and alias: { system, code }. A new resource is placed by its type's enrolment rules. Newly refused:
the resync system and built-in system types (400), an identifier already bound to a resource of another type
(409, no resource named), a key with no owning organization (403 owner_grant_required), a blank name
(400 invalid_resource_update).
GET /v1/resources/:id/ports/:portId/legal-targets: q narrows the candidates; optional limit (1–100) returns a
page with truncated and total. Without limit the response is unchanged.
GET /v1/resources: a key list honours tagId, and refuses sort with 400 sort_not_supported — it used to be
ignored without notice.
By-id reads follow the list: GET /v1/resource-types/:id, GET /v1/lifecycles/:id, GET /v1/lifecycles/:id/graph
and GET /v1/state-transitions?lifecycleId= answer 404 for anything outside your key's own list.
POST /v1/resources/:id/attestations: scopeResourceId must be a resource your key can read; otherwise
403 subject_outside_grant and nothing is recorded.
Documented, unchanged: the actor fields on members, exit and move; the codes replace_mismatch,
outside_acts_at_scope and invalid_resource_update; the full list of doors that answer some refusals with a
sentence (errors.md).
New codes: invalid_limit, invalid_tag_id, sort_not_supported.
Correction: the 2026-10-07 entry "attestation rules, ingest refusals, money fields" says freezing a line requires
its bid.line_frozen record. That requirement no longer exists: freezing a line photographs it instead
(resource.captured, see the 2026-10-16 and 2026-10-17 entries).
v1 · 2026-10-17 — bid declarations
Changed declaration (tenant vocabulary): bid.sent declares subtotal_cents, discount_cents, delivery_cents,
tax_cents, install_cents, total_cents and tax_rate_bp — customer_pct_bp is no longer declared and no longer
needed. A bid now carries its own price level and payment terms, and sending it records them (a resource.captured
event). Read the current declaration from GET /v1/attestation-event-types. See events.md.
v1 · 2026-10-16 — declared captures and containment limits
New event type (additive): resource.captured — a transition can declare what to photograph when it fires (the
resource's own properties, its settings, and properties of the resources attached at named ports); every firing records
one resource.captured on the resource, whichever way the status changed. In the all and changes filters; never
unread. See events.md.
New field (additive) on GET /v1/lifecycles/:id/graph: each transition carries capture for a reader who can author
the lifecycle — never for a key.
New error code: capture_shape (lifecycle authoring in the web application; a key never meets it).
Containment limits (additive): a type can declare how many containers of a given type its resources may be inside.
GET /v1/resource-types/:id carries containmentCaps. New error on POST /v1/resources/:id/members:
422 containment_cap — the resource is already inside as many containers of that type as its type allows. See
errors.md.
New filter value (additive): filter=quiet on GET /v1/resources/:id/events. Configuration entries
(lifecycle.config_changed, consequence_rule.changed, canvas.view_changed, status.attention_changed) now sit in it
and are no longer in all or changes. They were never unread.
v1 · 2026-10-16 — type configuration on the record
New event types (additive), recorded on a type's resource only, in the all and changes filters and never unread:
resource_type.profile_changed (the type's icon or key properties changed; typeId, changedKeys) and
resource_type.config_changed (an emission was declared or removed; kind, verb, id, typeId,
attestationEventTypeId, eventTypeKey). Neither carries a value or a name. See events.md.
New fields (additive) on GET /v1/lifecycles/:id/graph: each status carries legalTargets and illegalTargets
(a status id → the sentence saying why a transition to it is not allowed). Both are empty unless the reader can author
the lifecycle — always, for a key.
Changed content: a resource made by a consequence rule now has an interaction.created event for its link back to
the resource it was made from, in the same moment as the consequence itself. Earlier such links have none.
v1 · 2026-10-15 — a lifecycle as one graph
New endpoint (additive): GET /v1/lifecycles/:id/graph — a lifecycle in one read. It carries the statuses in
display order (from the entry status outward; statuses no transition reaches come last, flagged reachable: false),
each transition with its requirements (name, blocking, active flag, and a display label per condition) and gate counts,
the types that run the lifecycle, and the consequence rules on those types. A rule lists linkedTransitionIds — the
transitions an active requirement of which needs the very event that triggers the rule — with a display sentence.
Every human-readable string in the body is ready to show as is. The reference data rules apply (any valid key may read
it); lifecycle.can.author is always false for a key.
v1 · 2026-10-15 — nested containment in the view; containment loops refused
Changed content in GET /v1/api-keys/self/view: nodes now carries containment at every depth, not only the
view subject's direct members. Each node's parentId names the container it is drawn in (the subject, or a member
that itself has members). A resource held by more than one drawn container appears once, under the one nearest the
subject. A member you cannot read is left out together with everything inside it. Every drawn container that holds
at least one drawn member carries a rollup for its own members (total, shown, types, notReadable); the
subject's rollup is unchanged (present only when members were left out). tray.noInteractions now counts members
with no connections across every drawn container.
New error on POST /v1/resources/:id/members: 422 containment_cycle — the resource being added is the
container, or already contains it. See errors.md.
v1 · 2026-10-14 — field attachments drawn as settings, not connections
Changed content in GET /v1/api-keys/self/view: a field attachment (a value set on a field port) is no longer
listed in interactions, and a resource reachable only through one is no longer in nodes. The value is carried by
the port's setting in summary.nodes[id].ports. The attachment itself is unchanged — GET /v1/resources/:id/ports
and the connection doors still see it, and a member holding one still counts as connected.
New field (additive) in the same body's summary.nodes[id]: inherited — the field values a node inherits from an
enclosing item, for kinds it has no field of its own. See endpoints.md.
v1 · 2026-10-14 — field settings: deviation flag, words for what you cannot read
New response field (additive) in GET /v1/resources/:id/ports → setting: deviates — true when a field's
own value differs from the inherited value it replaces, false otherwise (a difference from a lock above is a
conflict, never a deviation). See endpoints.md.
Changed content in the same setting: a value or source your key cannot read no longer comes back with
name: null. Its name is the word a setting (a value) or an enclosing item (a source) and the object carries
readable: false; resourceId stays null. Show name as-is.
v1 · 2026-10-14 — compatibility candidates and labels, boundary-port doors removed
New field label on each type of GET /v1/compat/interaction-types: the type's display label for a chooser
(its forward label, first letter capitalized, else its name in plain words). Additive; session only.
New door (session only) GET /v1/resources/:id/compat-candidates: not available to keys — a key gets
403 compat_authoring_session_only, the same refusal a key gets when it tries to author a compatibility marking.
Removed (session only, 0 callers): the seven canvas boundary-port doors under /v1/canvases/:id/boundary-ports
(list, create, save positions, rename, delete, add a type, remove a type). None was ever available to keys.
v1 · 2026-10-14 — port type choices, view overlay, list order
New field typeChoices on each port of GET /v1/resources/:id/ports: the types that may still be added to the
port's accepted types (readable, accepted by the port's definition, not already on it), each { resourceTypeId, name, icon }. New optional parameter ?workspace= on the same door narrows those choices to global types and one
owner's.
Changed content: overlay.nodes on GET /v1/api-keys/self/view now holds position rows only for drawn nodes; a
row for a node the view hides (or a type filter leaves out) is no longer sent.
Changed order (no shape change): the nodes of GET /v1/api-keys/self/view are now in list order — the resource
itself, then nodes whose status attention is alert, then watch, then the rest, each group in the graph's own order.
v1 · 2026-10-14 — type icons
New field icon on each row of GET /v1/resources, on GET /v1/resources/:id, and on each readable node of
GET /v1/api-keys/self/view (also on the rollup's per-type rows there): the resource type's icon, always one of the
fixed icon names listed under GET /v1/resources. It is the nearest icon the type or an ancestor type declares,
counting only types your key can read, and box when there is none — so there is always something to draw.
Changed content: a type's presentation profile in the view's summary now reports icon: "box" instead of null
when no readable type in its chain declares one.
v1 · 2026-10-13 — hidden nodes and the tray in the view
Changed content in GET /v1/api-keys/self/view: nodes the default view hides are left out of nodes, together with
their connections. They still count in alerts, in memberCount and in the rollup.
New fields: a top-level tray ({ noInteractions, hidden }), and connected (true · false) on each drawn
member and outside node.
v1 · 2026-10-13 — the view's logic layer setting
New field in a view's settings (GET /v1/api-keys/self/view → view.settings): showLogic (true · false;
false = the view draws structure only). It is a setting for your renderer; this endpoint's body is the same either
way.
v1 · 2026-10-13 — attention, the alert list, type profiles in the view
New fields in GET /v1/api-keys/self/view: each readable node's statusAttention (none · watch · alert) and
statusShape (circle · square · triangle · diamond, or null = the shape for its attention); a top-level
alerts array (the watch / alert statuses among the resource's readable members, with counts and up to five sample
resource ids); in summary, a type's profile ({ icon, keyProperties }) and a node's keys.
v1 · 2026-10-12 — the resource a key acts at, through its default view
New endpoint: GET /v1/api-keys/self/view returns the resource your key acts at as its default view shows it — the
resource's graph (readable members, connections, per-node summary, stored layout) with the view's settings applied, plus
view ({ name, settings }, or null when the resource has no default view). Takes no parameters.
New code: no_acting_resource (404) — the key acts at no resource.
New field in a view's settings: density (comfortable · compact · large; null = comfortable).
v1 · 2026-10-12 — a resource's canvasId prefers its default canvas
Changed content: in GET /v1/resources/:id, canvasId now resolves deterministically when a resource has more
than one stored canvas: the resource's own forked canvas first, then the canvas marked as its default, then the
oldest. Before, the default was not considered. A resource with no default canvas returns the same value as before. No
field changes type, and canvasId stays null for a resource with no canvas.
v1 · 2026-10-12 — legal-targets predicts read-only refusals
Changed content: GET /v1/resources/:id/ports/:portId/legal-targets no longer lists a connection whose source sits
in a read-only status. Such a candidate is illegal with the reason status_read_only — the code a create already
returns (409) — and an empty viableTargetPorts. When the anchor is the read-only source, every candidate carries
it. No field changes type. See errors.
v1 · 2026-10-12 — connections must face the right way
New error code: POST /v1/resource-interactions refuses a connection between two ports that face the wrong way
with 422 port_direction. The source port must send (out or bidirectional) and the target port must receive
(in or bidirectional); out→out, in→in and in→out are refused. A connection with only one port, or
none, is not affected. See errors.
No change to legal-targets: it already listed only ports that face the right way. A create now enforces the same
rule.
v1 · 2026-10-10 — names you cannot read
Behaviour change, no field changes type: a response no longer names a resource, person, organization or type
your key cannot read. Where it would, the field keeps its type and reads a neutral word — someone, a resource,
a type, or the positional words an enclosing item, an item inside, a setting — and the id beside it is kept.
See conventions.
New response fields (additive):
readable: falseon a row that names something you cannot read:GET /v1/resources/:idedges[],derivedFrom[]andderivedKeys[]; an event'sactorand the resources in itsdisplay;sourceReadable/targetReadableon a connection event'sdisplay.
Changed content:
GET /v1/resources/:id— aderivativesrow you cannot read is left out; a type you cannot read reportsresourceTypeName: "a type"with an emptypropertySchemaanddeclaredPropertySchema.GET /v1/resources/:id/events—producer_idis another integration's key name only when you can read its service identity; for the event types the platform writes,displaycarries a fixed set of keys (unlisted stored keys are left out), and a connected app's key name and scopes appear only to administrators of its owner.- Requirement labels (
/requirements,/advancement, the422of a status change) name only what you can read. GET /v1/resources/:id/events— a connection made or removed between this resource and one you cannot read now appears on this resource's history (interaction.created/interaction.deleted), the other end asa resourcewithsourceReadable/targetReadable: false. It used to be left out.- New event code:
derivation.refusedwithcode: setting_name_unreadable. See events.md.
Documented: the compat_deny attribution fields on legal-targets reasons (denyEdgeId, layer,
firingNeighborId, firingNeighborName, firingNeighborTypeName). They were already present. See
endpoints.md.
v1 · 2026-10-10 — field flag on ports, inherited property schema
New response fields (additive):
GET /v1/resources/:id/ports— every port carriesisField:trueon a field (a port that points out, takes one connection and accepts exactly one type),falseotherwise. It is the same rule that decides which ports carry asetting. See endpoints.md.GET /v1/resources/:id—declaredPropertySchema: the resource type's ownpropertySchemamerged with every ancestor type's, a key declared nearer the resource's own type winning.propertySchemais unchanged.
v1 · 2026-10-10 — legal targets list only what you can read
Behaviour change: GET /v1/resources/:id/ports/:portId/legal-targets with scope=context (the default) now lists
only the resources in contextId that you can read — the same rule scope=type already applied. A resource you
cannot read was never a target you could connect to: creating the connection already refused it. Nothing else in the
response changes. See endpoints.md.
v1 · 2026-10-09 — conversation events
New event types (see events.md): thread.created, thread.participant_added and
thread.participant_left — a conversation's lifecycle, now recorded. A conversation that belongs to a resource is
scoped to it, so these can appear in that resource's events (all and changes filters). They never count as unread.
v1 · 2026-10-08 — polling for changes, new event types
Poll for changes (additive): GET /v1/resources/:id/events takes a new after parameter — events oldest
first, from a cursor you stored, with nextCursor always present as the start of your next poll (after=start
begins at the first event). A poll never skips an event, even one whose write started earlier and finished
later. Every newest-first read now also returns afterCursor, the cursor to start polling from. A cursor this
API did not issue answers the existing 400 invalid_cursor. See events.md.
New event types (see events.md): resource.tagged / resource.untagged,
resource.owner_changed, port.created / port.updated / port.deleted, and port_definition.changed. The
port types appear in the changes filter only.
New code: 403 live_key_not_supported — the live-update stream (GET /v1/live) serves ReSync's web app and
kiosk stations; an integration key is refused and polls instead. The stream's other codes (live_unavailable,
live_connection_not_found, live_connection_limit, live_view_limit, invalid_views) cannot be reached with
an integration key; they are noted here for completeness.
v1 · 2026-10-07 — attestation rules, ingest refusals, money fields
Repeatable is now enforced (see endpoints.md): an attestation type that is not declared
repeatable is recorded once per resource per status. A second one in the same status answers the new
409 attestation_not_repeatable (see errors.md); it can be recorded again after the resource
changes status. Types whose acts genuinely recur are declared repeatable.
Status rules on the bid attestations: bid.sent can be recorded only on a bid in Draft, bid.accepted
only on a Sent bid, and bid.line_frozen only on a line in Draft; 422 attestation_status_gate_unmet
otherwise. Freezing a line now also requires its bid.line_frozen record (a requirement on the move).
Event ingest: a declared attestation type sent through ingest now obeys the same status and repeat rules.
A refused event is recorded as attestation.refused in its place (see events.md); a resent
duplicate is still just a duplicate. The 202 response is unchanged.
New property attribute (additive): a type's propertySchema field may carry derivable: false — that
property (typically money) is never computed by a formula; you enter it.
Requirement labels: an evidence condition without its own label is now named by its declared action ("Send bid") instead of a reworded event type ("Bid sent").
New response field (additive): GET /v1/resources/:id carries readOnly — null, or { statusName } when the
resource is in a read-only status. Use it to show the resource as read-only instead of letting a write be refused.
v1 · 2026-10-07 — read-only statuses
New error code (see errors.md):
status_read_only(409) — a lifecycle can declare a status read-only: a resource sitting in it keeps its own state. Writes to its properties, name or identifier, enrolling or removing members it contains, creating, retyping or deleting a connection it is the source of, and un-confirming a property are refused. The body carriesresourceIdandstatusId;messageis a sentence with no ids. Advancing its status, attestations, archiving, connections that point at it, and confirming a property are unaffected.
New response fields (additive):
GET /v1/statuses— each status carriessubjectReadOnly.- The available moves (
GET /v1/state-transitions, andtransitions[]on/advancement) carrytoStatusSubjectReadOnly: whether taking that move makes the resource read-only. GET /v1/attestation-event-types— each type carriesrequiredSubjectStatusId: the status a resource must be in for that event to be attested (null= any). Attesting it in another status answers422 attestation_status_gate_unmet, as before; the check now happens atomically with the append.
v1 · 2026-10-07 — readable requirement conditions
New response field (additive; see endpoints.md):
- Every entry in a requirement's
leaves[]now carriescaption— where that condition stands for this resource, in words ("missing"/"present","not recorded yet"/"recorded","2/3 members at required status","everything inside fits"). It appears whereverleaves[]does:/requirements,/advancement, and the422body of a blockedPATCH /v1/resources/:id/status. - A leaf's
label, and the requirement'smissing[], now name things instead of numbering them — for example"Bid for connection to a Customer"rather than an interaction-type number. Show both as given.
Message change: the message of unique_key_taken now reads, for example, "W3012 is already used by another catalog product owned by Popular Design Mill & Cabinet, Inc." and no longer carries a resource number. Use
holderResourceId to reach the holder.
v1 · 2026-10-06 — declared unique keys
New error code (additive; see errors.md):
unique_key_taken(409) — a resource's type, or one of its parent types, can declare a unique key: itsidentifier, or one property. Within one owner, no two live resources of that type family may hold the same value. For example, a product SKU is unique across every product category, and a modification'sskuis unique among modifications. Any write that would create a second holder is refused with this code: a create (POST /v1/resources/find-or-create), a property or identifier change (PATCH /v1/resources/:id), or an unarchive.messagenames the holder.holderResourceId,keyandvaluesay which. An archived resource releases its key. Empty values are never compared.
A find-or-create that is refused this way leaves nothing behind: no resource and no alias.
v1 · 2026-10-05 — settings and allowed interaction types on ports
New response fields (additive; see endpoints.md):
GET /v1/resources/:id/ports— every port gainsallowedInteractionTypes([{ id, name }], the types a connection on that port may use) andsetting(on a field: the value it resolves to, where it comes from, what it overrides or conflicts with, a displaynote, and — on a field holding its own value —clear({ label, edgeId }: delete that connection to fall back to the inherited value);nullelsewhere). Resources your key cannot read are never named in asetting.GET /v1/interaction-types— every type gainsconnectable(falsefor the compatibility types).
New code: compat_type_not_connectable (422) — a compatibility type used for a connection on a port.
Behaviour, no shape change:
PATCH /v1/resources/:id/status: when another request moves the resource to the same status at the same moment, the second returns200and records nothing; the change is recorded once.DELETE /v1/resource-interactions/:id: deleting a connection that is already gone returns the existing404 interaction_not_found; only the deletion that happened is recorded.POST /v1/resource-interactions: of two simultaneous connections to a port that takes one, the second gets the existing422 port_max_connections. AreplaceInteractionIdalready replaced by another request gets the existing400 replace_mismatch.
v1 · 2026-10-04 — ports and attachments
Four new doors (see endpoints.md):
GET /v1/resources/:id/ports— a resource's ports, includingcontentsScope.GET /v1/resources/:id/ports/:portId/legal-targets— what a port could connect to, and why not.POST /v1/resource-interactions— make an attachment. Needs operate scope on the choosing side and read scope on the chosen side.DELETE /v1/resource-interactions/:id— remove one.
New codes, all in errors.md: port_max_connections, port_inactive, accepted_type,
compat_authoring_session_only, port_not_found, port_not_on_resource, ports_required, self_interaction,
source_resource_id_required, target_resource_id_required, interaction_type_id_required,
invalid_interaction_id, interaction_not_found, context_id_required, invalid_context_id,
exclude_mismatch, and the advisory no_viable_target_port. compat_deny and contents_locked now also come
from POST /v1/resource-interactions. Compatibility markings stay with signed-in people.
v1 · 2026-10-03 — a requirement that checks a resource's contents
No existing field changes. Requirements may now contain a condition with leaf: "subtree_compat":
everything inside the resource must fit the settings passed down to it, and nothing may break a locked
setting. Wherever requirement leaves appear — GET /v1/resources/:id/requirements,
GET /v1/resources/:id/advancement, and the 422 body of PATCH /v1/resources/:id/status — such a leaf
carries three new fields: violationCount, violations (up to 10) and subtreeCompatVacuous. See
endpoints.md. Clients that branch on leaf should treat an
unknown value as a non-attestable, server-evaluated condition.
v1 · 2026-10-03 — settings that pass down to contents
No request or response field changes. A container may now pass a setting down to the resources inside
it. POST /v1/resources/:id/members checks the resource you add — and everything inside it — against the
settings it would receive, and refuses a conflict with one of two new codes (see errors.md):
422 compat_deny— the resource, or something inside it, is marked incompatible with a setting it would receive.422 contents_locked— the container locks a setting and the resource, or something inside it, uses a different value.
Both may carry viaAncestorId, offendingResourceId and offenderCount; an id is omitted when your key
cannot read that resource. Nothing is written when either is returned.
v1 · 2026-10-03 — more precise refusals on four key paths
No request or response field changes. Four paths now refuse, or attribute, more precisely:
GET /v1/state-transitionswithresourceIdrequires that your key can read the resource. A resource in another organization returns403 subject_outside_grant, and a missing id returns404 resource_not_found, as forGET /v1/resources/:id. ThelifecycleIdform is unchanged.actorResourceIdonPATCH /v1/resources/:id,POST /v1/resources/:id/attestationsandPATCH /v1/resources/:id/statusmust name a person your key can read. Otherwise the request is refused with the new code403 actor_not_readable(see errors.md).actorAliasand the default (your key's own identity) are unchanged.POST /v1/resources/find-or-create: a resource it creates now records your key's service identity as the actor of its birth event, instead of no actor.- Event ingest: an event whose subject identifier resolves to a resource your key may not attest on is
accepted (
202) but is not attached to that resource. This was already true forresource:{id}subjects and now holds for every identifier. A person first seen through your key is created under your organization.
v1 · 2026-09-29 — authorization by grant
No request or response shape changes. Access is now decided from explicit grants on both sides of the API; for a key, those grants are exactly what its scopes always described. What you can see is what you could see before, except the cases below, which the documentation never promised:
GET /v1/aliases/resolveanswers only for resources your key can read. An identifier bound to a resource in another organization now returns404 alias_not_found.POST /v1/aliases/bindrequires the target to be a resource your key may change; otherwise403 subject_outside_grant. A409no longer names the resource the identifier is already bound to.POST /v1/resources/find-or-createreturns an existing resource only if your key can read it. An identifier already bound to a resource you cannot read returns409 find_or_create_refused, without naming that resource.- A revoked key sent to an endpoint that also accepts a signed-in person now returns
401 invalid_api_keyinstead of the session layer's phrase. POST /v1/resources/:id/attestationsandPATCH /v1/resources/:id/statuscheck your access to the resource before validating the body, so a refused key gets the scope refusal, never a list of the resource's event types.GET /v1/resources/:idand/advancement: a missing id is404 resource_not_foundfor everyone.verb_not_grantedandkey_unboundare now emitted (see errors.md); a key never holds more than the person who provisioned it.- The event-ingest endpoint authenticates with an API key; a producer's identity is its key.
- Members projection: on
GET /v1/orgs/:orgId/members, each row'smembershipRoleis replaced byaccess({ grantSet, administer, verbs }), read from what the member has actually been granted. This endpoint is for signed-in people only and cannot be reached with an API key; it is noted here for completeness.
v1 · 2026-09-29 — no door changes; two refusal codes reserved (not yet emitted)
No endpoint, parameter or response shape changes. Two codes are added to errors.md ahead of use, so clients can classify them before they are ever returned:
verb_not_granted(403) — the resource is within reach, the action is not.key_unbound(403) — the key has not been bound to an acting identity.
Neither is emitted yet. When they start to be, this changelog will say so.
v1 · 2026-09-25 — initial contract
First published contract for external developers.
Discovery
GET /v1/api-keys/self
Resources — reading
GET /v1/resources— list by type, withinclude=properties, keyset pagingGET /v1/resources/:idGET /v1/resources/:id/eventsGET /v1/resources/:id/membersGET /v1/resources/:id/advancementGET /v1/resources/:id/capabilitiesGET /v1/resources/:id/requirements
Resources — writing
PATCH /v1/resources/:idPOST /v1/resources/:id/membersPOST /v1/resources/:id/exitPOST /v1/resources/:id/attestationsPATCH /v1/resources/:id/status
Creating and addressing
POST /v1/resources/find-or-createPOST /v1/aliases/bindGET /v1/aliases/resolve
Reference data
GET /v1/resource-types,GET /v1/resource-types/:idGET /v1/statusesGET /v1/lifecycles,GET /v1/lifecycles/:idGET /v1/state-transitionsGET /v1/attestation-event-typesGET /v1/property-keysGET /v1/unitsGET /v1/interaction-types
Across all of them
- Bearer authentication, scoped by owning organization plus a read set and an operate set
- 300 requests per minute per key, with
x-ratelimit-*andretry-after x-request-idon every response, echoed if you send one- Error envelope
{ error, message?, ...extras }with stable codes