IngestionLoad
Push invoices INTO Nuntiq from a connector. Two flows:
- Drop a file into the standard pipeline: same path as email intake. Nuntiq runs OCR, picks a template, extracts fields.
- Build a structured invoice with fields + lines + addresses, then attach the original PDF, then submit. Skips OCR entirely.
For the worked-example tutorial, see Patterns → Invoice ingestion.
Import
from lib.objects.ingestion import IngestionLoad
Run it from the get_invoice entrypoint. Its job token may upload source
documents (pattern 1) and import invoices with attachments (pattern 2).
Other entrypoint keys get a 403.
ingest_pdf / ingest_files (Pattern 1)
load = IngestionLoad(context)
result = load.ingest_pdf(
filename='invoice.pdf',
content=pdf_bytes,
receiving_inbox='invoices@acme.apreceiving.com', # all kwargs optional
supplier_code='SUP-100',
company_code='EMEA',
)
if result.duplicate:
print('already sent as', result.duplicate_of)
else:
print(result.source_document_id) # int
print(result.files_uploaded) # int (>=1; ZIP entries count)
For multiple files in one call (they become one source document):
result = load.ingest_files(
files=[
('invoice-1.pdf', pdf_bytes_1, None),
('batch.zip', zip_of_pdfs, None),
],
receiving_inbox='invoices@acme.apreceiving.com',
)
| Arg | Required | Notes |
|---|---|---|
filename / files[*][0] | yes | Used by Nuntiq for display + MIME inference |
content / files[*][1] | yes | Raw bytes |
content_type | no | Inferred from filename when omitted |
receiving_inbox | no | Controls which template Nuntiq picks |
supplier_code | no | Routing hint; skips supplier AI match |
company_code | no | Routing hint; skips company AI match |
allow_duplicate | no | True processes files this connector already sent within 24 hours |
- Max 20 files per call.
- The type is read from the bytes, not the name: PDFs are captured,
loose or in a ZIP. Next to them, loose or in the ZIP, PNG, JPEG, TIFF,
GIF, DOCX, XLSX, PPTX, DOC, XLS, CSV, TXT, XML, JSON, EML and MSG files
are kept as attachments of the source document (not captured, attached
to every invoice made from the upload). Anything else, also an archive
inside the ZIP, is refused with 415
unsupported_file_typebefore anything is stored; an upload without a PDF with 422no_processable_file. - ZIPs are opened under limits: 200 entries, 250 MB unpacked, no
encrypted entries (422
zip_rejected). - To choose per file what is captured (a PDF kept as an attachment, for
example) or to send e-invoices, use
context.api.upload_files(): Sending files. - Files up to 50 MB are sent as a normal upload. A larger file (up to 500 MB) goes through a presigned upload; the framework does that by itself.
SourceDocumentResult
| Field | Type | Notes |
|---|---|---|
source_document_id | int | The new source document's id |
source_token | str (UUID) | The source document's token |
files_uploaded | int | Number of files stored (>=1; ZIP entries count) |
pdf_files_queued | int | Number of PDFs queued for processing |
upload_session_id | str (UUID) | Groups this upload, useful for log searches |
duplicate | bool | True when the same files came from this connector within 24 hours; nothing was processed |
duplicate_of | int | The earlier source document, when duplicate |
status()
st = load.status(result) # a SourceDocumentResult, a source document id or its token
st['status'] # received, processing, completed, failed, timeout
st['files'] # each with name, content_type, size_bytes, sha256, error
st['invoices'] # the invoices made from it
new_invoice(): Pattern 2 (structured + PDF)
load = IngestionLoad(context)
inv = load.new_invoice(
workflow='payment_portal', # default
receiving_inbox='invoices@acme.apreceiving.com',
)
# Header: set whatever you have. All fields optional, all template-driven.
inv.header.invoice_number = 'INV-2026-0042'
inv.header.invoice_date = '2026-05-17' # ISO date
inv.header.due_date = '2026-06-16'
inv.header.gross_amount = 1815.00 # total before deductions
inv.header.tax_amount = 315.00
inv.header.net_amount = 1500.00
inv.header.currency_code = 'EUR'
inv.header.supplier_name = 'Acme Corp'
inv.header.order_number_1 = 'PO-2026-0042'
inv.header.custom_text_1 = 'cost-center-42'
# Lines
line = inv.new_line()
line.product_code_1 = 'WIDGET-A'
line.product_name = 'Office Widget'
line.quantity = 100
line.unit_price = 12.50
line.unit_of_measure = 'EA'
line.net_amount = 1250.00
line.tax_amount = 262.50
line.gross_amount = 1512.50
# Addresses: type must be REMITTO | SUPPLIER | SHIPTO | BILLTO
addr = inv.new_address('SUPPLIER')
addr.street = 'Herengracht 500'
addr.city = 'Amsterdam'
addr.country = 'NL'
# Optional: taxes / charges / discounts / deductions
tax = inv.new_tax()
tax.tax_name = 'VAT'
tax.tax_rate = 21
tax.tax_amount = 315.00
# Deductions come off after the total: retainage, withholding tax, deposits.
# deduction_type must be retainage | withholding | deposit | other (else ValueError).
ded = inv.new_deduction('withholding')
ded.deduction_description = "Ritenuta d'acconto 20%"
ded.deduction_rate = 20
ded.deduction_base_amount = 1500.00
ded.deduction_amount = 300.00
inv.header.amount_due = 1515.00 # gross 1815.00 minus 300.00
# Attachments
inv.attach_image(filename='invoice.pdf', content=pdf_bytes) # exactly one
inv.attach_file(filename='delivery_note.pdf', content=dn_bytes) # 0..N
# Send
result = inv.submit()
print(result.invoice_token, result.invoice_status)
Children
| Method | Adds | Fields |
|---|---|---|
new_line() | IngestionLine | the line fields of the Customer API import |
new_address(address_type) | IngestionAddress | address_name, street, city, state, postal_code, country |
new_tax() | IngestionTax | tax_name, tax_rate, tax_base_amount, tax_amount, tax_description |
new_charge() | IngestionCharge | charge_name, charge_amount (freight, handling: part of the net amount) |
new_discount() | IngestionDiscount | discount_description, discount_rate, discount_amount, discount_due_date, due_date_basis, due_date_period_value, due_date_period_measure |
new_deduction(deduction_type) | IngestionDeduction | deduction_type, deduction_description, deduction_rate, deduction_base_amount, deduction_amount |
header.gross_amount is the invoice total (net plus tax) before
deductions; header.amount_due is the amount payable after them. Leave
amount_due out when the invoice has no deductions.
Workflow choice
workflow | Resulting status | Use when |
|---|---|---|
payment_portal (default) | 100 (Processed) | Upstream data is fully verified; skip enrichment |
basic_enrichment | 10 | Run light enrichment, no AI |
full_enrichment | 1 | Run the full AI-match + validation pipeline |
Attachment rules
attach_image(): at most one per invoice. Calling it twice raisesValueError. This is the PDF the AP team will view; multiple "primary" PDFs would confuse the viewer.attach_file(): 0..N supporting documents (delivery notes, supporting POs, etc.). Visible to the AP team but not the primary view.- If you don't pass any IMAGE, the invoice has no viewable PDF, so the AP team sees structured fields only. Usually a poor UX.
- Attachments may be PDF, PNG, JPEG, TIFF, XML or a ZIP of those (other
types: 415
unsupported_file_type).submit()uploads them first; the import refuses an unknown or already used document token with 422unknown_attachment_tokenbefore it creates anything.
InvoiceImportResult
| Field | Type | Notes |
|---|---|---|
invoice_token | str (UUID) | Pass to InvoiceLoad.get_by_token() |
invoice_id | int | Internal numeric id |
source_document_id | int | Underlying source document |
workflow | str | Workflow used |
invoice_status | int | Final numeric status (e.g. 100 = Processed) |
source_document_status | int | Final source-document status |
template_id | int | Template Nuntiq used |
template_type | str | 'global' or 'customer' |
attachments_linked | int | Count of attachments tied to the invoice |
receiving_inbox | str | Inbox used for template lookup |
Error handling
ingest_pdf / ingest_files and submit() raise ApiError on transport
or HTTP failure. See ApiClient → Error handling.
Local validation (workflow value, address_type value, max-1-image) raises
ValueError before the HTTP call, so you find out at code-write time,
not after a network round trip.
Idempotency
ingest_pdf()/ingest_files(): the same files from the same connector within 24 hours are not processed again.result.duplicateisTrueandresult.duplicate_ofnames the earlier source document.allow_duplicate=Trueprocesses them anyway.submit(): an import has no duplicate check on its content; sending the same invoice twice creates two invoices. The import endpoint takes anIdempotency-Keyheader (the same key and body within 24 hours replay the first answer), whichsubmit()does not send. See ApiClient → Idempotency-Key.
Track which upstream records you've already submitted in your own state
(typically via delta_run) and don't
re-submit.
What's next
- Patterns → Invoice ingestion: worked-example tutorials for both flows.
- InvoiceLoad: once ingested, this is how you read the resulting invoice or claim it for downstream push.