Skip to main content

IngestionLoad

Push invoices INTO Nuntiq from a connector. Two flows:

  1. Drop a file into the standard pipeline: same path as email intake. Nuntiq runs OCR, picks a template, extracts fields.
  2. 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',
)
ArgRequiredNotes
filename / files[*][0]yesUsed by Nuntiq for display + MIME inference
content / files[*][1]yesRaw bytes
content_typenoInferred from filename when omitted
receiving_inboxnoControls which template Nuntiq picks
supplier_codenoRouting hint; skips supplier AI match
company_codenoRouting hint; skips company AI match
allow_duplicatenoTrue 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_type before anything is stored; an upload without a PDF with 422 no_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​

FieldTypeNotes
source_document_idintThe new source document's id
source_tokenstr (UUID)The source document's token
files_uploadedintNumber of files stored (>=1; ZIP entries count)
pdf_files_queuedintNumber of PDFs queued for processing
upload_session_idstr (UUID)Groups this upload, useful for log searches
duplicateboolTrue when the same files came from this connector within 24 hours; nothing was processed
duplicate_ofintThe 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​

MethodAddsFields
new_line()IngestionLinethe line fields of the Customer API import
new_address(address_type)IngestionAddressaddress_name, street, city, state, postal_code, country
new_tax()IngestionTaxtax_name, tax_rate, tax_base_amount, tax_amount, tax_description
new_charge()IngestionChargecharge_name, charge_amount (freight, handling: part of the net amount)
new_discount()IngestionDiscountdiscount_description, discount_rate, discount_amount, discount_due_date, due_date_basis, due_date_period_value, due_date_period_measure
new_deduction(deduction_type)IngestionDeductiondeduction_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​

workflowResulting statusUse when
payment_portal (default)100 (Processed)Upstream data is fully verified; skip enrichment
basic_enrichment10Run light enrichment, no AI
full_enrichment1Run the full AI-match + validation pipeline

Attachment rules​

  • attach_image(): at most one per invoice. Calling it twice raises ValueError. 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 422 unknown_attachment_token before it creates anything.

InvoiceImportResult​

FieldTypeNotes
invoice_tokenstr (UUID)Pass to InvoiceLoad.get_by_token()
invoice_idintInternal numeric id
source_document_idintUnderlying source document
workflowstrWorkflow used
invoice_statusintFinal numeric status (e.g. 100 = Processed)
source_document_statusintFinal source-document status
template_idintTemplate Nuntiq used
template_typestr'global' or 'customer'
attachments_linkedintCount of attachments tied to the invoice
receiving_inboxstrInbox 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.duplicate is True and result.duplicate_of names the earlier source document. allow_duplicate=True processes them anyway.
  • submit(): an import has no duplicate check on its content; sending the same invoice twice creates two invoices. The import endpoint takes an Idempotency-Key header (the same key and body within 24 hours replay the first answer), which submit() 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​