Archiles Peppol API integration guide#
DRAFT — not the final 1.0 version. This guide and the contract it describes are circulated for review. Paths, schemas, field names and status codes may still change, without notice and without a version bump, until version 1.0.0 is published. Build against it to give feedback, not to ship.
This guide is the narrative companion to openapi.yaml, which is the authoritative machine-readable contract. Where the two ever disagree, the contract is right and this document is a bug. Please report it.
It describes how to use Archiles as a Peppol Access Point entirely over an API: onboarding a participant, checking whether a trading partner is reachable, validating an invoice before sending, sending it, tracking delivery, and pulling received invoices out. No interaction with the Archiles user interface is required at any point.
Language. This guide and the contract are maintained in English. Slovak vocabulary (DIČ, IČ DPH, IČO) is used only where it names a real Slovak identifier.
The rest of the documentation set
| Where | What it is |
|---|---|
openapi.yaml | the contract itself — the authoritative machine-readable source |
| API reference | every endpoint and schema of the contract, rendered for reading |
| API explorer | the same contract, with a form per endpoint for trying calls |
Contents
- What the API does
- The two surfaces
- Authentication and tenancy
- Provisioning a participant
- Sending
- Delivery status
- Receiving
- Errors
- Idempotency, limits, rate limits and retention
- Paging
- Environments
- A worked end-to-end example
- Support
- Sandbox test data
1. What the API does#
| # | Scenario | Endpoints |
|---|---|---|
| 1 | Create a new participant account | POST /v1/peppol/participants |
| 2 | Check whether a DIČ is registered in Peppol, and for which document types | GET /v1/peppol/registry/{participantId}, POST /v1/peppol/registry/lookup |
| 3 | Validate e-invoices before sending | POST /v1/e-invoices/validate, POST /v1/e-invoices/validate/batch |
| 4 | Send e-invoices, including attachments | POST /v1/e-invoices/submissions, POST /v1/e-invoices/submissions/batch |
| 5 | Check the status of sent e-invoices | GET /v1/e-invoices/submissions/{submissionId}, GET /v1/e-invoices/submissions, GET /v1/e-invoices/submissions/batch/{batchId} |
| 6 | Resend undelivered e-invoices | POST /v1/e-invoices/submissions/{submissionId}/retry, POST /v1/e-invoices/submissions/retry |
| 7 | Find out whether new invoices have been received | GET /v1/e-invoices/inbox?since=<cursor>&status=New |
| 8 | Download received invoices, attachments and an optional PDF rendering | GET /v1/e-invoices/inbox/{documentId}/content |
| 9 | Confirm custody of a received invoice | POST /v1/e-invoices/inbox/{documentId}/acknowledge, POST /v1/e-invoices/inbox/acknowledge |
Every scenario for which bulk processing is meaningful has both a single and a bulk form.
Identifiers#
Four names appear in the table below, covering three distinct ids. Learn them before anything else.
| Identifier | What it is | Accepted by |
|---|---|---|
documentId | the archive identity of an invoice, sent or received | all /v1/e-invoices/inbox/{documentId}* routes and /v1/sapi/document/receive/{documentId} |
submissionId | one submission of an outgoing document | GET /v1/e-invoices/submissions/{submissionId}, POST …/retry |
providerDocumentId | the same value as submissionId, under SAPI's name for it | returned by POST /v1/sapi/document/send; pass it to the submission routes above |
batchId | one bulk send | GET /v1/e-invoices/submissions/batch/{batchId} |
What follows from that:
- An id from either surface works on the other. A received document has the same
documentIdon the native inbox and on the SAPI receive route, so no mapping table is needed. In the other direction, theproviderDocumentIda SAPI send returns is thesubmissionIdthe native submission routes expect, which is how a SAPI client gets delivery status at all (§2). - Why sending returns two ids.
submissionIdexists from the moment a submission is accepted;documentIdappears once the document has been mapped into the archive. For a single send both come back together, but a batch is admitted before mapping runs, so a batch item that fails mapping has asubmissionId, statusFailed, and nodocumentIdat all. Poll and retry bysubmissionId; reconcile against your archive bydocumentId. - A corrected resend is a new pair. Retrying keeps both ids (it is the same document going out again); correcting the content and sending it is a new submission and a new document.
The single exception is SAPI's metadata.documentId on send, which SAPI defines as the client's own invoice number. It is declared metadata, checked against the payload, and is not an id you can later address anything with.
2. The two surfaces#
Archiles exposes two HTTP surfaces over one core, and they share authentication:
/v1/sapi/*is a SAPI (sapi-sk.sk) compatibility profile, wire-compatible with SAPI-SK v1.0.0. If your ERP already implements SAPI, point it athttps://<host>/v1/sapiand you are done./v1/peppol/*and/v1/e-invoices/*are the Archiles native surface: everything SAPI does not cover, plus bulk forms of everything that benefits from one. Onboarding and registry lookups live under/v1/peppol/*; validation, sending, sent-status and the inbox under/v1/e-invoices/*.
A token obtained from either surface works on both, so an integrator using both authenticates once.
What SAPI covers, and what it does not#
SAPI is a thin contract by design, scoped to technical acceptance and custody transfer. It does not cover semantic validation, Peppol delivery confirmation or legal effect. It covers 3 of the 9 scenarios above (send, list received, retrieve received) plus custody acknowledgement.
It has no account provisioning, no registry check, no pre-send validation and no sent-status endpoint. Those are native-only extensions with nothing in SAPI to conflict with.
If you are a SAPI-only client, note the sent-status gap, and how to close it. SAPI defines no endpoint for asking what happened to a document after it was accepted, and Archiles will not extend the SAPI surface to fill that gap. The profile stays frozen. Instead the SAPI send hands you the key to the native one:
providerDocumentIdin its response is thesubmissionId, soGET /v1/e-invoices/submissions/{providerDocumentId}with the same token returns the full delivery status. Store that value against your own invoice number when you send. It is the only handle on the submission the SAPI response carries. If you would rather not leave the SAPI surface at all, treat202as technical receipt only and reconcile out of band.
Path prefix#
The SAPI document itself carries no version segment. Its paths are /sapi/auth/token, /sapi/document/send. Archiles hosts the profile behind a version prefix as /v1/sapi/*, matching the native surface so that both surfaces version identically. This is not a deviation: SAPI declares /sapi through OpenAPI servers, which is a deployment address, and every client is configured with a base URL regardless. Point a conforming SAPI client at https://<host>/v1/sapi and every path, body and status code below it is unchanged.
The three deviations from SAPI#
All three are supersets or reinterpretations of an underspecified field. A conforming SAPI client is unaffected by any of them.
| # | SAPI says | Archiles does | Why |
|---|---|---|---|
| 1 | payload: "Maximum size: 10 MB" | accepts up to 20 MB | SAPI's wording is a ceiling, not a floor. Accepting more than a conforming client will ever send cannot break it. Applies to both surfaces. |
| 2 | DocumentMetadata.documentId is client-assigned | for received documents Archiles assigns it | Two senders can trivially both use INV-2026-0001 towards the same receiver, which would make the id non-unique and GET /document/receive/{documentId} ambiguous. Archiles assigns its own document id instead, so the path is well-defined. Consequence: SAPI's metadata does not carry the sender's invoice number. Read it from the payload, or use the native InboxDescriptor.documentNumber. |
| 3 | DocumentMetadata.creationDateTime is the document creation timestamp | treated as the submission timestamp, and excluded from the payload-mismatch check | UBL has no field that dependably means "when the client created this record", and rejecting a send over a clock difference would be hostile. It is recorded, not compared. |
One more SAPI behaviour worth knowing: its listing response returns bare metadata objects that carry neither status nor receivedAt, even though the endpoint offers a status filter. Archiles implements it exactly as specified. To see custody status or receipt time, use the filter, or use the native GET /v1/e-invoices/inbox, which returns both.
Custody status across the two surfaces#
There is one custody state per received document, and the two surfaces spell its values differently:
Native (/v1/e-invoices/inbox) | SAPI (/v1/sapi/document/receive) |
|---|---|
New | RECEIVED |
Acknowledged | ACKNOWLEDGED |
All (filter only) | omit the status parameter |
Because it is one state and not two, acknowledging through either surface is immediately visible through the other. There is nothing to synchronise, and no risk of handling a document twice because you switched surfaces.
Three things this contract writes down that SAPI's schema does not#
None of them changes behaviour, and none can break a conforming client. They only document what the wire already does:
429and500on the SAPI operations. Any HTTP API can rate-limit or fault and your client has to handle both regardless; we would rather write them down than let them arrive undocumented.402onPOST /v1/sapi/document/send. An account with no active subscription, or one whose document allowance is used up, cannot send on either surface (§8).refresh_tokenin the token response. SAPI's schema omits it even though its own/auth/renewrequires one, so a strictly conforming response could never give you the token you are meant to present. You get it on both surfaces.
3. Authentication and tenancy#
Getting a token#
One contract, shared by both surfaces: client_credentials in, a bearer JWT out.
POST /v1/oauth/token (alias: POST /v1/sapi/auth/token)
GET /v1/oauth/token/status (alias: GET /v1/sapi/auth/token/status)
POST /v1/oauth/renew (alias: POST /v1/sapi/auth/renew)
POST /v1/oauth/revoke (alias: POST /v1/sapi/auth/revoke)
The aliases are the same handlers over the same token store. The pair exists only because SAPI mandates its route names. Use whichever matches the surface you are already coding against.
curl -X POST "https://api.test.archiles.sk/v1/oauth/token" -H "Content-Type: application/json" -d '{"grant_type":"client_credentials","client_id":"...","client_secret":"..."}'
{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 900
}
Send the access token as Authorization: Bearer <access_token> on every subsequent call.
Lifetimes and refresh#
- Access token: 15 minutes (
expires_in: 900). Use the value from the response rather than hard-coding 900, and drive your refresh timer off it. - Refresh tokens rotate. Each call to
/v1/oauth/renewreturns a new refresh token and invalidates the one you presented. Store the new one; a second use of the old one fails. GET /v1/oauth/token/statusreports validity, remaining seconds and ashould_refreshhint, if you would rather ask than track expiry yourself.POST /v1/oauth/revokeinvalidates a refresh token. It requires no client authentication, only possession of the token, matching SAPI. The exposure is bounded: a holder of your refresh token can only invalidate it, and revocation is idempotent.
scope is accepted and ignored#
The token request accepts an optional scope, for SAPI conformance. It changes nothing in v1, and nothing comes back. The response does not echo a scope, because echoing one would suggest a grant was evaluated. Permissions are governed by the list of participants your client_id is entitled to, plus the X-Peppol-Participant-Id header on each call. Requesting document:send document:receive and requesting nothing at all give you identical access. Do not design around the field; narrowing permissions in a later version will be backwards compatible.
Lockout#
Repeated failed authentication attempts lock the credential temporarily, and the token endpoint returns 423. This is SAPI's defined behaviour. Back off rather than retrying a rejected secret in a loop.
No IP allow-list. Archiles does not implement per-client IP restrictions, so SAPI's
403on the token endpoint ("IP address not allowed for this client") is never emitted. You will not be asked for your IP ranges, and you should not expect address-based filtering to protect your credential. Keep the secret secret.
Tenancy and the participant header#
One credential acts on behalf of many participants: an ERP vendor, accounting firm or portal holds one client_id and manages many DIČs. Which participant a given call acts on is selected per request:
X-Peppol-Participant-Id: 0245:2022182030
Archiles checks entitlement against your credential's allowed-participant list before any other processing. A participant you are not entitled to returns 403, whether or not it exists. Nothing leaks about participants that are not yours.
The header is required on every operation that reads or writes data belonging to one participant: all of /v1/e-invoices/submissions*, /v1/e-invoices/inbox*, /v1/e-invoices/validate*, and the whole SAPI document surface. Five native operations deliberately take no participant context:
| Operation | No header because | Returns 403? |
|---|---|---|
POST /v1/peppol/participants | the participant does not exist yet | no |
GET /v1/peppol/participants | it is the operation that tells you which participants you have | no |
GET /v1/peppol/participants/{participantId} | the path segment is the tenancy key | yes |
GET /v1/peppol/registry/{participantId} | public SMP directory data | no |
POST /v1/peppol/registry/lookup | public SMP directory data | no |
On send, X-Peppol-Participant-Id must match the supplier endpoint derived from the payload (both surfaces), and on the SAPI surface it must also equal metadata.senderParticipantId. Either disagreement is a 400, category: VALIDATION, see §5.
Why require the header at all, when the sender is in the payload anyway? The entitlement check runs on the header before anything parses the body, so an unentitled call costs nothing to refuse. Metering, rate limiting and the audit trail get a participant to attribute the call to even when the payload turns out to be malformed. And it catches one participant's invoice being sent in another's context, before anything reaches the network.
4. Provisioning a participant#
POST /v1/peppol/participants create an account and a participant, keyed by DIČ
GET /v1/peppol/participants list the participants you are entitled to
GET /v1/peppol/participants/{id} state, including registrationState
A newly created participant can neither send nor receive until its Peppol registration has completed. Registration in the Peppol directory happens out of band, not as part of this call. Poll
GET /v1/peppol/participants/{id}and wait forregistrationStateto becomeActive; until then it isPending.
In rare cases an account cannot be provisioned automatically. That returns 422. The request was valid and the DIČ was free, but the operation needs a human. Contact support (§13) rather than retrying; 422 is not retryable.
Checking a trading partner#
Before sending to a new counterparty, check that they are reachable and accept the document type you intend to send:
GET /v1/peppol/registry/0245:2022182030
{
"participantId": "0245:2022182030",
"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"
]
}
documentTypes carries fully qualified Peppol document type identifiers, verbatim from the SMP. It is an open list of strings, not a closed set: the directory can advertise types Archiles does not process, and new BIS versions must not require a contract change. Match on the strings you care about; do not assume the list is exhaustive of anything but what that participant publishes.
An unregistered participant returns 200 with registered: false and an empty documentTypes, not an error.
POST /v1/peppol/registry/lookup takes many participant ids and returns per-item results, so one flaky lookup does not sink a 5000-partner sweep. Because failures are per item, the bulk endpoint has no whole-request 503; the single-participant GET does, with Retry-After.
Registry checks are not entitlement-scoped. SMP registration data is public Peppol directory information, so any authenticated client may look up any participant. That is deliberate. The usual subject of a lookup is a trading partner, which you do not manage.
5. Sending#
Validate first#
POST /v1/e-invoices/validate runs the identical chain the send path runs, with zero side effects: XSD → CEN/Peppol Schematron → SK national rules → a mapping dry-run. The mapping dry-run matters: without it you could get a 400 at send that no pre-check could have predicted.
{
"valid": false,
"clientReference": "ERP-2026-000123",
"errors": [
{ "source": "Schematron", "rule": "BR-CO-10", "location": "/Invoice/cac:LegalMonetaryTotal", "message": "Sum of line net amounts must equal the line extension amount" },
{ "source": "Mapping", "rule": "SUPPLIER_TAX_ID_MISSING", "message": "Supplier tax identifier could not be resolved" }
],
"warnings": [
{ "source": "Mapping", "rule": "CHARGE_CODE_UNMAPPED", "message": "Allowance/charge reason code 64 has no Archiles equivalent" }
]
}
source is Xsd, Schematron or Mapping, so you can tell a CEN rule violation apart from an Archiles mapping limitation. The result echoes clientReference back, and batch results are keyed by it.
Validation requires
X-Peppol-Participant-Ideven though it has no side effects, because it runs as a participant: the header selects the account whose validation strictness setting, EAS scheme and default currency/country change the verdict, and it is what entitlement, metering and rate limiting attribute the call to. So you cannot validate before you own a participant.POST /v1/peppol/participantscomes first.
POST /v1/e-invoices/validate/batch is the bulk form: one result per item, in request order, each echoing its clientReference when you supplied one.
The document envelope#
Validation and sending take the same body, so the document you validate is exactly the document you would send:
{
"clientReference": "ERP-2026-000123",
"format": "peppol-bis",
"payload": "<base64-encoded document>"
}
formatis optional and defaults topeppol-bis(Peppol BIS Billing 3.0 UBL), which is the only value v1 accepts, in either direction. Values may be added later; because a new value is additive it cannot break your client.payloadis base64 on the native surface. On the SAPI surfacepayloadis a raw UTF-8 XML string, exactly as SAPI specifies. The asymmetry is deliberate, because SAPI clients send raw XML.clientReferenceis your own id. Every response echoes it, and you can query it viaGET /v1/e-invoices/submissions?clientReference=…. It is indexed but not enforced unique;Idempotency-Keyis the deduplication mechanism, not this.- Maximum payload: 20 MB, on both surfaces.
multipart/form-datais available as an alternative body onPOST /v1/e-invoices/submissionsfor large payloads; supply the bytes in thepayloadpart or inside theenvelopepart, never both. Doing both is a400.POST /v1/e-invoices/submissions/batchis JSON only. - There is no
attachmentsfield on send. In Peppol BIS attachments live inside the UBL, asAdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject. Embed them there. Sincepeppol-bisis the only send format and Archiles passes its bytes through untouched, a separate array could only ever be rejected. emailDeliveryandemailare reserved and not usable yet. The envelope declares anemailDeliveryfield (none|fallback|only|always, defaultnone) and anemailobject for recipients. v1 acceptsnoneonly. Any other value, or anemailobject, is a400,category: PERMANENT. They are in the contract now so the delivery-channel choice is visible from the start, and so nobody writes a client assuming Peppol is the only channel there will ever be.
When email delivery ships, fallback will send over Peppol where the buyer is a registered participant and by email where it is not; only skips Peppol; always does both. The email leg will report its own outcome in a separate emailDelivery object on the status response, so status keeps meaning Peppol delivery. Delivered will stay a Peppol-only statement, because email cannot prove delivery.
What happens to your bytes#
Archiles transmits a Peppol BIS payload byte-for-byte as you supplied it, never re-serialised through an Archiles exporter. Nothing is lost in remapping, and a signed payload reaches the buyer unaltered.
In parallel, Archiles maps the document into its own archive model, for search, PDF rendering, approvals and format conversion. That mapping is a precondition of sending:
- a structural mapping failure rejects the submission with
400,category: VALIDATION - unmappable optional fields are accepted, and recorded as warnings
This is why the mapping dry-run in POST /v1/e-invoices/validate matters, and why you should validate new document shapes before relying on them in production.
Declared metadata must match the payload#
On the SAPI surface, metadata is required and declares the Peppol routing explicitly. Archiles also derives that routing from the UBL payload, so the two inputs can disagree.
Rule: Archiles rejects any disagreement with 400, category: VALIDATION, retryable: false.
| Declared field | Compared against |
|---|---|
senderParticipantId | cac:AccountingSupplierParty endpoint ID, built via the configured EAS scheme |
receiverParticipantId | cac:AccountingCustomerParty endpoint ID |
documentTypeId | UBL root element + cbc:CustomizationID |
processId | cbc:ProfileID |
documentId | cbc:ID of the invoice or credit note |
creationDateTime | not compared. It is a submission timestamp, not a document field |
checksum, when supplied, is verified as SHA-256 over the exact payload bytes; a mismatch is the same 400.
The reasoning: silently preferring the payload would deliver an invoice you believe went elsewhere. Silently preferring the metadata would route a document to a participant it does not name internally, which the receiving access point may then reject after transmission. A mismatch is a client bug, and the cheapest place to catch it is synchronously, at submission.
Single send#
POST /v1/e-invoices/submissions Idempotency-Key recommended
Full synchronous validation, then 202:
{
"clientReference": "ERP-2026-000123",
"submissionId": "8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0",
"documentId": "66c1f0a2b3d4e5f6a7b8c9d0",
"conversationId": "urn:uuid:5d9c1a7e-3f42-4b18-9a55-2c0d7e8f1b34",
"status": "Queued"
}
Delivery itself is asynchronous. 202 means validated, archived and enqueued, not delivered. This matches SAPI's ACCEPTED, which is technical acceptance.
Batch send#
POST /v1/e-invoices/submissions/batch ≤ 500 items or ≤ 100 MB, Idempotency-Key recommended
Batch admission runs cheap checks only, meaning entitlement, well-formedness, size and count, and then returns 202. Schematron, mapping and publishing run asynchronously per item.
{
"batchId": "b7d2c3e4-5f60-4718-8293-a1b2c3d4e5f6",
"accepted": 498,
"rejected": 2,
"items": [
{ "clientReference": "ERP-2026-000123", "submissionId": "8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0" },
{ "clientReference": "ERP-2026-000124",
"error": { "category": "VALIDATION", "code": "ARCH-VAL-002",
"message": "Payload is not well-formed XML",
"retryable": false,
"correlation_id": "550e8400-e29b-41d4-a716-446655440000" } }
]
}
accepted and rejected count the synchronous admission step only. Schematron and mapping have not run when the 202 is written, so an item counted in accepted can still fail afterwards. Those outcomes appear only in GET /v1/e-invoices/submissions/batch/{batchId}. accepted + rejected always equals the number of items you submitted.
Batch results come back in request order and echo each item's clientReference. Correlate by the reference where you set one and by position where you did not, but set one on every item. A reference keeps correlation correct no matter what else changes, and it is what GET /v1/e-invoices/submissions filters on later.
6. Delivery status#
GET /v1/e-invoices/submissions/{submissionId}
GET /v1/e-invoices/submissions?since=<cursor>&changedFrom=<ts>&status=Sent&clientReference=…
GET /v1/e-invoices/submissions/batch/{batchId}
POST /v1/e-invoices/submissions/{submissionId}/retry
POST /v1/e-invoices/submissions/retry { "submissionIds": [ … ] }
The statuses#
Under Slovak legislation a document receives at most one MLS (Peppol message level status), and that MLS is final. There is therefore no separate business-response phase to wait for: the MLS is the recipient's verdict.
| Status | Final? | Retry allowed? | Meaning |
|---|---|---|---|
Queued | no | no | validated, archived, waiting for hand-off to the network |
Sent | no | no | handed off, on the network, awaiting the MLS |
Delivered | yes | no | positive and final. Includes the case where the receiving access point forwarded the document onward without a verifiable acknowledgement, which SK treats as accepted |
Rejected | yes | no | the recipient rejected the document. The content must change |
Undeliverable | yes | yes | permanent transport failure. Usually the receiver is not a registered participant, or not registered for this document type |
DeliveryUnconfirmed | no | yes | no MLS arrived inside the SLA window. Not proof of failure, because a late MLS still resolves it |
Failed | yes | yes | an Archiles-side failure. failureReason says what |
An unrecognised status must be treated as non-final. New values can be added over time, and the safe reading of one your client does not know is "not finished yet": keep watching it and put it in front of a human rather than filing it as an outcome. Every new value arrives with its finality stated in the changelog entry that introduces it.
Transitions.
Queued → Sent | Undeliverable | Failed
Sent → Delivered | Rejected | Undeliverable | DeliveryUnconfirmed | Failed
DeliveryUnconfirmed → Delivered | Rejected | Failed
Failed → Delivered | Rejected (only if a late confirmation arrives; see below)
Statuses do not simply move forward. DeliveryUnconfirmed → Delivered is a legal transition, because a late MLS resolves an unconfirmed delivery positively. What does hold is that a recorded verdict is never overturned. Nothing moves a submission out of Delivered or Rejected except an explicit retry, which puts it back into Queued. Failed is the exception, and only because it holds no verdict. It means we could not complete or confirm the send, which does not prove the document never reached the buyer. If a confirmation for it does arrive afterwards, the submission resolves to Delivered or Rejected, and you would want it to, because the alternative is retrying a document the buyer already has.
When to stop polling#
At Delivered, Rejected, Undeliverable or Failed. The pipeline will not move those again. DeliveryUnconfirmed is the only status that can still change by itself, and it is also the one you are expected to act on. Nothing is ever left in a status that can neither change nor be treated as final.
Two refinements, both in your favour:
- A verdict is never overturned. Once a submission is
DeliveredorRejected, Archiles refuses a contradicting message that arrives afterwards rather than applying it, so what you read is what stands. Failedis the one final status the network can still resolve. It means we could not complete the send; if a delivery confirmation for that document turns up later anyway, the submission moves toDeliveredorRejected. TreatFailedas final for your own bookkeeping and treat that late resolution as good news.
Two further guarantees:
- You may miss intermediate statuses.
GETreturns the current status, not a log. A document can go fromQueuedtoDeliveredbetween two polls. If you need a history, recordlastStateChangeAtyourself. Queuedguarantees the document is archived and its payload persisted. It guarantees nothing about the network.
Tax reporting#
The status response also carries taxReporting: { state, message? }, so you can observe the Slovak tax-reporting side-flow. It is absent when no tax-reporting leg applies to the document. state is one of NotApplicable, Pending, Sent, Delivered, DeliveryUnconfirmed, Rejected, Unreachable, Disregarded. SAPI has no equivalent field.
Read it value by value rather than assuming it is the delivery-status enum again. Five values do mean the same kind of thing on both (Sent, Delivered, DeliveryUnconfirmed, Rejected, Disregarded), but the two sets are not interchangeable:
PendingandNotApplicableexist only here.Unreachableis about not reaching the tax authority's endpoint, which is a different statement from the buyer being unaddressable (Undeliverableabove).- There is deliberately no
Failedhere. An Archiles-side failure is somethingstatusreports about the invoice itself; everything in this list is the tax-reporting leg's own outcome.
Retry#
POST /v1/e-invoices/submissions/{submissionId}/retry re-enqueues the payload already stored for that submission, same document and same submission id, and returns 202 with the status back to Queued. It is a retransmission: it cannot create a second invoice in Archiles, and it appends a new send attempt rather than replacing the history.
Allowed from DeliveryUnconfirmed, Undeliverable and Failed. Everything else is 409:
| Current status | Why not |
|---|---|
Queued, Sent | a transmission is already in flight |
Delivered | already delivered; a resend risks a duplicate invoice at the buyer |
Rejected | the recipient rejected the content; the same bytes will be rejected again. Send a corrected document as a new submission |
A retry costs one item against the rate limit, exactly like a send. The bulk form (POST /v1/e-invoices/submissions/retry, ≤ 500 ids, per-item results) exists because re-driving a night's worth of DeliveryUnconfirmed items one call at a time is the same problem bulk send solves.
This is also how you retry part of a batch. An admitted batch item has a submissionId and is re-driven here. An item rejected at admission has none, because nothing was created for it, so resubmit it as a new send or batch under a new Idempotency-Key.
7. Receiving#
Inbound delivery is pull-only in v1. There are no webhooks: nothing for us to retry, no endpoint of yours to secure, no delivery guarantee for us to own. You poll. Optional webhooks for send-status changes and for newly received invoices are on the roadmap.
Listing#
GET /v1/e-invoices/inbox?since=<cursor>&receivedFrom=<ts>&status=New&limit=100
{
"items": [
{
"id": "66c1f0a2b3d4e5f6a7b8c9d0",
"cursor": "eyJyIjoiMjAyNi0wOC0xOVQwOToxNDoyMloiLCJpIjoiNjZjMWYwYTJiM2Q0ZTVmNmE3YjhjOWQwIn0",
"receivedAt": "2026-08-19T09:14:22Z",
"senderParticipantId": "0245:9876543210",
"documentType": "invoice",
"documentNumber": "2026/00417",
"issueDate": "2026-08-18",
"totalAmount": "1234.56",
"currency": "EUR",
"attachmentCount": 2,
"status": "New",
"pdfState": "Ready"
}
],
"nextCursor": "eyJyIjoiMjAyNi0wOC0xOVQwOToxNDoyMloiLCJpIjoiNjZjMWYwYTJiM2Q0ZTVmNmE3YjhjOWQwIn0"
}
Descriptors carry no payload, so "has anything new arrived?" is one small call.
status on a descriptor is New or Acknowledged. The status query parameter also accepts All, which is a filter value only and never appears in a response.
receivedFrom bounds a first sweep. Omitting both since and receivedFrom starts from the beginning of the account's history, which for an established account can be years of invoices. Pass the current timestamp as receivedFrom to start from empty. It is ignored once since is supplied.
documentType,documentNumber,issueDate,totalAmountandcurrencyare informational. They are products of the mapping layer and any of them may be absent when a received document maps imperfectly. The payload from/contentis the authoritative document. Post to your ledger from that, never from these fields.totalAmountis a decimal string ("1234.56"), not a JSON number: money must not travel through binary floating point, and this field is for display and search, not for arithmetic that has to reconcile.
Content retrieval#
GET /v1/e-invoices/inbox/{documentId}
GET /v1/e-invoices/inbox/{documentId}/content?format=peppol-bis&pdf=include
GET /v1/e-invoices/inbox/{documentId}/attachments/{attachmentId}
formattakespeppol-bis, the default and only value in v1. It returns the original received bytes.pdftakesnone(default),includeoronly.- Content negotiation chooses the response shape:
Accept: application/json → { id, format, document (base64), pdf?, pdfState, attachments }
Accept: application/zip → document.xml
document.pdf (when pdf=include|only)
attachments/001_delivery-note.pdf (original names, collision-suffixed)
manifest.json (ids, names, MIME, SHA-256, metadata)
The ZIP bundle exists because a received invoice with five attachments would otherwise be seven round trips.
pdfState: "Pending" means retry, not "no PDF". Inbound PDF rendering is asynchronous. The states are Ready, Pending and Unavailable.
Archiles extracts attachments embedded in the received UBL and exposes them as addressable sub-resources. The raw UBL still contains them, so nothing is lost either way.
Acknowledgement#
POST /v1/e-invoices/inbox/{documentId}/acknowledge single
POST /v1/e-invoices/inbox/acknowledge bulk { "ids": [ … ] }
Both return 200 with an acknowledgedAt timestamp. The custody state goes New → Acknowledged, one-way and idempotent. acknowledgedAt is the timestamp of the first acknowledgement and does not change on later calls, so a repeated acknowledge is safe and visibly a no-op. There is no un-acknowledge; a client that loses its own state re-reads with status=All.
Three different things are called an acknowledgement in Peppol. Do not conflate them.
- MLS. The Peppol message level status. Automatic on both sides, never client-driven. Outbound it is the recipient's verdict (see §6). Inbound, Archiles generates it when the document is imported.
/acknowledge. Custody transfer from Archiles to you. This is the one you call. It has nothing to do with the MLS, which was already sent at import time.- UBL
ApplicationResponse(Peppol Invoice Response). A separate, additional business response document. Out of scope for v1: Archiles neither sends nor consumes it, and nothing in the Slovak flow requires it. Its absence costs nothing, because the MLS already carries accept and reject.
8. Errors#
One envelope on both surfaces:
{ "error": {
"category": "VALIDATION",
"code": "SAPI-VAL-001",
"message": "Document payload is invalid",
"details": [ { "field": "metadata.receiverParticipantId",
"issue": "Does not match payload",
"value": "0245:9876543210" } ],
"retryable": false,
"correlation_id": "550e8400-e29b-41d4-a716-446655440000" } }
category, code, message, retryable and correlation_id are always present.
categoryis one ofAUTH,VALIDATION,PROCESSING,TEMPORARY,PERMANENT.retryableis authoritative. Retry only when it istrue, with exponential backoff. You never have to infer retry behaviour from a status code.correlation_idis a UUID, logged on our side. Successful responses carry it too, as theX-Correlation-Idheader, so you can quote one for any interaction, not just failures.details[]carries the machine-usable part when there is one: the offending field and value. It is the one array in the API that is absent rather than empty when it has nothing to report; every other array (items,results,errors,warnings,attachments,documentTypes) is always present as[].TEMPORARYalways carriesRetry-Afterin addition toretryable: true.
Do not branch on
code. It exists for support, logging and display. Show it to the user together withmessage, and drive control flow from the HTTP status plusretryable. The code set is deliberately open and grows without a contract change, which is why we publish no exhaustive catalogue. An enumerated list would invite the branching we are asking you not to do.ARCH-*codes are native-only conditions,SAPI-*are SAPI's. A published code is never repurposed.
Status codes#
| Status | Category | Retryable | Used for |
|---|---|---|---|
| 400 | VALIDATION | no | Malformed body, schema violation, failed Schematron or mapping, declared metadata disagreeing with the payload |
| 401 | AUTH | no; re-authenticate first | Missing, invalid or expired token |
| 402 | PERMANENT | no | The account cannot send more documents right now |
| 403 | AUTH | no | Participant not in your entitlements |
| 404 | PERMANENT | no | No such resource for this participant |
| 406 | PERMANENT | no | The Accept type or format requested cannot be produced for this document (content retrieval only) |
| 409 | PERMANENT | no | Idempotency key reused with a different payload; duplicate participant; retry from a status that forbids it |
| 413 | PERMANENT | no | Payload or batch over limit |
| 415 | PERMANENT | no | Unsupported content type |
| 422 | PERMANENT | no | Participant cannot be provisioned automatically; the request is valid, the operation needs support |
| 423 | AUTH | no; retry after the lockout expires | Credential temporarily locked after repeated failed authentications |
| 429 | TEMPORARY | yes, after Retry-After | Rate limit |
| 500 | PROCESSING | yes, with backoff | Unexpected server fault |
| 503 | TEMPORARY | yes, after Retry-After | SMP lookup unavailable (registry checks), and on any operation, maintenance or an infrastructure fault |
402 is deliberately one condition, not three. It means "this account cannot send more documents right now" and does not distinguish a subscription that never existed from one that expired or one whose document allowance is exhausted. All three have the same remedy: extend the subscription, which is also the act of agreeing to the terms. A split would give you nothing to do differently.
403 versus 404. A participant you are not entitled to always returns 403, whether or not it exists. A document id that exists but belongs to another participant returns 404, not 403, so nobody can probe document ids for existence across tenants.
503 is listed on the registry check only, but can reach you anywhere. The contract declares it where the application itself produces it, on an SMP lookup that cannot be performed. During a deployment or an infrastructure fault any operation may answer 503, and we do not declare it on all thirty of them, because that would say nothing an integrator does not already have to handle. Treat any 503 as TEMPORARY, honour Retry-After, and keep your Idempotency-Key for the retry.
Retry guidance#
Retry only 429, 500, 503 and transport-level errors. Use exponential backoff starting at 1 second, capped at 5 minutes, with jitter. Always honour Retry-After. Always send an Idempotency-Key on send, so that a retry after an ambiguous timeout cannot duplicate an invoice.
9. Idempotency, limits, rate limits and retention#
Idempotency#
Idempotency-Key is honoured on five operations: POST /v1/e-invoices/submissions, POST /v1/e-invoices/submissions/batch, POST /v1/e-invoices/submissions/{submissionId}/retry, POST /v1/e-invoices/submissions/retry and the SAPI send. Keys are retained 24 hours and scoped to your client.
A key identifies one call to one operation on one resource. The method, the path (including the submissionId in it) and the body all take part. So retrying two different submissions under a single key is two different requests and both are honoured; it is only the same call repeated that replays the first response.
| Situation | Result |
|---|---|
| New key | Processed normally; the response is stored |
| Same key, same body | The original stored response is replayed verbatim, including its status code |
| Same key, different body | 409 PERMANENT; the key is already bound to a different payload |
| No key | Processed normally, no deduplication |
| Concurrent duplicate still in flight | The second request blocks briefly, then replays the first response |
This is a transport guarantee only. If you resubmit the same invoice under a new key, it is sent twice. Idempotency protects you against ambiguous timeouts, not against your own duplicate logic.
Batches. The key covers the whole batch, so replaying it returns the original 202 and cannot re-drive part of the work. That is deliberate. The retry routes in §6 are what re-drive individual items.
Limits#
| Limit | Value |
|---|---|
| Maximum payload per document | 20 MB (both surfaces) |
| Maximum items per batch | 500 |
| Maximum total size per batch | 100 MB |
| Maximum ids per bulk retry | 500 |
| Inbox / submissions page size | default 100, maximum 500 (SAPI's receive listing: default 20, maximum 100) |
| Idempotency key retention | 24 hours |
| Access token lifetime | 15 minutes |
| Refresh token | idle 30 minutes, maximum 10 hours, rotated on every use |
The per-minute request rate depends on your subscription and comes with your credentials. The live values are always on the wire; see below.
Rate limits#
A token bucket per API client. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; read them rather than modelling the bucket yourself.
Bulk endpoints count per item, not per request. Otherwise a 500-item batch would cost the same as a single send. A retry counts as one item, like a send.
The budget is per credential, not per participant. If you front many participants, they share one budget and there is no fair-share scheme between them: one participant's nightly 5000-invoice sweep can consume the whole allowance. Interleave your work across participants rather than draining one at a time. Pacing is yours to manage.
A batch that would exceed your remaining budget is accepted whole and drained at the permitted rate. It is never partially accepted and never split. You get 429 only when the budget is already exhausted at admission. In that case nothing was enqueued, and the whole request is safe to retry after Retry-After.
Retention#
Nothing is purged automatically. A received document, its payload and its attachments remain available for as long as the account keeps them. A 404 on a document id means the account's own users deleted it, not that it aged out. The only time-bounded record is Idempotency-Key state (24 hours), which is a transport artefact rather than a document.
10. Paging#
Both list surfaces page forward only, but they signal the end differently. The difference is deliberate, because the SAPI half is frozen to its published schema.
Native (/v1/e-invoices/*) | SAPI (/v1/sapi/*) | |
|---|---|---|
| Cursor parameter | since | pageToken |
| Cursor in response | nextCursor, always present | nextPageToken, absent on the last page |
| How you stop | when a page comes back with an empty items array | when nextPageToken is absent |
| Bounding a first sweep | receivedFrom (inbox), changedFrom (submissions) | not available |
If you use both surfaces, implement both stop conditions.
The native cursor is opaque, an encoding of a timestamp plus a document id. Do not parse it or construct one; store the last value you were given and pass it back.
Per-item cursor fields on inbox descriptors and submissions are for resuming mid-page. The list-level nextCursor equals the cursor of the last item on the page, so if you process whole pages you can ignore them entirely.
The submission listing is at-least-once; the inbox is exactly-once. The inbox orders by receivedAt, which never changes, so a document appears once per sweep. The submission listing orders by last status change, which is the point of it. A submission that moves while you are paging reappears at its new position rather than being skipped, so the same submissionId can appear on two pages of one sweep. Deduplicate on submissionId and keep your processing idempotent. Being shown something twice is recoverable; being silently skipped would not be.
Archiles ignores receivedFrom and changedFrom once you supply since, which is the more specific instruction.
11. Environments#
| Test / sandbox | Production | |
|---|---|---|
| API base URL | https://api.test.archiles.sk | https://api.archiles.sk (final hostname confirmed at go-live) |
| Native surface | https://api.test.archiles.sk/v1/peppol, .../v1/e-invoices | https://api.archiles.sk/v1/peppol, .../v1/e-invoices |
| SAPI surface | https://api.test.archiles.sk/v1/sapi | https://api.archiles.sk/v1/sapi |
| Token endpoint | https://api.test.archiles.sk/v1/oauth/token | https://api.archiles.sk/v1/oauth/token |
The token endpoint is part of the API host. There is no separate identity host to configure, and no other host should appear in your client configuration. If any documentation ever points you at one, treat it as an error and tell us.
How credentials are issued#
Credentials are issued per integrator during onboarding. You receive:
| Item | Notes |
|---|---|
| Base URL | test first; production at go-live |
client_id | e.g. archiles-api-client |
client_secret | sent over a separate channel, never in plain text by email |
| Participant list | the DIČs your credential is entitled to act on |
| Rate limit | the per-minute value for your subscription |
We do not send you access or refresh tokens. You obtain those yourself from the token endpoint. From us you get only client_id and client_secret.
You will not be asked for IP ranges: nothing filters on them (§3).
12. A worked end-to-end example#
The whole loop, in order, with the same participant throughout. The base URL is the test host. The Authorization header is elided after step 1 but is required on every call, and X-Correlation-Id comes back on every response except the token call. The example shows the headers once, on step 2.
1. Get a token#
curl -X POST "https://api.test.archiles.sk/v1/oauth/token" -H "Content-Type: application/json" -d '{"grant_type":"client_credentials","client_id":"archiles-api-client","client_secret":"<secret>"}'
HTTP/1.1 200 OK
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900
}
The token routes are the one place that returns no X-Correlation-Id and no rate-limit headers. Every other response in this example carries all four (§8, §13).
2. Check that the buyer is reachable#
curl "https://api.test.archiles.sk/v1/peppol/registry/0245:9876543210" -H "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
X-Correlation-Id: 550e8400-e29b-41d4-a716-446655440000
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1755594900
{
"participantId": "0245:9876543210",
"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"
]
}
3. Validate the invoice#
curl -X POST "https://api.test.archiles.sk/v1/e-invoices/validate" -H "Authorization: Bearer $TOKEN" -H "X-Peppol-Participant-Id: 0245:2022182030" -H "Content-Type: application/json" -d @validation-request.json
validation-request.json:
{
"clientReference": "ERP-2026-000123",
"format": "peppol-bis",
"payload": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4..."
}
{
"valid": true,
"clientReference": "ERP-2026-000123",
"errors": [],
"warnings": []
}
Note the empty arrays: they are always present, never omitted and never null.
4. Send it#
curl -X POST "https://api.test.archiles.sk/v1/e-invoices/submissions" -H "Authorization: Bearer $TOKEN" -H "X-Peppol-Participant-Id: 0245:2022182030" -H "Idempotency-Key: 4f8c2b10-6d3a-4e91-8a77-0b5f1c9e2d43" -H "Content-Type: application/json" -d @validation-request.json
HTTP/1.1 202 Accepted
{
"clientReference": "ERP-2026-000123",
"submissionId": "8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0",
"documentId": "66c1f0a2b3d4e5f6a7b8c9d0",
"conversationId": "urn:uuid:5d9c1a7e-3f42-4b18-9a55-2c0d7e8f1b34",
"status": "Queued"
}
The body is the same file you validated in step 3, which is the point of the shared envelope.
5. Poll until a final status#
curl "https://api.test.archiles.sk/v1/e-invoices/submissions/8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0" -H "Authorization: Bearer $TOKEN" -H "X-Peppol-Participant-Id: 0245:2022182030"
First poll:
{
"submissionId": "8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0",
"clientReference": "ERP-2026-000123",
"documentId": "66c1f0a2b3d4e5f6a7b8c9d0",
"conversationId": "urn:uuid:5d9c1a7e-3f42-4b18-9a55-2c0d7e8f1b34",
"cursor": "eyJjIjoiMjAyNi0wOC0xOVQwOToxMjowMVoiLCJzIjoiOGYxNGU0NWYifQ",
"status": "Sent",
"senderParticipantId": "0245:2022182030",
"receiverParticipantId": "0245:9876543210",
"documentType": "invoice",
"submittedAt": "2026-08-19T09:11:48Z",
"lastStateChangeAt": "2026-08-19T09:12:01Z",
"taxReporting": { "state": "Sent" }
}
A later poll, once the MLS has arrived:
{
"submissionId": "8f14e45f-ea0a-4c1f-9b2e-71cbd4a1c2f0",
"clientReference": "ERP-2026-000123",
"documentId": "66c1f0a2b3d4e5f6a7b8c9d0",
"conversationId": "urn:uuid:5d9c1a7e-3f42-4b18-9a55-2c0d7e8f1b34",
"cursor": "eyJjIjoiMjAyNi0wOC0xOVQwOToxNDozM1oiLCJzIjoiOGYxNGU0NWYifQ",
"status": "Delivered",
"senderParticipantId": "0245:2022182030",
"receiverParticipantId": "0245:9876543210",
"documentType": "invoice",
"submittedAt": "2026-08-19T09:11:48Z",
"lastStateChangeAt": "2026-08-19T09:14:33Z",
"taxReporting": { "state": "Delivered" }
}
Delivered is final, so stop polling. Had it come back DeliveryUnconfirmed, you would keep polling or retry (§6); had it come back Rejected, you would correct the document and send it as a new submission.
6. Poll the inbox#
curl "https://api.test.archiles.sk/v1/e-invoices/inbox?status=New&limit=100&receivedFrom=2026-08-19T00:00:00Z" -H "Authorization: Bearer $TOKEN" -H "X-Peppol-Participant-Id: 0245:2022182030"
{
"items": [
{
"id": "66c1f0a2b3d4e5f6a7b8c9d1",
"cursor": "eyJyIjoiMjAyNi0wOC0xOVQwOToxNDoyMloiLCJpIjoiNjZjMWYwYTIifQ",
"receivedAt": "2026-08-19T09:14:22Z",
"senderParticipantId": "0245:9876543210",
"documentType": "invoice",
"documentNumber": "2026/00417",
"issueDate": "2026-08-18",
"totalAmount": "1234.56",
"currency": "EUR",
"attachmentCount": 1,
"status": "New",
"pdfState": "Ready"
}
],
"nextCursor": "eyJyIjoiMjAyNi0wOC0xOVQwOToxNDoyMloiLCJpIjoiNjZjMWYwYTIifQ"
}
Store nextCursor and pass it as since on the next poll. Keep polling until items comes back empty, because nextCursor is present even on the last page.
7. Fetch the content#
curl "https://api.test.archiles.sk/v1/e-invoices/inbox/66c1f0a2b3d4e5f6a7b8c9d1/content?pdf=include" -H "Authorization: Bearer $TOKEN" -H "X-Peppol-Participant-Id: 0245:2022182030" -H "Accept: application/json"
{
"id": "66c1f0a2b3d4e5f6a7b8c9d1",
"format": "peppol-bis",
"document": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4...",
"pdf": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGgg...",
"pdfState": "Ready",
"attachments": [
{
"id": "66c1f0a2b3d4e5f6a7b8c9e0",
"filename": "delivery-note.pdf",
"mimeType": "application/pdf",
"sizeBytes": 48213,
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
]
}
document is the original received bytes. Post to your ledger from this, not from the descriptor's informational fields. For the whole thing in one file, ask for Accept: application/zip instead.
8. Acknowledge custody#
curl -X POST "https://api.test.archiles.sk/v1/e-invoices/inbox/66c1f0a2b3d4e5f6a7b8c9d1/acknowledge" -H "Authorization: Bearer $TOKEN" -H "X-Peppol-Participant-Id: 0245:2022182030"
{
"id": "66c1f0a2b3d4e5f6a7b8c9d1",
"acknowledged": true,
"acknowledgedAt": "2026-08-19T09:31:07Z"
}
Calling it again returns the same acknowledgedAt, the timestamp of the first acknowledgement. That is how you can tell the operation is genuinely idempotent rather than merely tolerant.
Use POST /v1/e-invoices/inbox/acknowledge with { "ids": [ … ] } to acknowledge a whole page at once; it returns the same object per item, plus a per-item error where one failed.
13. Support#
Every response carries an X-Correlation-Id header, and every error body repeats it as error.correlation_id. It is logged on our side and resolves to the exact log lines for your call.
Quote the correlation id in every support request. With it, we can usually diagnose a problem without a reproduction. Without it, we are guessing.
| Support channel | support@archiles.sk |
| Include | the correlation id, the endpoint and HTTP status, your clientReference and, for send problems, the submissionId |
| For a token-endpoint problem | those routes carry no correlation id, so send your client_id and the time of the call (with timezone) instead |
| Do not include | your client_secret, access tokens or refresh tokens |
A 422 on POST /v1/peppol/participants, or a 402 you believe is wrong, are support cases rather than retry cases.
14. Sandbox test data#
The test environment is a real Peppol test network, so a send needs a receiver that actually resolves in the test SMP. You get sandbox test data with your test credentials during onboarding rather than published here, because we refresh it periodically and a stale participant id inside a document is worse than none.
You receive:
| Item | What it is for |
|---|---|
| Sender participant id | a test DIČ your credential is entitled to act as |
| Receiver participant id | a test participant registered in the test SMP for BIS Billing 3.0 invoices |
| A receiver that is not registered | to exercise registered: false and the Undeliverable path |
| A sample BIS invoice that passes validation | a known-good baseline for step 3 of §12 |
| A sample BIS invoice that fails validation | with the rule it violates, to exercise your error handling |
Ask support (§13) if you need these reissued, or if you need a test case for a specific document shape or failure mode.