Lifecycle code catalog
The complete list of invoice lifecycle codes Nuntiq knows about, plus the state-machine rules that govern how invoices transition between them.
For how to use the codes (write a message, poll for new ones, handle errors) see Concepts → Lifecycle messages. This page is the field-level reference for the codes themselves.
Tiers
Every code belongs to one of three tiers. Tiers are loose groupings —
they don't restrict transitions on their own, but they're useful for
filtering (get_for_invoice(tier=2) returns only buyer-side events) and
for understanding where in an invoice's life a code typically appears.
| Tier | Number | Stage | Typical actor |
|---|---|---|---|
| Intake | 1 | From document arrival through validation | Nuntiq platform |
| Buyer-side processing | 2 | ERP, matching, approval, query/hold | The customer's AP team or their ERP |
| Financial | 3 | Payment scheduling, payment execution, cancellation | Treasury / payments platform |
Externally writable
As of 2026-05-17, every code in the catalog is externally writable via
the Customer API. Any connector or integration with appropriate permissions
can post any code — including intake codes like RECEIVED and READY,
which were previously Nuntiq-only.
This doesn't mean any sequence is valid: the state machine
(lifecycleStateMachine.js) still enforces ordering rules. See
State transitions below.
Tier 1 — Intake codes
| Code | Label | Description | Portal-visible default | Email default |
|---|---|---|---|---|
RECEIVED | Received | Invoice has been successfully captured and saved. | yes | no |
DUPLICATE_DETECTED | Duplicate Invoice Detected | Invoice matches an existing record. Processing halted pending review. | yes | yes |
VALIDATION_FAILED | Validation Failed | Structural or compliance checks failed. Processing halted. | yes | yes |
VALIDATION_WARNING | Validation Warning | Validation produced warnings. Processing continues but review recommended. | no | no |
VALIDATION_INFO | Validation Info | Informational note from validation rules. No action required. | no | no |
READY | Ready for Integration | Invoice has passed all intake checks and is queued for export. | no | no |
Tier 2 — Buyer-side processing
| Code | Label | Description | Portal-visible default | Email default |
|---|---|---|---|---|
ACKNOWLEDGED | Sent to ERP | Invoice has been exported to the customer's ERP. | yes | no |
IN_PROCESS | In Process | ERP confirms the invoice has been received and workflow has started. | yes | no |
MATCHED | PO Match Successful | Invoice matched to purchase orders within tolerances. | no | no |
MATCH_EXCEPTION | PO Match Exception | Invoice could not be auto-matched due to a discrepancy. | no | no |
CODING_COMPLETE | GL Coding Complete | Invoice coded to GL accounts, cost centers, and accounting dimensions. | no | no |
UNDER_QUERY | Under Query | Buyer requests clarification or corrected information from supplier. | yes | yes |
UNDER_QUERY_RESOLVED | Query Resolved | Outstanding query has been resolved; invoice continues processing. | yes | no |
ON_HOLD | On Hold | Invoice placed on hold (budget hold, internal dispute, etc.). | yes | no |
ON_HOLD_RESOLVED | Hold Resolved | Hold has been lifted; invoice continues processing. | yes | no |
IN_APPROVAL | In Approval | Invoice routed into the buyer's approval workflow. | yes | no |
CONDITIONALLY_ACCEPTED | Conditionally Accepted | Buyer accepts under stated conditions (e.g. deduction applied). | yes | yes |
APPROVED | Approved | Buyer has given final approval for payment as submitted. | yes | no |
APPROVAL_REVOKED | Approval Revoked | A previously granted approval has been withdrawn before payment. ⚠️ Compensating event. | no | no |
REJECTED | Rejected | Buyer will not process this invoice further. Hard terminal state. | yes | yes |
ACCEPTED | Accepted / Posted to ERP | Invoice posted as a payable in the buyer's ERP. Payment not yet scheduled. | no | no |
Tier 3 — Financial
| Code | Label | Description | Portal-visible default | Email default |
|---|---|---|---|---|
SCHEDULED_FOR_PAYMENT | Scheduled for Payment | Invoice included in a payment run. | yes | no |
PAYMENT_RUN_CANCELLED | Payment Run Cancelled | Payment run voided before funds were transferred. ⚠️ Compensating event. | no | no |
PAID | Paid | Invoice has been fully paid. | yes | no |
PARTIALLY_PAID | Partially Paid | A partial payment has been made with intent to pay the remainder. | yes | yes |
PAYMENT_REVERSED | Payment Reversed | Funds returned or bank transaction reversed. ⚠️ Compensating event. | yes | yes |
PAYMENT_ON_HOLD | Payment on Hold | Payment withheld (bank details verification, account frozen, etc.). | yes | yes |
CANCELLED | Cancelled | Invoice voided, typically following a credit note. Hard terminal state. | yes | no |
State transitions
The server-side state machine enforces ordering rules in three strictness
modes (configurable per tenant in tenant_settings):
| Mode | What's enforced |
|---|---|
none | Only hard terminals (REJECTED, CANCELLED) block further messages |
relaxed (default) | Hard terminals plus compensating-event predecessor rules |
strict | The full transition graph (see STRICT_VALID_PREDECESSORS) |
Happy-path flow
Side-paths: query, hold, rejection, cancellation
Any of these can happen at most points in the buyer-side flow:
Compensating events (reverse-flow)
Three codes are explicitly designed to reverse a prior state. They have
strict predecessor + successor rules even in relaxed mode:
Posting a compensating code from an unsupported predecessor returns 409
LIFECYCLE_TRANSITION_INVALID with valid_next_codes populated.
Hard terminals
Once an invoice reaches REJECTED or CANCELLED, no further lifecycle
messages are accepted — the API returns 409 TERMINAL_STATE. These
events are final; if the underlying business situation reverses, create a
new invoice (typically alongside a credit note for CANCELLED).
Full predecessor reference (strict mode)
In strict strictness mode, every code can only be posted when the
invoice's latest code is in the allow-list below. (In relaxed mode this
table is informational only — only compensating-event rules + hard
terminals are enforced.)
| New code | Valid predecessors (latest existing code) |
|---|---|
RECEIVED | (first message — invoice has no prior code) |
DUPLICATE_DETECTED | RECEIVED |
VALIDATION_FAILED | RECEIVED |
VALIDATION_WARNING | RECEIVED |
VALIDATION_INFO | RECEIVED |
READY | RECEIVED, VALIDATION_INFO |
ACKNOWLEDGED | READY |
IN_PROCESS | ACKNOWLEDGED |
MATCHED | IN_PROCESS, ACKNOWLEDGED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED |
MATCH_EXCEPTION | IN_PROCESS, ACKNOWLEDGED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED |
CODING_COMPLETE | IN_PROCESS, ACKNOWLEDGED, MATCHED, MATCH_EXCEPTION, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED |
UNDER_QUERY | IN_PROCESS, ACKNOWLEDGED, MATCHED, MATCH_EXCEPTION, CODING_COMPLETE, APPROVAL_REVOKED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED |
UNDER_QUERY_RESOLVED | UNDER_QUERY |
ON_HOLD | IN_PROCESS, ACKNOWLEDGED, MATCHED, MATCH_EXCEPTION, CODING_COMPLETE, APPROVAL_REVOKED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED |
ON_HOLD_RESOLVED | ON_HOLD |
IN_APPROVAL | IN_PROCESS, ACKNOWLEDGED, MATCHED, CODING_COMPLETE, UNDER_QUERY, UNDER_QUERY_RESOLVED, ON_HOLD, ON_HOLD_RESOLVED, APPROVAL_REVOKED |
CONDITIONALLY_ACCEPTED | IN_APPROVAL |
APPROVED | IN_APPROVAL |
APPROVAL_REVOKED | APPROVED, CONDITIONALLY_ACCEPTED, ACCEPTED |
REJECTED | IN_APPROVAL, IN_PROCESS, ACKNOWLEDGED, UNDER_QUERY, UNDER_QUERY_RESOLVED, ON_HOLD, ON_HOLD_RESOLVED, MATCH_EXCEPTION |
ACCEPTED | APPROVED, CONDITIONALLY_ACCEPTED |
SCHEDULED_FOR_PAYMENT | ACCEPTED, PAYMENT_RUN_CANCELLED, PAYMENT_REVERSED |
PAYMENT_RUN_CANCELLED | SCHEDULED_FOR_PAYMENT |
PAID | SCHEDULED_FOR_PAYMENT |
PARTIALLY_PAID | SCHEDULED_FOR_PAYMENT |
PAYMENT_REVERSED | PAID, PARTIALLY_PAID |
PAYMENT_ON_HOLD | SCHEDULED_FOR_PAYMENT, PAYMENT_RUN_CANCELLED, PAYMENT_REVERSED |
CANCELLED | (any non-terminal state) |
Programmatic access
The connector framework SDK exposes the same catalog as Python constants so connector code can iterate, look up, or validate without hardcoding.
from lib.objects.lifecycle_message import (
LIFECYCLE_CODES, # the full {code: {tier, label, description, ...}} dict
ALL_CODES, # frozenset of all valid code strings
EXTERNALLY_WRITABLE_CODES, # frozenset of codes the API accepts via POST
HARD_TERMINAL_CODES, # frozenset of {'REJECTED', 'CANCELLED'}
CODES_BY_TIER, # {'INTAKE': [...], 'BUYER_SIDE': [...], 'FINANCIAL': [...]}
COMPENSATING_CODES, # {code: {'valid_from': frozenset, 'valid_next': frozenset}}
get_code_info, # (code) -> dict | None
is_valid_code, # (code) -> bool
is_externally_writable, # (code) -> bool
is_hard_terminal, # (code) -> bool
)
# Look up
info = get_code_info('PAID')
print(info['tier'], info['label']) # FINANCIAL Paid
# Iterate by tier
for code in CODES_BY_TIER['FINANCIAL']:
print(code, LIFECYCLE_CODES[code]['label'])
# Build a UI dropdown
options = [
(code, info['label'])
for code, info in LIFECYCLE_CODES.items()
if info['tier'] == 'BUYER_SIDE'
]
The SDK catalog is kept in sync with the server's lifecycleCatalog.js
manually — if your code branches on the catalog and you suspect drift,
the server is the source of truth.
What's next
- Lifecycle messages — usage — how to read and write messages (ApiClient calls, examples, common pitfalls).
- LifecycleMessageLoad reference — field-level reference for the Load class.