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 Full predecessor reference and the API reference) |
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.