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. The response
field pdf_will_be_generated: true confirms that's what happened.
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).
A successful resolution returns 200 with the path used:
"resolution_path": "explicit_inbox" | "external_identifier" | "default_fallback"
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:
HTTP/1.1 200 OK
Warning: 199 nuntiq "unknown receiving_inbox 'invoices@old.example.com', used default"
{
"success": true,
"data": {
"receiving_inbox": "default@acme.apreceiving.com",
"external_identifier": "OLD-ID",
"resolution_path": "default_fallback",
"receiving_inbox_fallback_used": true,
"...": "..."
}
}
The receiving_inbox_fallback_used: true flag tells you "we accepted
this but you should fix your config." You do not need to retry.
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 with resolution_path: "default_fallback"
and no Warning header (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": "01HX5W6Y3VEXAMPLE...",
"invoice_id": 94821,
"source_document_id": 30481,
"workflow": "full_enrichment",
"effective_workflow": "full_enrichment",
"invoice_status": 1,
"source_document_status": 1,
"template_id": 5,
"template_type": "global",
"attachments_linked": 1,
"pdf_will_be_generated": false,
"receiving_inbox": "invoices@acme.apreceiving.com",
"external_identifier": "ACME-001",
"resolution_path": "explicit_inbox",
"receiving_inbox_fallback_used": false
}
}
Fields you care about
| Field | Use it for |
|---|---|
invoice_token | UUID handle for every downstream call (status, events, attachments via the Customer API). Stable forever, save this. |
source_document_id | Numeric id useful for support tickets. |
invoice_id | Numeric internal id. Use invoice_token in your code; use this when Nuntiq support asks. |
effective_workflow | What Nuntiq actually ran. May be collapsed from full_enrichment to basic_enrichment if you sent no IMAGE attachment AND the customer has no image-capture rules. |
invoice_status | Initial status code. For full_enrichment this is 1 ("API enrich with PDF capture"). |
pdf_will_be_generated | true when Nuntiq is rendering a display PDF from the customer template (because you sent no IMAGE). |
receiving_inbox_fallback_used | true only when you specified a routing target that didn't match. See Routing. |
resolution_path | explicit_inbox, external_identifier, or default_fallback. |
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; you can poll with the Customer API once the reserved status endpoint lands, or look for lifecycle messages via the Customer API today.
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).
What's reserved but not yet implemented
Three GET endpoints under /v1/partner/invoices/{id}/... are
documented in the reference but return 501 Not Implemented today:
GET /v1/partner/invoices/{id}returns the full invoice when shipped.GET /v1/partner/invoices/{id}/statusreturns the current LCM + payment state, a cheap polling endpoint.GET /v1/partner/invoices/{id}/eventsreturns the chronological LCM event history, useful for an audit trail.
If you need any of these now, use the Customer API with customer-issued credentials. The URL shape is reserved so your client code can target it now and start working the day the endpoints land.