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