Skip to main content

Sending files: capture or e-invoice

A connector that receives invoice files hands them to Nuntiq with one call: context.api.upload_files(process, files, ...). You say per call which process runs and per file what the file is. Nothing is routed by guesswork: a missing or unknown role stops the call before anything is sent.

processWhat Nuntiq doesRolesOne call is
'capture'OCR, template extraction and enrichment, the same path as email intake'document': captured (a PDF, or a ZIP of PDFs). 'attachment': kept with the source document and attached to every invoice captured from it, never captured (any allowed type, a PDF too)one source document
'einvoice'The customer's inbound file formats map the data file into invoices'data': an e-invoice or data file (XML, EDI, CSV, JSON, TXT). 'attachment': a PDF or image (PNG, JPEG, TIFF) for the invoice whose data file names itone batch

A file is (name, bytes, role), or (name, bytes, role, content_type). Run the code under the get_invoice entrypoint key (get_invoice_status may send e-invoices too); other keys get a 403.

Scenario 1: a PDF to capture, with the XML behind it​

The source system gives you the invoice PDF and the XML it was printed from, and the customer captures from the PDF. Keep the XML with the invoice:

connectors/get_invoice.py
r = context.api.upload_files('capture', [
('INV-1001.pdf', pdf_bytes, 'document'),
('INV-1001.xml', xml_bytes, 'attachment'),
('terms.pdf', terms_bytes, 'attachment'), # a PDF that is not an invoice: kept, not captured
], receiving_inbox='invoices@acme.apreceiving.com')

d = r['data']
d['sourceDocumentId'], d['pdfFilesQueued'], d['attachmentsStored'] # 1 PDF queued, 2 attachments
context.api.get_source_document(d['sourceDocumentId']) # status, files, invoices

Images, Office documents (DOCX, XLSX, PPTX, DOC, XLS), CSV, TXT, XML, JSON, EML and MSG files can be attachments; at least one PDF must be a 'document'. A ZIP takes one role for all its PDFs; its other files are always attachments. company_code and supplier_code are optional hints for the capture.

Scenario 2: an e-invoice and the PDF it references​

The source system gives you a UBL invoice whose AdditionalDocumentReference names INV-1002.pdf, and that PDF. Both go in one batch; the file format maps the XML and links the PDF as the invoice image:

r = context.api.upload_files('einvoice', [
('INV-1002.xml', xml_bytes, 'data'),
('INV-1002.pdf', pdf_bytes, 'attachment'),
])
batch = r['data']['upload_batch_id'] # made by the framework when you do not pass one
for f in context.api.get_inbound_uploads(batch)['data']:
print(f['filename'], f['status'], f['invoices']) # received, queued, consumed or failed

The customer needs an inbound file format that identifies the file and maps the referenced file name to attachment_reference (Inbound File Formats in the admin portal; global UBL, CII, Peppol and XRechnung templates exist). A file no format identifies ends failed with NO_MATCH. A data file that names no attachment gets a generated PDF.

Scenario 3: a PDF to capture and an unrelated e-invoice​

Two different invoices, two processes, two calls:

context.api.upload_files('capture', [('scan-0042.pdf', scan_bytes, 'document')])
context.api.upload_files('einvoice', [('INV-1003.xml', xml_bytes, 'data')])

How e-invoice attachments find their invoice​

An attachment is linked to the invoice whose data file references its name. The match looks at all attachments this connector sent in the last 30 days that are not linked yet, preferring the data file's own batch, then the closest name, then the newest. So an image may also arrive in a later call (the invoice waits for it as the file format's image policy says) or in an earlier one: a call with attachments only is accepted. Pass upload_batch_id to keep files that belong together in one batch over several calls.

:::warning A PDF is captured only with process='capture' With 'einvoice', a PDF can only be an attachment: it waits for a data file that names it and is never captured. A PDF sent as 'data' is refused. If a PDF is the invoice, send it with upload_files('capture', ...). :::

What is checked before the call​

upload_files() refuses before anything is sent (ValueError, naming the file):

  • a missing or unknown role, or a role of the other process;
  • upload_batch_id with 'capture', company_code or supplier_code with 'einvoice';
  • with check=True (the default), a file whose bytes do not fit its role: a 'document' that is not a PDF or ZIP, a 'data' file that is a PDF, a type the API refuses, a capture call without a 'document' PDF.

check=False leaves the type decisions to the API, which answers the same cases with ApiError 415 or 422 (invalid_file_role, no_processable_file). Files over 50 MB go through a presigned upload by themselves (up to 500 MB). The same files in the same roles within 24 hours are answered as a duplicate unless allow_duplicate=True.

Optional: classify_file() for mixed inboxes​

When a source hands you files of unknown type, read them first with the same rules the API applies:

from lib.file_types import classify_file

for name, content in incoming:
c = classify_file(name, content) # c.category, c.kind, c.reason
if c.category == 'unsupported':
context.logger.warn(f"{name}: skipped, {c.reason}")
categoryFiles
capturePDF; a ZIP with at least one PDF and otherwise only allowed files
inbound_dataXML, EDI, CSV, JSON, TXT
attachmentPNG, JPEG, TIFF (either process); GIF, Office files, EML, MSG, a ZIP without a PDF (capture only)
unsupportedanything the API refuses: executables, scripts, HTML and SVG, macro-enabled Office files

It answers what a file is, not what you meant: the role is still yours to choose (an XML can be the e-invoice or the attachment of a captured PDF).

Older helpers​

upload_source_documents(files, ...) and upload_inbound_files(data_files, attachments, ...) still work and call upload_files() with roles taken from the file types (a PDF or ZIP is a document, everything else an attachment). New code uses upload_files(). See also ApiClient and Invoice ingestion for structured imports.