Submitting invoices
POST /v1/partner/invoices is the workhorse of the Supplier API. One call
pushes an invoice (header, lines, taxes, charges, discounts, addresses,
attachments) into the customer's processing queue and returns a stable
invoice_token you can use to query status later.
This page covers the request shape, the field map, attachment rules, the three-step routing chain, the idempotency contract, the response, and every error you can expect.
For the auto-generated interactive reference, see Submit invoice (Partner). The page you're reading is the human guide.
Endpoint
POST /v1/partner/invoices
Host: api.apreceiving.com
X-API-Key: <your active key>
Idempotency-Key: <optional, your unique submission id, max 255 chars>
Content-Type: application/json
Mounted at https://api.apreceiving.com/api/v1/partner/invoices in
production. See Environments for the test and dev
hosts. The path is identical across all three; only the host changes.
The handler is a thin wrapper around the Customer API's
/invoice-import route with three partner-only policies:
- Workflow is forced to
full_enrichment. Anyworkflowvalue in the body is ignored. Suppliers do not choose how their invoice is enriched; the customer's processing rules apply. - Attachments are base64 only. The Customer API also accepts
{ document_token, type }references to pre-uploaded files. The partner endpoint rejects that shape with a 400, since a supplier cannot reference an upload they did not perform. - Unknown routing is a soft fallback, not a hard error. If your
receiving_inboxorexternal_identifierdoes not match anything, Nuntiq routes to the customer's default inbox, sets aWarningheader, and returns 200. Invoice loss is worse than mis-routing.
Request body
The body has up to four top-level keys. Only invoice is required.
{
"receiving_inbox": "invoices@acme.apreceiving.com",
"external_identifier": "ACME-001",
"attachments": [ /* base64 attachments, see below */ ],
"invoice": {
"header": { /* invoice-level fields */ },
"lines": [ /* line items */ ],
"addresses": [ /* REMITTO, SUPPLIER, SHIPTO, BILLTO */ ],
"taxes": [ /* tax lines */ ],
"charges": [ /* freight, handling, etc. */ ],
"discounts": [ /* early-payment discounts */ ]
}
}
| Top-level key | Type | Required | Notes |
|---|---|---|---|
receiving_inbox | string | no | Direct routing target. See Routing. |
external_identifier | string | no | Alias for an inbox. Used when receiving_inbox is missing or unknown. |
attachments | array | no | Inline base64 only. See Attachments. |
invoice | object | yes | The invoice itself. invoice.header is required in practice; the others are optional. |
Invoice header fields
The header is where most of your data lives. Below is the complete field map; anything not in this list is silently dropped.
Identification
| Field | Type | Notes |
|---|---|---|
invoice_number | string | Your supplier-side invoice id. |
document_type | string | INVOICE (default), CREDIT_NOTE, etc. Optional. |
original_invoice_number | string | For credit notes, the invoice being credited. |
Dates (ISO 8601 YYYY-MM-DD)
| Field | Notes |
|---|---|
invoice_date | Issue date. |
due_date | Payment due date. |
delivery_date | Goods or service delivery date. |
Bad dates fail with 400 and a message naming the field.
References
| Field | Notes |
|---|---|
order_number_1 | Purchase order number. The "primary PO" field. |
order_number_2 | Secondary PO reference. |
contract_number | Master contract id. |
delivery_note | Delivery / packing slip number. |
reference_number | General-purpose reference. |
account_number | Supplier-side account number. |
Amounts
| Field | Type | Notes |
|---|---|---|
net_amount | decimal | Subtotal before tax. |
tax_amount | decimal | Total tax. |
gross_amount | decimal | Net + tax. |
currency_code | string | ISO 4217 (USD, EUR, GBP, ...). |
Supplier (you)
| Field | Notes |
|---|---|
supplier_name | Display name. |
supplier_tax_id | VAT / EIN / equivalent. |
supplier_contact | Free-text contact info. |
Customer (the bill-to)
| Field | Notes |
|---|---|
customer_name | |
customer_tax_id | |
customer_contact |
Payment
| Field | Notes |
|---|---|
bank_account | Your bank account / IBAN. |
bank_name | |
payment_method | WIRE, ACH, CHECK, CARD, free text. |
payment_terms | Net 30, 2/10 Net 30, etc. |
Custom fields
The customer can map these to ERP fields on their side. Use whichever slots they ask for; unused slots can be omitted.
| Family | Slots | Type | Notes |
|---|---|---|---|
custom_text_1..25 | 25 | string | Free text. |
custom_number_1..15 | 15 | number | Numeric, no formatting. |
custom_date_1..15 | 15 | string (YYYY-MM-DD) | ISO date only. |
ext_reference_1..5 | 5 | string | External system reference ids. |
identifier_field_1..5 | 5 | string | Generic identifier slots. |
Invoice lines
Each line is one row in invoice.lines. The recognized fields are:
| Field | Notes |
|---|---|
invoice_line_number | 1-based row index. |
product_code_1 | Primary SKU / item code. |
product_name | Display name. |
product_code_2 | Secondary code (e.g. supplier internal). |
quantity | Decimal. |
unit_price | Decimal. |
unit_of_measure | EA, HR, KG, etc. |
net_amount | Line subtotal before tax. |
tax_amount | Line tax. |
tax_rate | Percent (e.g. 10.00). |
gross_amount | net_amount + tax_amount. |
order_number_1 | Per-line PO reference. |
order_number_2 | Secondary PO reference. |
delivery_date | ISO 8601. |
delivery_note |
Custom fields available at line level: custom_text_1..15,
custom_number_1..10, custom_date_1..10.
Invoice addresses
Each address row needs address_type. Allowed values:
REMITTO (pay-to), SUPPLIER (you), SHIPTO (delivery), BILLTO (the
customer's billing party).
| Field | Notes |
|---|---|
address_type | One of the four above. Required. |
address_name | Display name (e.g. "Headquarters"). |
street | Street and number. |
city | |
state | |
postal_code | |
country | ISO 3166-1 alpha-2 or alpha-3 preferred. |
Unknown address_type values fail with a 400.
Taxes, charges, discounts
"taxes": [
{ "tax_name": "Sales Tax", "tax_rate": 10.0, "tax_base_amount": 1000.00, "tax_amount": 100.00, "tax_description": "TX state" }
],
"charges": [
{ "charge_name": "Shipping", "charge_amount": 15.00 }
],
"discounts": [
{ "discount_description": "2/10 Net 30", "discount_rate": 2.0, "discount_amount": 20.00, "discount_due_date": "2024-01-25" }
]
- Taxes:
tax_name,tax_rate,tax_base_amount,tax_amount,tax_description. - Charges:
charge_name,charge_amount. - Discounts:
discount_description,discount_rate,discount_amount,discount_due_date(ISO 8601).
Attachments
attachments is an array of base64-encoded files. Each entry has:
| Field | Required | Notes |
|---|---|---|
type | yes | IMAGE or ATTACHMENT. Exactly one IMAGE per request. |
filename | yes | Display name with extension. No path separators. |
content | yes | Base64-encoded body. Data-URL prefix (data:application/pdf;base64,...) is tolerated. |
content_type | no | MIME type. If supplied, must match the sniffed type, or the request fails. |
Limits
- 10 MB decoded per file. Larger files are rejected.
- Encrypted PDFs are rejected.
- PDFs containing JavaScript actions are rejected.
- Max one
IMAGEper request. TheIMAGEis the human-display PDF that the AP team will see in the portal. Anything else (delivery notes, supporting docs, contracts) goes in asATTACHMENT. - No
document_tokenshape. Reserved for the Customer API.
What if you don't send an IMAGE?
Nuntiq generates a display PDF from the customer's configured invoice template, so the AP team always has something to look at. This happens automatically; nothing in the response signals it, and no action is needed on your side.
Attachment validation errors
A failed attachment returns 400 with the failing index pinned:
{
"error": "Attachment validation failed",
"message": "attachments[2].content exceeds 10 MB decoded",
"attachment_index": 2
}
The base64-only check uses a different error envelope (same status, same
attachment_index):
{
"error": "Validation failed",
"message": "attachments[0].document_token is not allowed on this endpoint; provide { filename, content (base64), type } instead",
"attachment_index": 0
}
Routing
Most customers have multiple receiving inboxes (per business unit, per entity, per workflow). Nuntiq resolves the target inbox in this exact order, top to bottom, first match wins:
receiving_inboxmatches one of the customer's inboxes (case-insensitive, exact-string match against the configured prod / test / dev addresses for this environment).external_identifiermatches an alias configured against a specific inbox in the admin portal (under each inbox's External Identifiers list, case-insensitive, scoped per customer).- Customer default inbox (configured once per customer).
Resolution happens server-side. The response body does not tell you which
step matched — watch the Warning header instead, as described below.
Soft fallback
If you provided receiving_inbox or external_identifier and neither
matched, but the customer has a default inbox, Nuntiq still accepts
the invoice and routes it to the default. You get a Warning header:
HTTP/1.1 200 OK
Warning: 199 nuntiq "unknown receiving_inbox 'invoices@old.example.com', used default"
The Warning header is your signal that "we accepted this but you should
fix your config." You do not need to retry — the invoice is already
in the customer's queue. Log the header and correct the routing value in
your next submission.
If neither value is provided and no default is configured, you get a
400 with error: "Configuration error".
If neither value is provided but a default IS configured, the
invoice routes to the default and no Warning header is set (since
you never asked for a specific target).
Idempotency
Send Idempotency-Key: <your-unique-id> on every submission. Within a
24-hour window, repeated requests with the same key return the
original result, including the original invoice_token. No
duplicate invoice is created.
- Namespace is per API key (specifically, per
api_user_idresolved fromX-API-Key). Two partners using the same string do not collide; the same partner using the same string twice gets the original result back. - Max length 255 characters.
- Keep the key stable across retries of the same logical submission. Do not generate a new UUID on each retry attempt.
A replay returns the same shape with an extra flag:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"idempotent": true,
"data": {
"invoice_token": "<the original token>"
}
}
idempotent: true is the marker that this was a replay. Use it to
detect "my retry succeeded, the original call also succeeded."
Successful response
{
"success": true,
"data": {
"invoice_token": "750e8400-e29b-41d4-a716-446655440002",
"external_identifier": "ACME-001"
}
}
Fields
| Field | Use it for |
|---|---|
invoice_token | UUID handle for every downstream call. Stable forever, save this. |
external_identifier | Echoes back the external_identifier you sent, or null if you sent none. Useful for tying the response to the submission in your own logs. |
The response is deliberately small. How Nuntiq processed your invoice
internally — record ids, status codes, which template was applied, which
inbox it landed in — is the customer's operational detail, not yours. If
you need to reference a submission when contacting the customer or Nuntiq
support, quote the invoice_token.
What happens after the 200
The 200 means Nuntiq has accepted, persisted, and queued the invoice. What runs next depends on the workflow:
full_enrichment(the partner default): the invoice goes through PDF capture (OCR + AI extraction against yourIMAGE), enrichment against supplier and PO data, then validation rules. Total processing time is typically a few minutes. See Checking invoice status for following it afterwards.
You receive nothing else from this endpoint after the 200. Status changes are queryable separately.
Error responses
All 4xx error bodies are JSON with at minimum a message. Many also
include an error short code and context fields.
| Status | When | Body shape |
|---|---|---|
400 | Bad payload, missing field, attachment shape error, base64 decode error, encrypted PDF, JS-PDF, oversized file, unknown address_type, bad date, neither routing field matched and no customer default. | { "error": "...", "message": "...", "attachment_index"?: N } |
401 | Missing X-API-Key, unknown key, expired key (see below). | { "error": "...", "code"?: "key_expired", "message": "...", "regenerate_url"?: "..." } |
403 | Your key lacks SUBMIT_SUPPLIER_INVOICE. Every partner key carries this permission, so this almost only happens after the customer has revoked the key. | { "message": "..." } |
429 | Rate limit. The customer-API instance applies a global 300-request-per-minute IP limit; this is shared across all routes, not per-key. Back off and retry. | { "message": "Too many requests, please try again later." } |
500 | Server-side. Retry with the same Idempotency-Key after a back-off. | { "message": "..." } |
Expired key (401 key_expired)
When your key is past its expiry date the response is a structured 401:
{
"error": "Unauthorized",
"code": "key_expired",
"message": "This API key expired on 2026-05-19. Generate a new key at https://acme.apquery.com/regen?token=...",
"regenerate_url": "https://acme.apquery.com/regen?token=..."
}
Send a recipient through the regenerate_url to mint a replacement.
See Expired-key error for the full handling
guide, including how to avoid hitting it (rotate ahead of time, watch
the rotation reminder emails).
Checking invoice status
The Supplier API does not expose invoice status today. To follow an invoice after submission, use the Customer API with customer-issued credentials — lifecycle messages are queryable there.
Keep the invoice_token from your submission; it is the handle any
status lookup will need.