An Idempotency-Key protects a request. It does not identify a document,
so the same invoice submitted under two different keys was two different
requests — and on POST /v1/invoices/import, two real deliveries. Peppol does
not de-duplicate, so nothing downstream would have collapsed them: a second copy
lands in the recipient's ledger as an ordinary invoice.
POST /v1/invoices has looked at the document itself since 19 August. The
import route did not. It does now, and both routes run the same check before
the document reaches the provider.
A document is recognised by six things: account, environment, invoice number, credit-note flag, recipient, and sending legal entity.
- Inside 15 minutes —
409 duplicate_document, nothing is transmitted, and the response names the earlier document induplicateOf. Read that document's status rather than sending again. - Past 15 minutes — the document is sent, and the
201carriesduplicateOfwith the earlier document id.
The lookback stops at 30 days. Past that, a repeated invoice number is a question for BT-1 uniqueness, which this guard does not claim to enforce.
The contract said otherwise
ImportInvoiceResult already declared duplicateOf, inherited from the shared
send schema, on a route that never produced it. Anyone reading "no
duplicateOf, therefore no duplicate" was reading a field that could not
appear, so its absence meant nothing.
409 is now documented on /invoices/import in
the OpenAPI spec, and the field is
produced.
If you retry after a failure
A retry landing outside the 15-minute window is flagged, not refused. We cannot
tell a late retry from a deliberate resend — someone whose customer says the
invoice never arrived makes exactly the same call — so the document goes and the
201 says so. If your client retries on a long ceiling, duplicateOf is the
field to check.