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:

v1 · 2026-10-19 — key-lane completeness

New doors for keys:

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.


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):

Changed content:

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):


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):

New response fields (additive):

v1 · 2026-10-07 — readable requirement conditions

New response field (additive; see endpoints.md):

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):

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):

New code: compat_type_not_connectable (422) — a compatibility type used for a connection on a port.

Behaviour, no shape change:

v1 · 2026-10-04 — ports and attachments

Four new doors (see endpoints.md):

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):

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:


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:


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:

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

Resources — reading

Resources — writing

Creating and addressing

Reference data

Across all of them