API · Conventions
Lifecycle state machine
Every entity's allowed states and transitions. Invalid transitions return 409 invalid_state_transition. Each transition writes an immutable AuditEntry.
Last updated
TL;DR
Every Matter entity moves through a fixed state machine — draft → submitted → registered → active → dissolving → dissolved, with suspended as a side-state for
compliance lapses. Mutations that would violate this graph return
409 invalid_state_transition. Every transition writes an immutable
AuditEntry and emits entity.state_changed with both the
old and new state.
After reading this page you'll know which states an entity can be in, which
transitions are legal, which API call drives each transition, which 409 you'll
hit when you attempt an illegal one, and which webhook to listen for.
Why this exists
State filings are not API mutations — they're durable, multi-day, externally
witnessed legal events. Modeling the entity as a state machine lets the API
be idempotent on the boundary (your POST /v1/entities/{id}/dissolve is safe
to retry until the state actually flips) and strict in the middle (you cannot
issue a grant on a draft entity, you cannot dissolve a dissolved one). The
state is the source of truth; everything else (cap-table rows, filings,
documents) is derivative.
States
| State | Meaning | Mutable? |
|---|---|---|
draft | Intent resolved, awaiting authorization. No state filing yet. | Yes (limited) |
submitted | Filed with the secretary of state. Awaiting their response. | No |
registered | Certificate issued. Operational setup (EIN, governance docs, registered agent) in progress. | Yes |
active | Fully operational legal entity. The working state for 99% of API calls. | Yes |
suspended | Compliance lapse — missed annual report, missed franchise tax. Side-state from active. | Yes (limited) |
dissolving | Dissolution cascade in progress (Form 966 → final franchise tax → certificate of dissolution → BOI closure). | No |
dissolved | Terminal. Immutable. The entity legally no longer exists. | No |
rejected | The state denied the filing. The draft is retryable after fixing the cause. | Yes (re-edit and re-submit) |
cancelled | The caller cancelled before filing. Terminal but not legally meaningful. | No |
draft, registered, active, and suspended accept some mutations; consult
the per-endpoint docs for the exact allow-list. The other five states are read-only.
Transition graph
┌──────── rejected ◀─────┐
│ │
draft ──────────▶┴── submitted ─────▶ registered ─▶ active ───▶ dissolving ─▶ dissolved
│ ▲ │
▼ │ ▼
cancelled suspended
│
└──▶ dissolving| From | To | Driving API call | Webhook |
|---|---|---|---|
draft | submitted | POST /v1/intents/{id}/execute (or POST /v1/entities with execute: true) | entity.state_changed |
draft | cancelled | POST /v1/intents/{id}/cancel | intent.cancelled |
submitted | registered | (none — driven by state acceptance) | filing.accepted then entity.state_changed |
submitted | rejected | (none — driven by state denial) | filing.rejected then entity.state_changed |
rejected | draft | PATCH /v1/entities/{id} + re-execute | entity.state_changed |
registered | active | (none — driven by operational-setup completion) | entity.state_changed |
active | suspended | (none — driven by compliance-deadline lapse) | compliance.lapsed then entity.state_changed |
suspended | active | POST /v1/entities/{id}/reinstate | entity.state_changed |
active | dissolving | POST /v1/entities/{id}/dissolve | entity.state_changed |
suspended | dissolving | POST /v1/entities/{id}/dissolve | entity.state_changed |
dissolving | dissolved | (none — driven by certificate of dissolution) | filing.accepted (cert. of dissolution) then entity.state_changed |
There are no other transitions. Anything you might attempt that is not in this
table returns 409 invalid_state_transition. Terminal states (dissolved,
cancelled) accept no transitions; even retries return 409.
What you can call in each state
| Surface | draft | submitted | registered | active | suspended | dissolving | dissolved |
|---|---|---|---|---|---|---|---|
Issue a Grant | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
Create a Filing | ❌ | ❌ | ❌ | ✅ | ✅ | ⚠️ dissolution-only | ❌ |
Create a Resolution | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
Sign a Document | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ |
Generate a Document | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ (read-only) |
| Read everything | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
⚠️ means type-restricted: e.g. during dissolving, only filings of
type: "form_966", "final_franchise_tax", "certificate_of_dissolution", and
"boi_closure" are accepted; other types fail with 422 invalid_filing_type_for_state.
What happens on every transition
Each state change atomically:
- Writes a
Filingrecord (where applicable) with the external receipt. - Writes an immutable
AuditEntrycarrying both thehuman_principal_idandagent_id(see agents). - Emits an
entity.state_changedevent withprevious_state,new_state,transition_reason, and the strict per-entitysequencenumber (see webhook ordering). - Increments the entity's
versionfield for optimistic-concurrency callers (If-MatchETag).
These four are atomic — you'll never observe an entity.state_changed event
without the corresponding AuditEntry, and you'll never see the new state via
GET /v1/entities/{id} before the event has been queued for delivery.
Common 409s
| Error | Cause | Recovery |
|---|---|---|
invalid_state_transition | The target state is not reachable from the current state. The body's from, to, and allowed_to[] fields tell you what would be legal. | Read the entity, choose a legal target, or wait for an upstream transition (e.g. registered → active). |
state_filing_in_flight | A previous state filing has not yet been accepted or rejected by the SoS. The body includes the in-flight filing_id. | Wait for the filing.accepted or filing.rejected webhook before retrying. |
dissolution_blocked_open_obligations | Attempted to dissolve while the entity has open compliance obligations or unsettled filings. | Resolve via POST /v1/workflows/file_all_due, then retry the dissolve. |
optimistic_concurrency_failure | Your If-Match ETag was stale because another caller transitioned the state. | Re-fetch and reconcile. |
See the errors index for the RFC 7807 envelope these codes ride in.
Reading the current state
curl https://api.mattermode.com/v1/entities/ent_Nq3KcAbc \
-H "Authorization: Bearer $MATTER_KEY" \
-H "Matter-Version: 2026-05-01"{
"id": "ent_Nq3KcAbc",
"object": "entity",
"state": "active",
"previous_state": "registered",
"state_entered_at": 1745539200,
"version": 7,
…
}previous_state and state_entered_at always reflect the most recent transition
— they're stable across GET retries.
Subscribing to transitions
curl https://api.mattermode.com/v1/webhook_endpoints \
-H "Authorization: Bearer $MATTER_KEY" \
-H "Matter-Version: 2026-05-01" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your.app/webhooks/matter",
"enabled_events": ["entity.state_changed", "filing.accepted", "filing.rejected"]
}'Strict per-entity ordering means an entity.state_changed for ent_X always
arrives after the preceding entity.state_changed for the same entity, even
across redeliveries. See ordering for the
contract.
Related
- Form a company — the canonical
draft → activecascade. - Dissolve an entity — the canonical
active → dissolvedcascade. - Errors — RFC 7807 envelope and the full code catalog.
- Webhook ordering — strict per-entity sequence guarantees.
- Audit — every transition leaves a row.