Archiles Peppol API (1.0.0-draft)

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:

  1. payload limit is 20 MB (SAPI's wording is "Maximum size: 10 MB", i.e. a ceiling — accepting more than a conforming client will ever send is a superset);
  2. for RECEIVED documents Archiles assigns 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;
  3. 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

  • Arrays are always present. Every array-typed response field is returned as [] 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.
  • Optional scalars are omitted, not nulled. A scalar with no value is absent from the JSON; clients must treat absent and null as the same thing.
  • Paging. The native surface (/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.
  • Correlation. Every response of every participant, registry, validation, submission and inbox operation — successful or not — carries an 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.
  • Rate-limit headers. The same operations carry 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.
  • Maintenance and infrastructure faults. Any operation may answer 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.
  • Identifiers. 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.

Auth

OAuth2 client_credentials, shared by both surfaces.

Exchange client credentials for an access token

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.

Request Body schema: application/json
required
grant_type
required
string
Value: "client_credentials"
client_id
required
string
client_secret
required
string

Responses

Request samples

Content type
application/json
{
  • "grant_type": "client_credentials",
  • "client_id": "string",
  • "client_secret": "string"
}

Response samples

Content type
application/json
{
  • "access_token": "string",
  • "refresh_token": "string",
  • "token_type": "Bearer",
  • "expires_in": 900,
  • "scope": "document:send document:receive"
}

Check access-token validity and expiry

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "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"
}

Issue a new token pair from a refresh token (with rotation)

Request Body schema: application/json
required
refresh_token
required
string

Responses

Request samples

Content type
application/json
{
  • "refresh_token": "string"
}

Response samples

Content type
application/json
{
  • "access_token": "string",
  • "refresh_token": "string",
  • "token_type": "Bearer",
  • "expires_in": 900,
  • "scope": "document:send document:receive"
}

Invalidate a refresh token

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.

Request Body schema: application/json
required
token
required
string
token_type_hint
string
Default: "refresh_token"
Value: "refresh_token"

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "token_type_hint": "refresh_token"
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

Participants

Onboarding and participant state (native only).

Create a new participant account for a DIČ

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
taxNumber
required
string

DIČ

vatNumber
string

IČ DPH, when the subject is a VAT payer.

legalName
required
string
companyNumber
string

IČO

email
string <email>
phone
string
object

Responses

Request samples

Content type
application/json
{
  • "taxNumber": "string",
  • "vatNumber": "string",
  • "legalName": "string",
  • "companyNumber": "string",
  • "email": "user@example.com",
  • "phone": "string",
  • "address": {
    }
}

Response samples

Content type
application/json
{
  • "participantId": "0245:2022182030",
  • "accountId": "string",
  • "taxNumber": "string",
  • "legalName": "string",
  • "registrationState": "Pending",
  • "createdDate": "2019-08-24T14:15:22Z"
}

List participants this API client is entitled to

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.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Get participant state, including SMP registration readiness

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.

Authorizations:
bearerAuth
path Parameters
participantId
required
string
Example: 0245:2022182030

Responses

Response samples

Content type
application/json
{
  • "participantId": "0245:2022182030",
  • "accountId": "string",
  • "taxNumber": "string",
  • "legalName": "string",
  • "registrationState": "Pending",
  • "createdDate": "2019-08-24T14:15:22Z"
}

Registry

Peppol/SMP registration and capability checks (native only).

Check whether a participant is registered in Peppol and what it can receive

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.

Authorizations:
bearerAuth
path Parameters
participantId
required
string
Example: 0245:2022182030

Responses

Response samples

Content type
application/json
{
  • "participantId": "string",
  • "registered": true,
  • "documentTypes": [
    ]
}

Bulk registry check

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.

Authorizations:
bearerAuth
Request Body schema: application/json
required
participantIds
required
Array of strings <= 1000 items

Responses

Request samples

Content type
application/json
{
  • "participantIds": [
    ]
}

Response samples

Content type
application/json
{
  • "results": [
    ]
}

Validation

Pre-send validation with no side effects (native only).

Validate a document without sending it

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.

Authorizations:
bearerAuth
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Request Body schema: application/json
required
clientReference
string <= 128 characters

Your own identifier for the document being validated, echoed back in ValidationResult so you can match a result to its source. Recommended on every item of POST /v1/e-invoices/validate/batch: results come back in request order, so position works without it, but a reference is what keeps correlation correct independently of ordering.

format
string (DocumentFormat)
Default: "peppol-bis"
Value: "peppol-bis"

Document format, on send and on retrieval alike — one shared enum, and v1 supports peppol-bis (Peppol BIS Billing 3.0 UBL) only. It is the default when omitted, so the field can be left out entirely.

On send the payload is transmitted byte-for-byte, never re-serialised. On retrieval peppol-bis returns the original received bytes.

peppol-bis is the only value v1 defines, in either direction. Values may be added in a later version; because a new value is additive it cannot break a client, which is why one shared enum is used rather than a split per direction.

payload
required
string

Base64-encoded document.

Responses

Request samples

Content type
application/json
{
  • "clientReference": "ERP-2026-000123",
  • "format": "peppol-bis",
  • "payload": "string"
}

Response samples

Content type
application/json
{
  • "valid": true,
  • "clientReference": "string",
  • "errors": [
    ],
  • "warnings": [
    ]
}

Validate many documents without sending them

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.

Authorizations:
bearerAuth
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Request Body schema: application/json
required
required
Array of objects (ValidationRequest) <= 500 items

Responses

Request samples

Content type
application/json
{
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "results": [
    ]
}

Submissions

Sending and sent-status (native).

Send a single e-invoice

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.

Authorizations:
bearerAuth
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

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 submissionId and belong to POST /v1/e-invoices/submissions/retry; items rejected at admission have no submissionId — nothing was created for them — so resubmit those as a new send or a new batch, under a new key.

Request Body schema:
required
clientReference
string <= 128 characters

The caller's own identifier, echoed in every response and queryable via GET /v1/e-invoices/submissions?clientReference=…. Indexed per participant, not enforced unique — Idempotency-Key remains the deduplication mechanism.

format
string (DocumentFormat)
Default: "peppol-bis"
Value: "peppol-bis"

Document format, on send and on retrieval alike — one shared enum, and v1 supports peppol-bis (Peppol BIS Billing 3.0 UBL) only. It is the default when omitted, so the field can be left out entirely.

On send the payload is transmitted byte-for-byte, never re-serialised. On retrieval peppol-bis returns the original received bytes.

peppol-bis is the only value v1 defines, in either direction. Values may be added in a later version; because a new value is additive it cannot break a client, which is why one shared enum is used rather than a split per direction.

emailDelivery
string
Default: "none"
Enum: "none" "fallback" "only" "always"

Reserved. v1 accepts none only; any other value is rejected with 400 VALIDATION, category: PERMANENT. Declared now so the delivery-channel axis is visible from the start and no client hard-codes its absence.

When the email channel ships: fallback sends over Peppol when the buyer is a registered participant and by email when it is not (the registry check Archiles performs anyway decides); only sends by email without attempting Peppol; always does both. The outcome of the email leg is reported in Submission.emailDelivery, separately from the Peppol status, and Delivered remains a Peppol-only statement — email cannot prove delivery.

object

Recipients for the email channel. Reserved — supplying it in v1 is a 400 VALIDATION, since no value of emailDelivery other than none is accepted yet.

payload
required
string

Base64-encoded document. (The multipart/form-data alternative on POST /v1/e-invoices/submissions takes the raw binary payload in its own part — see there.)

Responses

Request samples

Content type
{
  • "clientReference": "ERP-2026-000123",
  • "format": "peppol-bis",
  • "emailDelivery": "none",
  • "email": {
    },
  • "payload": "string"
}

Response samples

Content type
application/json
{
  • "clientReference": "string",
  • "submissionId": "string",
  • "documentId": "string",
  • "conversationId": "string",
  • "status": "Queued"
}

List sent documents and their states

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.

Authorizations:
bearerAuth
query Parameters
since
string

Opaque cursor from a previous page, encoding (lastStateChangeAt, submissionId). Paging is keyset-based over a value that changes as submissions progress, which has one consequence worth designing for: a submission whose status changes while you page moves to a later position and is therefore returned again, rather than being skipped.

So this list is at-least-once: the same submissionId can appear on two pages of one sweep. Deduplicate on submissionId and make your processing idempotent. (The inbox does not behave this way — receivedAt never changes, so GET /v1/e-invoices/inbox is exactly-once.) Skipping is the failure mode that would lose data, and it is the one this ordering rules out.

changedFrom
string <date-time>

Return only submissions whose state last changed at or after this instant. Bounds a first sweep the same way receivedFrom does on the inbox; ignored when since is supplied.

status
string (SubmissionState)
Enum: "Queued" "Sent" "Delivered" "Rejected" "Undeliverable" "DeliveryUnconfirmed" "Failed"

Filter by current status. The filter and the status in the response always agree: both read the same stored value, which is written by the same handler that advances the underlying conversation.

clientReference
string

Filter by the caller's own reference.

limit
integer [ 1 .. 500 ]
Default: 100
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "nextCursor": "string"
}

Send many e-invoices (bulk, asynchronous)

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.

Authorizations:
bearerAuth
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

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 submissionId and belong to POST /v1/e-invoices/submissions/retry; items rejected at admission have no submissionId — nothing was created for them — so resubmit those as a new send or a new batch, under a new key.

Request Body schema: application/json
required
required
Array of objects (DocumentEnvelope) <= 500 items

Also bounded by a total request size of 100 MB.

Responses

Request samples

Content type
application/json
{
  • "items": [
    ]
}

Response samples

Content type
application/json
{
  • "batchId": "string",
  • "accepted": 0,
  • "rejected": 0,
  • "items": [
    ]
}

Get per-item outcomes for a batch

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.

Authorizations:
bearerAuth
path Parameters
batchId
required
string
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "batchId": "string",
  • "state": "Processing",
  • "items": [
    ]
}

Get the status of one sent document

The submission must belong to the participant in X-Peppol-Participant-Id; a submission id belonging to another participant returns 404, not 403.

Authorizations:
bearerAuth
path Parameters
submissionId
required
string
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "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": {
    },
  • "taxReporting": {
    }
}

Retransmit a submission that was not delivered

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.

Authorizations:
bearerAuth
path Parameters
submissionId
required
string
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

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 submissionId and belong to POST /v1/e-invoices/submissions/retry; items rejected at admission have no submissionId — nothing was created for them — so resubmit those as a new send or a new batch, under a new key.

Responses

Response samples

Content type
application/json
{
  • "clientReference": "string",
  • "submissionId": "string",
  • "documentId": "string",
  • "conversationId": "string",
  • "status": "Queued"
}

Retransmit many submissions

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).

Authorizations:
bearerAuth
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

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 submissionId and belong to POST /v1/e-invoices/submissions/retry; items rejected at admission have no submissionId — nothing was created for them — so resubmit those as a new send or a new batch, under a new key.

Request Body schema: application/json
required
submissionIds
required
Array of strings <= 500 items

Responses

Request samples

Content type
application/json
{
  • "submissionIds": [
    ]
}

Response samples

Content type
application/json
{
  • "results": [
    ]
}

Inbox

Received e-invoices, content retrieval, custody acknowledgement (native).

List received e-invoices (cursor-paged descriptors)

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.

Authorizations:
bearerAuth
query Parameters
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 and use the returned cursor from then on.

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 since is supplied — the cursor already carries a position.

status
string
Default: "New"
Enum: "New" "Acknowledged" "All"

Custody filter. All is a filter value only — a response never carries it.

The SAPI surface names the same two states RECEIVED (= New) and ACKNOWLEDGED, and both surfaces read and write one custody state per document: an acknowledgement made through either is visible through the other.

limit
integer [ 1 .. 500 ]
Default: 100
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "nextCursor": "string"
}

Get full metadata for a received e-invoice (no payload)

Authorizations:
bearerAuth
path Parameters
documentId
required
string

The Archiles document identifier. One identifier space, both surfaces: the value returned as InboxDescriptor.id on the native surface and as SapiDocumentMetadata.documentId for a received document on the SAPI surface is the same id, so an id obtained from either surface works on the other. It is not the sender's invoice number — read that from the payload, or from the informational documentNumber.

header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "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": {
    },
  • "customer": {
    },
  • "attachments": [
    ]
}

Download a received e-invoice, optionally converted, with PDF and attachments

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.

Authorizations:
bearerAuth
path Parameters
documentId
required
string

The Archiles document identifier. One identifier space, both surfaces: the value returned as InboxDescriptor.id on the native surface and as SapiDocumentMetadata.documentId for a received document on the SAPI surface is the same id, so an id obtained from either surface works on the other. It is not the sender's invoice number — read that from the payload, or from the informational documentNumber.

query Parameters
format
string (DocumentFormat)
Default: "peppol-bis"
Value: "peppol-bis"

Document format, on send and on retrieval alike — one shared enum, and v1 supports peppol-bis (Peppol BIS Billing 3.0 UBL) only. It is the default when omitted, so the field can be left out entirely.

On send the payload is transmitted byte-for-byte, never re-serialised. On retrieval peppol-bis returns the original received bytes.

peppol-bis is the only value v1 defines, in either direction. Values may be added in a later version; because a new value is additive it cannot break a client, which is why one shared enum is used rather than a split per direction.

pdf
string
Default: "none"
Enum: "none" "include" "only"
attachments
string
Default: "none"
Enum: "none" "inline"

JSON responses only; ignored for Accept: application/zip, which always bundles them.

header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
{
  • "id": "string",
  • "format": "peppol-bis",
  • "document": "string",
  • "pdf": "string",
  • "pdfState": "Ready",
  • "attachments": [
    ]
}

Download a single attachment

Authorizations:
bearerAuth
path Parameters
documentId
required
string

The Archiles document identifier. One identifier space, both surfaces: the value returned as InboxDescriptor.id on the native surface and as SapiDocumentMetadata.documentId for a received document on the SAPI surface is the same id, so an id obtained from either surface works on the other. It is not the sender's invoice number — read that from the payload, or from the informational documentNumber.

attachmentId
required
string
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Acknowledge custody of one received e-invoice

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).

Authorizations:
bearerAuth
path Parameters
documentId
required
string

The Archiles document identifier. One identifier space, both surfaces: the value returned as InboxDescriptor.id on the native surface and as SapiDocumentMetadata.documentId for a received document on the SAPI surface is the same id, so an id obtained from either surface works on the other. It is not the sender's invoice number — read that from the payload, or from the informational documentNumber.

header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "acknowledged": true,
  • "acknowledgedAt": "2019-08-24T14:15:22Z",
  • "error": {
    }
}

Acknowledge custody of many received e-invoices

Authorizations:
bearerAuth
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Request Body schema: application/json
required
ids
required
Array of strings <= 1000 items

Responses

Request samples

Content type
application/json
{
  • "ids": [
    ]
}

Response samples

Content type
application/json
{
  • "results": [
    ]
}

SAPI profile

Wire-compatible subset for ERPs that implement SAPI only.

[SAPI] Obtain access and refresh tokens

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.

Request Body schema: application/json
required
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 TokenResponse.scope.

Responses

Request samples

Content type
application/json
{
  • "client_id": "string",
  • "client_secret": "string",
  • "grant_type": "client_credentials",
  • "scope": "document:send document:receive"
}

Response samples

Content type
application/json
{
  • "access_token": "string",
  • "refresh_token": "string",
  • "token_type": "Bearer",
  • "expires_in": 900,
  • "scope": "document:send document:receive"
}

[SAPI] Check token validity and expiration

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "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"
}

[SAPI] Renew tokens using a refresh token

Request Body schema: application/json
required
refresh_token
required
string

Responses

Request samples

Content type
application/json
{
  • "refresh_token": "string"
}

Response samples

Content type
application/json
{
  • "access_token": "string",
  • "refresh_token": "string",
  • "token_type": "Bearer",
  • "expires_in": 900,
  • "scope": "document:send document:receive"
}

[SAPI] Revoke a refresh token

Request Body schema: application/json
required
token
string
token_type_hint
string
Default: "refresh_token"
Value: "refresh_token"

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "token_type_hint": "refresh_token"
}

Response samples

Content type
application/json
{
  • "error": {
    }
}

[SAPI] Submit a document for Peppol delivery

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.

Authorizations:
bearerAuth
header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Idempotency-Key
required
string <uuid>

Required on the SAPI surface, per SAPI v1.0. Same semantics as the native header.

Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "metadata": {
    },
  • "payload": "string",
  • "payloadFormat": "XML",
  • "payloadEncoding": "UTF-8",
  • "checksum": "string"
}

Response samples

Content type
application/json
{
  • "providerDocumentId": "8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0",
  • "status": "ACCEPTED",
  • "receivedAt": "2019-08-24T14:15:22Z",
  • "timestamp": "2019-08-24T14:15:22Z"
}

[SAPI] List received documents

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.

Authorizations:
bearerAuth
query Parameters
pageToken
string

Opaque cursor from a previous response's nextPageToken.

limit
integer [ 1 .. 100 ]
Default: 20
status
string
Enum: "RECEIVED" "ACKNOWLEDGED"

SAPI's custody vocabulary, and the same custody state the native surface calls status: RECEIVED is the native New, and ACKNOWLEDGED is the native Acknowledged. There is one custody state per document, not one per surface — acknowledging through POST /v1/sapi/document/receive/{documentId}/acknowledge is immediately visible as Acknowledged on GET /v1/e-invoices/inbox, and vice versa. Omit the parameter to list both.

header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "documents": [
    ],
  • "nextPageToken": "string"
}

[SAPI] Retrieve a received document

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.

Authorizations:
bearerAuth
path Parameters
documentId
required
string

The Archiles document identifier. One identifier space, both surfaces: the value returned as InboxDescriptor.id on the native surface and as SapiDocumentMetadata.documentId for a received document on the SAPI surface is the same id, so an id obtained from either surface works on the other. It is not the sender's invoice number — read that from the payload, or from the informational documentNumber.

header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "metadata": {
    },
  • "payload": "string",
  • "payloadFormat": "XML"
}

[SAPI] Acknowledge document receipt

Confirms handover to the software endpoint. Idempotent — re-acknowledging returns success without changing acknowledgedDateTime.

Authorizations:
bearerAuth
path Parameters
documentId
required
string

The Archiles document identifier. One identifier space, both surfaces: the value returned as InboxDescriptor.id on the native surface and as SapiDocumentMetadata.documentId for a received document on the SAPI surface is the same id, so an id obtained from either surface works on the other. It is not the sender's invoice number — read that from the payload, or from the informational documentNumber.

header Parameters
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.senderParticipantId as well; either disagreement is a 400 VALIDATION.

Responses

Response samples

Content type
application/json
{
  • "documentId": "string",
  • "status": "ACKNOWLEDGED",
  • "acknowledgedDateTime": "2019-08-24T14:15:22Z"
}