Download OpenAPI specification:
DRAFT — not the final 1.0 contract. This document is circulated for review. Paths, schemas, field names and status codes may still change, without notice and without a version bump, until it is published as 1.0.0. Build against it to give feedback, not to ship.
Allows a third party to use Archiles as a Peppol Access Point entirely over an API, with no Archiles UI interaction: onboarding, registry checks, pre-send validation, sending (Peppol BIS or other agreed formats, incl. attachments), sent-status tracking, and pull-based retrieval of received e-invoices with optional PDF visualisation.
Two surfaces over one core:
/v1/sapi/* — SAPI (sapi-sk.sk) compatibility profile, frozen to their schema./v1/peppol/* and /v1/e-invoices/* — Archiles native: onboarding and SMP under
/v1/peppol/*; validation, sending, sent-status and the inbox under /v1/e-invoices/*.The /v1/sapi/* half is verified field-by-field against the published SAPI-SK v1.0.0 schema.
Three deliberate, documented deviations, none of which can break a conforming client:
documentId, because SAPI's client-assigned
reading is not unique across senders — two senders can use the same invoice number towards
one receiver, which would make the receive path ambiguous;metadata.creationDateTime is treated as a submission timestamp and is excluded from the
payload-mismatch check, because UBL carries no equivalent field. It is recorded, not
compared.Three further differences are matters of declaration rather than behaviour, and none of them can break a conforming client:
429 and 500 are documented on the SAPI operations. SAPI's schema omits them, but they are
transport realities of any HTTP API and a client already handles both.402 is documented on POST /v1/sapi/document/send — an account with no active subscription
or an exhausted document allowance cannot send on either surface, and saying so is better than
answering with something vaguer.TokenResponse includes refresh_token. SAPI's schema omits it while its own /auth/renew
requires one, so a strictly conforming response could never supply the token the client is
expected to present. It is returned on both surfaces./v1/sapi/auth/* and /v1/oauth/* are ALIASES of one another: same handlers, same token store.
A token obtained from either works on both surfaces.
This is the ONLY authentication contract: client_credentials in, a bearer JWT out. Every
call made with the resulting token acts on behalf of the account owner, with admin rights on
the account. How tokens are issued internally is an implementation detail and is deliberately
not part of this contract.
Errors use SAPI's envelope on BOTH surfaces ({"error": {...}} with category, code,
message, retryable, correlation_id). RFC 7807 problem+json is NOT used.
Roadmap: optional webhooks/callbacks are planned as an alternative to polling — one
notifying a client of outbound send-status changes, one notifying a client of a newly received
e-invoice. Until then, both are polled (GET /v1/e-invoices/submissions*, GET /v1/e-invoices/inbox*).
Conventions that hold everywhere in this contract
[] when it has
no members — never omitted, never null. This covers documentTypes, errors, warnings,
items, results and attachments, each of which is declared required so a generated client
types it as guaranteed. The one exception is error.details, which is absent when there is
nothing to say about individual fields.null as the same thing./v1/peppol/*, /v1/e-invoices/*) always returns nextCursor, including on the
last page; a client stops when a page comes back with an empty items array. The SAPI surface
(/v1/sapi/*) omits nextPageToken entirely on the last page, as SAPI specifies. A client
using both surfaces must implement both stop conditions — they are deliberately different
because the SAPI half is frozen to its published schema.X-Correlation-Id header, and error bodies repeat it
as error.correlation_id. Quote it in any support request. The token routes (/v1/oauth/*,
/v1/sapi/auth/*) are the exception and return no correlation id; for a problem there, quote
your client_id and the time of the call instead.X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset; the token routes do not. The SAPI document operations return both sets of
headers as well, but deliberately do not declare them, because that half of the contract is
frozen to SAPI's published schema.503 during a deployment or an
infrastructure fault, whether or not 503 is listed on it. Treat it as category: TEMPORARY and
honour Retry-After.documentId always means the Archiles document id, on both surfaces and in both
directions — a received document has the same id on /v1/e-invoices/inbox/* and on
/v1/sapi/document/receive/*. submissionId identifies one submission of an outgoing document
and is what the submission and retry routes address. It is issued when the submission is accepted,
which can be before the document itself exists — so a batch item that fails later carries a
submissionId and no documentId. A retry keeps both; a corrected resend is a new submission
and a new document. SAPI's providerDocumentId on send is the submissionId, so a SAPI
client can poll GET /v1/e-invoices/submissions/{submissionId} with the value it was handed. The single
exception is SAPI's metadata.documentId on send, which SAPI defines as the client's own
invoice number.Error handling. Branch on the HTTP status and on error.retryable — never on error.code.
Codes are stable strings for support and logging, and they are meant to be shown to the user
together with error.message; the set is open and grows without notice, so no client should
switch on it. In particular 402 is a single condition — "this account cannot send more documents
right now" — and does not distinguish a subscription that never existed from one that expired or
exhausted its document allowance: the remedy is the same, extend the subscription.
Retention. Nothing is purged automatically. A received document, its payload and its
attachments are kept for as long as the account keeps them, and a 404 on a document id means it
was deleted by the account's own users, not aged out. The one time-bounded record is
Idempotency-Key state (24 hours), which is a transport artefact, not a document.
Rate limits are per API client, not per participant. An integrator that fronts many
participants therefore shares one budget across all of them and has to pace its own work; see
X-RateLimit-* on every response.
Companion narrative documentation: integration-guide.md. Version history and change policy:
CHANGELOG.md.
Alias of POST /v1/sapi/auth/token — same handler, same token store, interchangeable tokens.
Access tokens are valid for 15 minutes (expires_in: 900), matching SAPI.
The issued token identifies the account owner user, with admin rights on the account:
every subsequent call acts on their behalf, and Archiles' authorization checks and audit
trail record them as the actor. Scoping is therefore per participant
(X-Peppol-Participant-Id + your entitlements), not per operation.
| grant_type required | string Value: "client_credentials" |
| client_id required | string |
| client_secret required | string |
{- "grant_type": "client_credentials",
- "client_id": "string",
- "client_secret": "string"
}{- "access_token": "string",
- "refresh_token": "string",
- "token_type": "Bearer",
- "expires_in": 900,
- "scope": "document:send document:receive"
}{- "valid": true,
- "token_type": "access",
- "client_id": "string",
- "issued_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "expires_in_seconds": 0,
- "should_refresh": true,
- "refresh_recommended_at": "2019-08-24T14:15:22Z"
}| refresh_token required | string |
{- "refresh_token": "string"
}{- "access_token": "string",
- "refresh_token": "string",
- "token_type": "Bearer",
- "expires_in": 900,
- "scope": "document:send document:receive"
}No client authentication, by decision. SAPI's /auth/revoke is unauthenticated and takes
a bare token, and Archiles keeps that shape on both the SAPI route and this alias rather
than diverging on one of them: possession of the refresh token is the only check. The exposure
is bounded — the only thing an attacker who already holds your refresh token can do here is
invalidate it, which is what you would want to happen anyway — and revocation is idempotent,
so a replayed call changes nothing.
| token required | string |
| token_type_hint | string Default: "refresh_token" Value: "refresh_token" |
{- "token": "string",
- "token_type_hint": "refresh_token"
}{- "error": {
- "category": "VALIDATION",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload"
}
], - "retryable": false,
- "correlation_id": "550e8400-e29b-41d4-a716-446655440000"
}
}Creates a new Archiles account for the given DIČ, ready to be used as a Peppol participant.
Deliberately takes no X-Peppol-Participant-Id: the participant does not exist yet, so
there is no participant context to send. Entitlement to create is a property of the
credential itself.
The SMP registration itself is NOT performed by this call — it continues to arrive
inbound via the PDS webhook, out of band. A newly created participant can neither send nor
receive until registrationState becomes Active, which happens once that webhook
confirms SMP registration. Poll GET /v1/peppol/participants/{participantId} for readiness.
| taxNumber required | string DIČ |
| vatNumber | string IČ DPH, when the subject is a VAT payer. |
| legalName required | string |
| companyNumber | string IČO |
string <email> | |
| phone | string |
object |
{- "taxNumber": "string",
- "vatNumber": "string",
- "legalName": "string",
- "companyNumber": "string",
- "email": "user@example.com",
- "phone": "string",
- "address": {
- "street": "string",
- "city": "string",
- "postalCode": "string",
- "countryCode": "SK"
}
}{- "participantId": "0245:2022182030",
- "accountId": "string",
- "taxNumber": "string",
- "legalName": "string",
- "registrationState": "Pending",
- "createdDate": "2019-08-24T14:15:22Z"
}Deliberately takes no X-Peppol-Participant-Id — it is the operation that tells a client
which participants exist for it. Returns every participant the credential is entitled to,
so there is nothing to scope and no 403 to emit.
{- "items": [
- {
- "participantId": "0245:2022182030",
- "accountId": "string",
- "taxNumber": "string",
- "legalName": "string",
- "registrationState": "Pending",
- "createdDate": "2019-08-24T14:15:22Z"
}
]
}The participantId in the path is the tenancy key for this call, so no
X-Peppol-Participant-Id header is sent. A participant the credential is not entitled to
returns 403 whether or not it exists.
| participantId required | string Example: 0245:2022182030 |
{- "participantId": "0245:2022182030",
- "accountId": "string",
- "taxNumber": "string",
- "legalName": "string",
- "registrationState": "Pending",
- "createdDate": "2019-08-24T14:15:22Z"
}Returns the participant's registration flag and the fully qualified Peppol document type identifiers it advertises in the SMP. No request parameter selects document types — whatever the SMP publishes is returned in full.
Not entitlement-scoped, and therefore no X-Peppol-Participant-Id and no 403. SMP
registration data is public Peppol directory information: any authenticated client may look
up any participant, including ones it does not manage. That is deliberate — the normal use
is checking a trading partner before invoicing them.
When the SMP lookup cannot be performed (misconfiguration, network, SML/SMP failure) the
response is 503 TEMPORARY rather than a third boolean state. Archiles itself remains
fail-closed on send.
| participantId required | string Example: 0245:2022182030 |
{- "participantId": "string",
- "registered": true,
- "documentTypes": [
- "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
- "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"
]
}Per-item errors are reported inside the result array, so a single flaky lookup does not
fail an entire multi-thousand-partner sweep. For the same reason there is no
whole-request 503: an SMP failure surfaces as an error on the affected entries, with the
remaining entries answered normally. Retry only the failed participants.
Like the single registry check, this is public SMP data: not entitlement-scoped, no
X-Peppol-Participant-Id, no 403.
| participantIds required | Array of strings <= 1000 items |
{- "participantIds": [
- "0245:2022182030"
]
}{- "results": [
- {
- "participantId": "string",
- "registered": true,
- "documentTypes": [
- "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
- "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"
]
}
]
}Runs the identical chain the send path runs — XSD, CEN/Peppol Schematron, SK national
rules, and an Archiles mapping dry-run — with zero side effects. No Document is
created and nothing is enqueued.
The mapping dry-run is included because successful mapping into the Archiles archive model is a hard precondition for sending; without it a client could not predict a send-time rejection.
Why this side-effect-free operation still requires X-Peppol-Participant-Id: validation
is performed as a participant, not in the abstract. The header selects the account whose
settings shape the outcome — the validation-strictness setting below, the EAS scheme used to
build participant identifiers from the payload, and the default currency/country applied
when the payload omits them — and it is the value the entitlement check runs against, so
metering and rate limiting attribute the call correctly. A client therefore cannot validate
before it has a participant; create one first.
clientReference is your own identifier for the document being validated. It is not used
by the validation logic itself — it is only echoed back in ValidationResult so you can
match a result to the document you sent, which matters most for
POST /v1/e-invoices/validate/batch, where results are correlated by this field rather than
by array position.
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
| clientReference | string <= 128 characters Your own identifier for the document being validated, echoed back in |
| format | string (DocumentFormat) Default: "peppol-bis" Value: "peppol-bis" Document format, on send and on retrieval alike — one shared enum, and v1 supports
On send the payload is transmitted byte-for-byte, never re-serialised. On retrieval
|
| payload required | string Base64-encoded document. |
{- "clientReference": "ERP-2026-000123",
- "format": "peppol-bis",
- "payload": "string"
}{- "valid": true,
- "clientReference": "string",
- "errors": [
- {
- "source": "Xsd",
- "rule": "BR-CO-10",
- "location": "string",
- "message": "string"
}
], - "warnings": [
- {
- "source": "Xsd",
- "rule": "BR-CO-10",
- "location": "string",
- "message": "string"
}
]
}Each item's clientReference is your own identifier for that document, and results carry it
back so a result can be matched to its source. Results are returned in request order, so
position works as a fallback when an item has no clientReference — but a reference is the
sturdier key and is recommended for every item.
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
required | Array of objects (ValidationRequest) <= 500 items |
{- "items": [
- {
- "clientReference": "ERP-2026-000123",
- "format": "peppol-bis",
- "payload": "string"
}
]
}{- "results": [
- {
- "valid": true,
- "clientReference": "string",
- "errors": [
- {
- "source": "Xsd",
- "rule": "BR-CO-10",
- "location": "string",
- "message": "string"
}
], - "warnings": [
- {
- "source": "Xsd",
- "rule": "BR-CO-10",
- "location": "string",
- "message": "string"
}
]
}
]
}Validation, mapping into the Archiles archive model, and enqueueing all happen
synchronously; network delivery is asynchronous. 202 therefore means technical
acceptance, not delivery.
When format is peppol-bis, the supplied payload is transmitted byte-for-byte —
it is never re-serialised through the Archiles exporter.
Mapping into the Archiles archive model is a hard precondition: a structural mapping failure
rejects the submission with 400 VALIDATION. Unmappable optional fields are accepted and
recorded as warnings on the archived document.
In the application/json body, payload must be base64-encoded (see
DocumentEnvelope); the multipart/form-data alternative below takes the raw binary
payload directly.
Attachments must already be embedded in the UBL you submit, as
AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject. There is no
separate attachments field on send: the payload is transmitted byte-for-byte, so Archiles
cannot add anything to it without rewriting a document it promised not to touch. (Inbound is
the other direction and does expose attachments as sub-resources — see
GET /v1/e-invoices/inbox/{documentId}/attachments/{attachmentId}.)
X-Peppol-Participant-Id must match the payload. The supplier endpoint derived from the
UBL (cac:AccountingSupplierParty, built via the account's EAS scheme) has to be the
participant in the header; a disagreement is a 400 VALIDATION and is not retryable without
changing one of the two. Sending participant A's invoice in participant B's context is a client
bug, and refusing it here is cheaper than discovering it after transmission. The same rule holds
on POST /v1/sapi/document/send, which additionally compares the whole declared metadata
block against the payload.
Planned: account-level validation strictness setting. An upcoming account setting will
control how strictly submitted documents are checked before sending: strict (the planned
default) runs the full chain, including CEN/Peppol Schematron, identical to
POST /v1/e-invoices/validate; a basic mode will check only well-formed UBL structure. The
relaxed mode is intended for accounts that also work through the Archiles UI, where a human
can add or correct data before the document is actually transmitted. This setting is
account-wide and will be configured outside this API.
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
| Idempotency-Key | string <uuid> Retained 24 hours, scoped per API client. The same key with the same payload returns the original response verbatim; the same key with a different payload is a 409. Optional on the native surface, but strongly recommended on every send. On a batch, the key covers the whole batch. Replaying it returns the original 202, so it
cannot be used to re-drive part of a batch. To re-drive failed work: items that were
admitted have a |
| clientReference | string <= 128 characters The caller's own identifier, echoed in every response and queryable via
|
| format | string (DocumentFormat) Default: "peppol-bis" Value: "peppol-bis" Document format, on send and on retrieval alike — one shared enum, and v1 supports
On send the payload is transmitted byte-for-byte, never re-serialised. On retrieval
|
| emailDelivery | string Default: "none" Enum: "none" "fallback" "only" "always" Reserved. v1 accepts When the email channel ships: |
object Recipients for the email channel. Reserved — supplying it in v1 is a | |
| payload required | string Base64-encoded document. (The |
{- "clientReference": "ERP-2026-000123",
- "format": "peppol-bis",
- "emailDelivery": "none",
- "email": {
- "to": [
- "user@example.com"
], - "cc": [
- "user@example.com"
], - "subject": "string"
}, - "payload": "string"
}{- "clientReference": "string",
- "submissionId": "string",
- "documentId": "string",
- "conversationId": "string",
- "status": "Queued"
}Lists submissions for the participant in X-Peppol-Participant-Id only; there is no
cross-participant listing.
Polling is currently the only way to observe status changes. An optional webhook/callback for send-status changes is planned — see the top-level API description.
| since | string Opaque cursor from a previous page, encoding So this list is at-least-once: the same |
| changedFrom | string <date-time> Return only submissions whose state last changed at or after this instant. Bounds a first
sweep the same way |
| status | string (SubmissionState) Enum: "Queued" "Sent" "Delivered" "Rejected" "Undeliverable" "DeliveryUnconfirmed" "Failed" Filter by current status. The filter and the |
| clientReference | string Filter by the caller's own reference. |
| limit | integer [ 1 .. 500 ] Default: 100 |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "items": [
- {
- "submissionId": "string",
- "clientReference": "string",
- "documentId": "string",
- "conversationId": "string",
- "cursor": "string",
- "status": "Queued",
- "failureReason": "string",
- "senderParticipantId": "string",
- "receiverParticipantId": "string",
- "documentType": "invoice",
- "submittedAt": "2019-08-24T14:15:22Z",
- "lastStateChangeAt": "2019-08-24T14:15:22Z",
- "emailDelivery": {
- "status": "NotApplicable",
- "message": "string"
}, - "taxReporting": {
- "state": "NotApplicable",
- "message": "string"
}
}
], - "nextCursor": "string"
}Only cheap checks run synchronously: entitlement, well-formedness, and size/count limits.
Schematron validation, mapping and publishing run asynchronously per item; retrieve
outcomes from GET /v1/e-invoices/submissions/batch/{batchId}.
Results are returned in request order and echo each item's clientReference. Correlate by the
reference where you supplied one, by position otherwise.
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
| Idempotency-Key | string <uuid> Retained 24 hours, scoped per API client. The same key with the same payload returns the original response verbatim; the same key with a different payload is a 409. Optional on the native surface, but strongly recommended on every send. On a batch, the key covers the whole batch. Replaying it returns the original 202, so it
cannot be used to re-drive part of a batch. To re-drive failed work: items that were
admitted have a |
required | Array of objects (DocumentEnvelope) <= 500 items Also bounded by a total request size of 100 MB. |
{- "items": [
- {
- "clientReference": "ERP-2026-000123",
- "format": "peppol-bis",
- "emailDelivery": "none",
- "email": {
- "to": [
- "user@example.com"
], - "cc": [
- "user@example.com"
], - "subject": "string"
}, - "payload": "string"
}
]
}{- "batchId": "string",
- "accepted": 0,
- "rejected": 0,
- "items": [
- {
- "clientReference": "string",
- "submissionId": "string",
- "error": {
- "category": "AUTH",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload",
- "value": "string"
}
], - "retryable": true,
- "correlation_id": "807686c4-116c-44b3-a01c-b14b50e31bcc"
}
}
]
}The batch must belong to the participant in X-Peppol-Participant-Id; a batch id belonging
to another participant returns 404, not 403, so ids cannot be probed across tenants.
| batchId required | string |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "batchId": "string",
- "state": "Processing",
- "items": [
- {
- "clientReference": "string",
- "submissionId": "string",
- "status": "Queued",
- "error": {
- "category": "AUTH",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload",
- "value": "string"
}
], - "retryable": true,
- "correlation_id": "807686c4-116c-44b3-a01c-b14b50e31bcc"
}
}
]
}The submission must belong to the participant in X-Peppol-Participant-Id; a submission id
belonging to another participant returns 404, not 403.
| submissionId required | string |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "submissionId": "string",
- "clientReference": "string",
- "documentId": "string",
- "conversationId": "string",
- "cursor": "string",
- "status": "Queued",
- "failureReason": "string",
- "senderParticipantId": "string",
- "receiverParticipantId": "string",
- "documentType": "invoice",
- "submittedAt": "2019-08-24T14:15:22Z",
- "lastStateChangeAt": "2019-08-24T14:15:22Z",
- "emailDelivery": {
- "status": "NotApplicable",
- "message": "string"
}, - "taxReporting": {
- "state": "NotApplicable",
- "message": "string"
}
}Re-enqueues the payload already stored for this submission — the same bytes, the same
archived document, the same Peppol conversation and the same transmission identity. It is a
retransmission, not a new invoice, so it cannot duplicate the document in the archive; the
submission returns to status Queued and keeps its submissionId.
Allowed from DeliveryUnconfirmed, Undeliverable and Failed. Everything else is a
409:
| Current status | Result | Why |
|---|---|---|
DeliveryUnconfirmed |
202, back to Queued |
no MLS arrived; retransmitting is the documented remedy |
Undeliverable |
202, back to Queued |
retry once the cause is fixed — typically after the receiver has registered in the SMP for this document type. Retrying before that simply fails the same way |
Failed |
202, back to Queued |
our side failed; a retry is a genuine second attempt |
Queued, Sent |
409 | a transmission is already in flight |
Delivered |
409 | the document was delivered — a resend risks a duplicate invoice at the buyer |
Rejected |
409 | the recipient rejected the content; the same bytes will be rejected again. Send a corrected document as a new submission |
A retry is not rate-limit-free: it counts as one item, like a send.
| submissionId required | string |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
| Idempotency-Key | string <uuid> Retained 24 hours, scoped per API client. The same key with the same payload returns the original response verbatim; the same key with a different payload is a 409. Optional on the native surface, but strongly recommended on every send. On a batch, the key covers the whole batch. Replaying it returns the original 202, so it
cannot be used to re-drive part of a batch. To re-drive failed work: items that were
admitted have a |
{- "clientReference": "string",
- "submissionId": "string",
- "documentId": "string",
- "conversationId": "string",
- "status": "Queued"
}Bulk form of POST /v1/e-invoices/submissions/{submissionId}/retry, with the same per-status rules.
Per-item outcomes are reported in the response, so one non-retryable submission does not fail
the whole request — which is the point when re-driving a night's worth of
DeliveryUnconfirmed items.
This is also the natural way to re-drive the failed items of a batch: a batch item that was
admitted has a submissionId and belongs here. An item rejected at admission never got
one — nothing was created for it, so resubmit it as a new send or a new batch (see
BatchAccepted).
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
| Idempotency-Key | string <uuid> Retained 24 hours, scoped per API client. The same key with the same payload returns the original response verbatim; the same key with a different payload is a 409. Optional on the native surface, but strongly recommended on every send. On a batch, the key covers the whole batch. Replaying it returns the original 202, so it
cannot be used to re-drive part of a batch. To re-drive failed work: items that were
admitted have a |
| submissionIds required | Array of strings <= 500 items |
{- "submissionIds": [
- "string"
]
}{- "results": [
- {
- "submissionId": "string",
- "retried": true,
- "status": "Queued",
- "error": {
- "category": "AUTH",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload",
- "value": "string"
}
], - "retryable": true,
- "correlation_id": "807686c4-116c-44b3-a01c-b14b50e31bcc"
}
}
]
}Descriptors carry no payload, so polling for new mail is cheap.
since takes the nextCursor from the previous page. That value is an opaque encoding of
(receivedAt, documentId), and paging compares the pair — a document is returned when its
receivedAt is later than the cursor's, or equal to it with a greater document id. That is what
makes paging tie-safe when a bulk arrival stamps many documents with the same timestamp, and
because receivedAt never changes, each document is returned exactly once across a sweep.
Polling is currently the only way to learn about new mail. An optional webhook/callback notifying a client when a new e-invoice arrives is planned — see the top-level API description.
| since | string Opaque cursor from a previous page. Omit to start from the oldest document — on an account
with history that is a full replay, so bound the first call with |
| receivedFrom | string <date-time> Return only documents received at or after this instant. Intended for the first poll
of an account: pass "now" to start from empty, or a date to backfill a bounded window.
Ignored once |
| status | string Default: "New" Enum: "New" "Acknowledged" "All" Custody filter. The SAPI surface names the same two states |
| limit | integer [ 1 .. 500 ] Default: 100 |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "items": [
- {
- "id": "string",
- "cursor": "string",
- "receivedAt": "2019-08-24T14:15:22Z",
- "senderParticipantId": "string",
- "documentType": "invoice",
- "documentNumber": "string",
- "issueDate": "2019-08-24",
- "totalAmount": "1234.56",
- "currency": "EUR",
- "attachmentCount": 0,
- "status": "New",
- "pdfState": "Ready"
}
], - "nextCursor": "string"
}| documentId required | string The Archiles document identifier. One identifier space, both surfaces: the value returned
as |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "id": "string",
- "cursor": "string",
- "receivedAt": "2019-08-24T14:15:22Z",
- "senderParticipantId": "string",
- "documentType": "invoice",
- "documentNumber": "string",
- "issueDate": "2019-08-24",
- "totalAmount": "1234.56",
- "currency": "EUR",
- "attachmentCount": 0,
- "status": "New",
- "pdfState": "Ready",
- "supplier": {
- "name": "string",
- "taxNumber": "string",
- "vatNumber": "string",
- "countryCode": "SK"
}, - "customer": {
- "name": "string",
- "taxNumber": "string",
- "vatNumber": "string",
- "countryCode": "SK"
}, - "attachments": [
- {
- "id": "string",
- "filename": "string",
- "mimeType": "string",
- "sizeBytes": 0,
- "sha256": "string"
}
]
}Response shape is chosen by content negotiation:
Accept: application/json — metadata plus the document base64-encoded, PDF and
attachments inline or omitted.Accept: application/zip — a bundle containing document.xml (in the requested format),
document.pdf (when requested), attachments/NNN_<original filename> (collision-suffixed)
and manifest.json (ids, filenames, MIME types, SHA-256 hashes, source metadata).format=peppol-bis returns the original received bytes, and is the only format v1
supports (see DocumentFormat).
Inbound PDF generation is asynchronous. When pdfState is Pending, retry rather than
assuming no PDF exists.
| documentId required | string The Archiles document identifier. One identifier space, both surfaces: the value returned
as |
| format | string (DocumentFormat) Default: "peppol-bis" Value: "peppol-bis" Document format, on send and on retrieval alike — one shared enum, and v1 supports
On send the payload is transmitted byte-for-byte, never re-serialised. On retrieval
|
string Default: "none" Enum: "none" "include" "only" | |
| attachments | string Default: "none" Enum: "none" "inline" JSON responses only; ignored for |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "id": "string",
- "format": "peppol-bis",
- "document": "string",
- "pdf": "string",
- "pdfState": "Ready",
- "attachments": [
- {
- "id": "string",
- "filename": "string",
- "mimeType": "string",
- "sizeBytes": 0,
- "sha256": "string",
- "content": "string"
}
]
}| documentId required | string The Archiles document identifier. One identifier space, both surfaces: the value returned
as |
| attachmentId required | string |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "error": {
- "category": "VALIDATION",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload"
}
], - "retryable": false,
- "correlation_id": "550e8400-e29b-41d4-a716-446655440000"
}
}Custody transfer Archiles → client. Flips New to Acknowledged. Idempotent — a repeated
call succeeds and returns the original acknowledgedAt.
This is not the Peppol MLS (transport acknowledgement to the sending AP, sent
automatically by Archiles), and not a business accept/reject
(UBL ApplicationResponse, out of scope for v1).
| documentId required | string The Archiles document identifier. One identifier space, both surfaces: the value returned
as |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "id": "string",
- "acknowledged": true,
- "acknowledgedAt": "2019-08-24T14:15:22Z",
- "error": {
- "category": "AUTH",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload",
- "value": "string"
}
], - "retryable": true,
- "correlation_id": "807686c4-116c-44b3-a01c-b14b50e31bcc"
}
}| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
| ids required | Array of strings <= 1000 items |
{- "ids": [
- "string"
]
}{- "results": [
- {
- "id": "string",
- "acknowledged": true,
- "acknowledgedAt": "2019-08-24T14:15:22Z",
- "error": {
- "category": "AUTH",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload",
- "value": "string"
}
], - "retryable": true,
- "correlation_id": "807686c4-116c-44b3-a01c-b14b50e31bcc"
}
}
]
}Alias of POST /v1/oauth/token — same handler, same token store. A token obtained here is
valid on the native surface as well. Identical semantics to /v1/oauth/token, including acting
on behalf of the account owner with admin rights.
403 ("IP address not allowed for this client") is defined by SAPI but never emitted by
Archiles in phase 1 — per-client IP restriction is not implemented. It is listed here for
contract fidelity.
| client_id required | string |
| client_secret required | string |
| grant_type required | string Value: "client_credentials" |
| scope | string Optional, per SAPI. Accepted, but neither enforced nor echoed back in v1 — see
|
{- "client_id": "string",
- "client_secret": "string",
- "grant_type": "client_credentials",
- "scope": "document:send document:receive"
}{- "access_token": "string",
- "refresh_token": "string",
- "token_type": "Bearer",
- "expires_in": 900,
- "scope": "document:send document:receive"
}{- "valid": true,
- "token_type": "access",
- "client_id": "string",
- "issued_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "expires_in_seconds": 0,
- "should_refresh": true,
- "refresh_recommended_at": "2019-08-24T14:15:22Z"
}| refresh_token required | string |
{- "refresh_token": "string"
}{- "access_token": "string",
- "refresh_token": "string",
- "token_type": "Bearer",
- "expires_in": 900,
- "scope": "document:send document:receive"
}| token | string |
| token_type_hint | string Default: "refresh_token" Value: "refresh_token" |
{- "token": "string",
- "token_type_hint": "refresh_token"
}{- "error": {
- "category": "VALIDATION",
- "code": "SAPI-VAL-001",
- "message": "Document payload is invalid",
- "details": [
- {
- "field": "metadata.receiverParticipantId",
- "issue": "Does not match payload"
}
], - "retryable": false,
- "correlation_id": "550e8400-e29b-41d4-a716-446655440000"
}
}Delegates to the same application service as POST /v1/e-invoices/submissions.
202 ACCEPTED confirms technical receipt only. It does not imply semantic validation,
successful Peppol delivery, or legal effect.
metadata is required and is compared against the payload; any disagreement is rejected
with 400 VALIDATION. X-Peppol-Participant-Id must equal
metadata.senderParticipantId.
Archiles accepts payloads up to 20 MB (SAPI mandates 10 MB) — a documented superset.
Subject to the same planned account-level validation-strictness setting as
POST /v1/e-invoices/submissions (see there) — it is account-wide, not per-surface.
SAPI defines no sent-status endpoint, so within SAPI this 202 is the last thing a client
hears about the document. Archiles does not extend the SAPI surface to fill that gap (it stays
frozen to SAPI-SK v1.0.0) — it hands you the key to the native one instead:
providerDocumentId in the response is the submissionId, so
GET /v1/e-invoices/submissions/{providerDocumentId} returns the full delivery status, and the
token from /v1/sapi/auth/token works there unchanged. Store it against your own invoice number at send
time; nothing else in the SAPI response identifies the submission. A client that does not want
to leave the SAPI surface at all can instead treat 202 as technical receipt only and reconcile
out of band.
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
| Idempotency-Key required | string <uuid> Required on the SAPI surface, per SAPI v1.0. Same semantics as the native header. |
required | object (SapiDocumentMetadata) |
| payload required | string Raw UTF-8 UBL XML — not base64 (this differs from the native envelope, which is base64). Max 20 MB in Archiles; SAPI mandates 10 MB. |
| payloadFormat required | string Value: "XML" |
| payloadEncoding | string Defaults to UTF-8. |
| checksum | string^[a-f0-9]{64}$ Optional SHA-256 over the exact payload bytes, lowercase hex. When supplied it is verified; a mismatch is a 400 VALIDATION. |
{- "metadata": {
- "documentId": "INV-2026-0001",
- "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
- "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
- "senderParticipantId": "0245:1234567890",
- "receiverParticipantId": "0245:9876543210",
- "creationDateTime": "2019-08-24T14:15:22Z"
}, - "payload": "string",
- "payloadFormat": "XML",
- "payloadEncoding": "UTF-8",
- "checksum": "string"
}{- "providerDocumentId": "8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0",
- "status": "ACCEPTED",
- "receivedAt": "2019-08-24T14:15:22Z",
- "timestamp": "2019-08-24T14:15:22Z"
}Sorted oldest first. nextPageToken is absent when there are no more pages — this
differs deliberately from the native /v1/e-invoices/inbox, which always returns a cursor.
Note that SAPI list items carry neither status nor receivedAt, as SAPI specifies. To
see custody status or receipt time, use the status filter, or the native
GET /v1/e-invoices/inbox, which returns both.
| pageToken | string Opaque cursor from a previous response's |
| limit | integer [ 1 .. 100 ] Default: 20 |
| status | string Enum: "RECEIVED" "ACKNOWLEDGED" SAPI's custody vocabulary, and the same custody state the native surface calls |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "documents": [
- {
- "documentId": "INV-2026-0001",
- "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
- "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
- "senderParticipantId": "0245:1234567890",
- "receiverParticipantId": "0245:9876543210",
- "creationDateTime": "2019-08-24T14:15:22Z"
}
], - "nextPageToken": "string"
}documentId is the Archiles-assigned identifier returned in the listing, not the
sender's invoice number. The payload is returned exactly as received
from the Peppol network, unaltered.
| documentId required | string The Archiles document identifier. One identifier space, both surfaces: the value returned
as |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "metadata": {
- "documentId": "INV-2026-0001",
- "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
- "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
- "senderParticipantId": "0245:1234567890",
- "receiverParticipantId": "0245:9876543210",
- "creationDateTime": "2019-08-24T14:15:22Z"
}, - "payload": "string",
- "payloadFormat": "XML"
}Confirms handover to the software endpoint. Idempotent — re-acknowledging returns success
without changing acknowledgedDateTime.
| documentId required | string The Archiles document identifier. One identifier space, both surfaces: the value returned
as |
| X-Peppol-Participant-Id required | string Example: 0245:2022182030 The participant this request acts for. Resolved to an Archiles account and checked against the API client's entitlements before any other processing — which is also why it is required rather than derived from the payload: an unentitled call is refused before anything is parsed. A participant outside your entitlements returns 403 without revealing whether it exists. On send it must also agree with the document. The supplier endpoint derived from the payload
has to be this participant, and on the SAPI surface it must equal |
{- "documentId": "string",
- "status": "ACKNOWLEDGED",
- "acknowledgedDateTime": "2019-08-24T14:15:22Z"
}