LifecycleMessageLoad
Read and write the timeline of events on an invoice. The conceptual page is Concepts → Lifecycle messages — read that for the state machine, code catalog, and write-vs-read overview. This page is the field-level reference for the Load class.
Import
from lib.objects.lifecycle_message import LifecycleMessageLoad
Write — new() + post_to() / post_by_reference()
Build a message in memory, set its fields, then post to either the invoice token (preferred) or the external invoice_number+supplier_code reference.
load = LifecycleMessageLoad(context)
msg = load.new()
msg.code = 'ACCEPTED' # required
msg.reference_type = 'PAYMENT_REF'
msg.reference_value = 'ERP-INV-12345'
msg.note_supplier = 'Invoice posted to AP queue.'
msg.note_internal = 'ERP batch 2026-05-17-001'
resp = msg.post_to(
invoice_token='7f3a...',
idempotency_key='erp-accept-12345', # optional but recommended
)
| Field | Type | Required | Notes |
|---|---|---|---|
code | str | yes | Any valid lifecycle code from the code catalog. All 28 codes are externally writable; the state machine enforces ordering. The SDK logs a warning if you set a code that isn't in its catalog (typo guard). |
occurred_at | str (ISO-8601) | no | Defaults to NOW server-side. Must be within 30 days of current time. |
actor_display_name | str | no | Defaults to the API user's name |
clarification_code | str | no | Per-customer config can make this required for specific codes |
note_internal | str | no | Max 4000 chars. AP-team-only — not visible to suppliers |
note_supplier | str | no | Max 4000 chars. Visible to suppliers in the supplier portal AND the AP team |
reference_type | str | no | One of PO_NUMBER, PAYMENT_REF, CREDIT_NOTE, OTHER |
reference_value | str | no | Free-form id from the external system |
Local validation runs in post_to() / post_by_reference() before
the HTTP call — invalid reference_type, oversized notes, missing code
all raise ValueError before any network round-trip.
post_by_reference() — when you don't have the invoice token
resp = msg.post_by_reference(
invoice_number='INV-2026-0042',
supplier_code='SUP-100',
supplier_location_code='MAIN', # optional, narrows the match
erp_company_code='EMEA', # optional
idempotency_key='pmt-77-3-paid',
)
If the (invoice_number, supplier_code) pair matches more than one invoice,
the server returns 400 — pass supplier_location_code and/or
erp_company_code until you narrow to one match.
Read — get_for_invoice()
Returns the full ordered timeline for one invoice as a list of
LifecycleMessage objects.
for m in load.get_for_invoice('7f3a...'):
print(m.recorded_at, m.code, m.note_supplier)
Optional args:
| Arg | Notes |
|---|---|
tier | 1 (intake), 2 (buyer-side), 3 (financial), or None (all) |
include_internal_notes | Requires LIFECYCLE_READ_INTERNAL on the API user. When True without the permission, internal notes are silently omitted (no error). |
Read — poll_since() (cross-invoice delta)
For "what changed across many invoices since I last polled". Auto-paginates through results — you get a generator.
for m in load.poll_since(
since='2026-04-25T08:00:00Z', # ISO-8601 UTC
ext_references={1: 'CUSTOMER-A'}, # at least one ext_reference_N required
tz='UTC', # only consulted when `since` is naked
tier=None, # 1, 2, 3, or None for all
include_internal_notes=False,
page_size=500, # default 500, max 5000
):
print(m.invoice_token, m.recorded_at, m.code)
Watermark for next poll
Messages come back ordered by recorded_at ASC. Store the
recorded_at of the last message you saw, and pass it as since on the
next call. Use the delta state helpers to
manage the watermark across runs without writing the bookkeeping yourself.
ext_references filter
Required — at least one of keys 1..5 must be a non-empty string.
Comparison is case-insensitive equality — wildcards are treated
literally. Use it to scope per customer / per ERP / per BU according to
how you've populated ext_reference_1..5 on the invoice side.
LifecycleMessage (read shape)
Returned by both get_for_invoice() and poll_since(). Attribute names
mirror the API response.
| Field | Notes |
|---|---|
id | int — internal message id |
invoice_id | int (from delta endpoint) or token string (from per-invoice endpoint) |
invoice_token | str (UUID) — always populated when available |
code | str — the lifecycle event code |
source | str — NUNTIQ, EXTERNAL_API, or MANUAL_USER |
actor_type | str — SYSTEM, API_CLIENT, or USER |
actor_id | str (UUID) or None |
actor_display_name | str or None |
occurred_at | str (ISO-8601) — when the event actually happened |
recorded_at | str (ISO-8601) — when Nuntiq learned about the event |
clarification_code | str or None |
note_internal | str or None (gated on permission) |
note_supplier | str or None |
reference_type | str or None |
reference_value | str or None |
ext_reference_1..5 | str or None — only populated when returned by poll_since() (carried from the parent invoice) |
The difference between occurred_at and recorded_at: when an upstream
system posts a back-dated event ("this was approved last Tuesday"),
occurred_at is Tuesday but recorded_at is now. Always page on
recorded_at for poll watermarks; sort/display on occurred_at.
Common pitfalls
Most failure modes are documented on Concepts → Lifecycle messages → Common pitfalls. Quick recap:
| Server response | Cause |
|---|---|
400 INVALID_CODE | Typo / unknown code string. Check against LIFECYCLE_CODES or the catalog. |
400 NOTE_REQUIRED / CLARIFICATION_CODE_REQUIRED | Tenant config requires the field |
422 INVOICE_NOT_READY | Posting before intake finished (status < 90) |
409 LIFECYCLE_TRANSITION_INVALID | State machine refused. Read valid_next_codes. |
409 TERMINAL_STATE | Invoice is at REJECTED or CANCELLED |
200 with idempotent: true | Re-post deduped by idempotency_key |
What's next
- Concepts → Lifecycle messages — the full conceptual write-up including the code catalog and state machine.
- InvoiceLoad — the read/claim/ack/result flow for the invoices these messages are written against.