Skip to main content

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.

TierNumberStageTypical actor
Intake1From document arrival through validationNuntiq platform
Buyer-side processing2ERP, matching, approval, query/holdThe customer's AP team or their ERP
Financial3Payment scheduling, payment execution, cancellationTreasury / 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​

CodeLabelDescriptionPortal-visible defaultEmail default
RECEIVEDReceivedInvoice has been successfully captured and saved.yesno
DUPLICATE_DETECTEDDuplicate Invoice DetectedInvoice matches an existing record. Processing halted pending review.yesyes
VALIDATION_FAILEDValidation FailedStructural or compliance checks failed. Processing halted.yesyes
VALIDATION_WARNINGValidation WarningValidation produced warnings. Processing continues but review recommended.nono
VALIDATION_INFOValidation InfoInformational note from validation rules. No action required.nono
READYReady for IntegrationInvoice has passed all intake checks and is queued for export.nono

Tier 2: Buyer-side processing​

CodeLabelDescriptionPortal-visible defaultEmail default
ACKNOWLEDGEDSent to ERPInvoice has been exported to the customer's ERP.yesno
IN_PROCESSIn ProcessERP confirms the invoice has been received and workflow has started.yesno
MATCHEDPO Match SuccessfulInvoice matched to purchase orders within tolerances.nono
MATCH_EXCEPTIONPO Match ExceptionInvoice could not be auto-matched due to a discrepancy.nono
CODING_COMPLETEGL Coding CompleteInvoice coded to GL accounts, cost centers, and accounting dimensions.nono
UNDER_QUERYUnder QueryBuyer requests clarification or corrected information from supplier.yesyes
UNDER_QUERY_RESOLVEDQuery ResolvedOutstanding query has been resolved; invoice continues processing.yesno
ON_HOLDOn HoldInvoice placed on hold (budget hold, internal dispute, etc.).yesno
ON_HOLD_RESOLVEDHold ResolvedHold has been lifted; invoice continues processing.yesno
IN_APPROVALIn ApprovalInvoice routed into the buyer's approval workflow.yesno
CONDITIONALLY_ACCEPTEDConditionally AcceptedBuyer accepts under stated conditions (e.g. deduction applied).yesyes
APPROVEDApprovedBuyer has given final approval for payment as submitted.yesno
APPROVAL_REVOKEDApproval RevokedA previously granted approval has been withdrawn before payment. ⚠️ Compensating event.nono
REJECTEDRejectedBuyer will not process this invoice further. Hard terminal state.yesyes
ACCEPTEDAccepted / Posted to ERPInvoice posted as a payable in the buyer's ERP. Payment not yet scheduled.nono

Tier 3: Financial​

CodeLabelDescriptionPortal-visible defaultEmail default
SCHEDULED_FOR_PAYMENTScheduled for PaymentInvoice included in a payment run.yesno
PAYMENT_RUN_CANCELLEDPayment Run CancelledPayment run voided before funds were transferred. ⚠️ Compensating event.nono
PAIDPaidInvoice has been fully paid.yesno
PARTIALLY_PAIDPartially PaidA partial payment has been made with intent to pay the remainder.yesyes
PAYMENT_REVERSEDPayment ReversedFunds returned or bank transaction reversed. ⚠️ Compensating event.yesyes
PAYMENT_ON_HOLDPayment on HoldPayment withheld (bank details verification, account frozen, etc.).yesyes
CANCELLEDCancelledInvoice voided, typically following a credit note. Hard terminal state.yesno

State transitions​

The server-side state machine enforces ordering rules in three strictness modes (configurable per tenant in tenant_settings):

ModeWhat's enforced
noneOnly hard terminals (REJECTED, CANCELLED) block further messages
relaxed (default)Hard terminals plus compensating-event predecessor rules
strictThe 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 codeValid predecessors (latest existing code)
RECEIVED(first message, invoice has no prior code)
DUPLICATE_DETECTEDRECEIVED
VALIDATION_FAILEDRECEIVED
VALIDATION_WARNINGRECEIVED
VALIDATION_INFORECEIVED
READYRECEIVED, VALIDATION_INFO
ACKNOWLEDGEDREADY
IN_PROCESSACKNOWLEDGED
MATCHEDIN_PROCESS, ACKNOWLEDGED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED
MATCH_EXCEPTIONIN_PROCESS, ACKNOWLEDGED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED
CODING_COMPLETEIN_PROCESS, ACKNOWLEDGED, MATCHED, MATCH_EXCEPTION, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED
UNDER_QUERYIN_PROCESS, ACKNOWLEDGED, MATCHED, MATCH_EXCEPTION, CODING_COMPLETE, APPROVAL_REVOKED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED
UNDER_QUERY_RESOLVEDUNDER_QUERY
ON_HOLDIN_PROCESS, ACKNOWLEDGED, MATCHED, MATCH_EXCEPTION, CODING_COMPLETE, APPROVAL_REVOKED, UNDER_QUERY_RESOLVED, ON_HOLD_RESOLVED
ON_HOLD_RESOLVEDON_HOLD
IN_APPROVALIN_PROCESS, ACKNOWLEDGED, MATCHED, CODING_COMPLETE, UNDER_QUERY, UNDER_QUERY_RESOLVED, ON_HOLD, ON_HOLD_RESOLVED, APPROVAL_REVOKED
CONDITIONALLY_ACCEPTEDIN_APPROVAL
APPROVEDIN_APPROVAL
APPROVAL_REVOKEDAPPROVED, CONDITIONALLY_ACCEPTED, ACCEPTED
REJECTEDIN_APPROVAL, IN_PROCESS, ACKNOWLEDGED, UNDER_QUERY, UNDER_QUERY_RESOLVED, ON_HOLD, ON_HOLD_RESOLVED, MATCH_EXCEPTION
ACCEPTEDAPPROVED, CONDITIONALLY_ACCEPTED
SCHEDULED_FOR_PAYMENTACCEPTED, PAYMENT_RUN_CANCELLED, PAYMENT_REVERSED
PAYMENT_RUN_CANCELLEDSCHEDULED_FOR_PAYMENT
PAIDSCHEDULED_FOR_PAYMENT
PARTIALLY_PAIDSCHEDULED_FOR_PAYMENT
PAYMENT_REVERSEDPAID, PARTIALLY_PAID
PAYMENT_ON_HOLDSCHEDULED_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​