AP
Developer Portal
APEX AP Service — REST API & SAPI Documentation
Peppol Access Point • APEX API v1/v2 • SAPI-SK v1.0

APEX AP Developer Portal — complete documentation of the API and SAPI endpoints.

Technical reference for all 37 operations of the APEX Access Point: the APEX API for deep integration (single and batch sending, receiving, validation, precheck, events, responses, webhooks and MQTT, participants, reporting, licence) and SAPI for simplified access according to the Slovak SAPI-SK v1.0 specification. Verified against the APEX 26.4 source code.

APEX API
Sending, receiving, batches
Asynchronous sending with idempotency, batches of up to 5,000 documents, inbox v1/v2, status, events and responses.
SAPI
SAPI-SK v1.0
Send, receive, acknowledge — with its own authentication, a refresh token and synchronous checks of both the document and the recipient.
Participants
Lookup by DIČ / IČO
Partner lookup in Peppol (SML/SMP) by DIČ (Slovak tax ID) or IČO (company registration number), including the AS4 endpoint and certificate check.
Notifications & reporting
Webhooks, MQTT, statistics
Push notifications about received and sent documents, traffic statistics, EUSR/TSR and licence.
Portal navigation
Documentation structure
This documentation reflects APEX 26.4 (October 2026). The binding machine-readable contract is the OpenAPI in Swagger UI — /api-docs/api and /api-docs/sapi.
Integration paths

Two ways to connect to APEX

The APEX Access Point offers two equivalent ways to integrate. Your choice depends on where your system runs and which communication style you need. Both paths cover the full Peppol flow (sending and receiving) and you can also combine them.

A
Path A — direct REST integration

APEX REST API and SAPI

REST OAuth 2.0 JSON UBL 2.1 Webhooks / MQTT
👤 Who it is for
Developers of ERP, accounting, e-commerce and portal systems who want to build their own client and keep full control over the flow.
⚙️ How it works
HTTPS calls to APEX with an OAuth 2.0 Bearer token (client_credentials). JSON body, the UBL document as XML text. Sending is asynchronous (202 / ACCEPTED); you track the status by polling, webhooks or MQTT.
✅ Why choose this path
  • No additional infrastructure — an HTTPS client is enough
  • Immediate feedback: document validation and recipient check within the same request
  • Idempotent sending and batches of up to 5,000 documents
  • Easy testing on the sandbox via Swagger UI, Postman or curl
Open the API Reference →
B
Path B — managed messaging

QHUB (RabbitMQ message bus)

AMQPS RabbitMQ async DLQ Audit
👤 Who it is for
ERPs and portals in the ASOL ecosystem (e.g. QASIDA ERP, HOREC) and third-party systems that already use messaging (RabbitMQ / AMQP 0-9-1) and want an asynchronous integration without calling APEX directly.
⚙️ How it works
AMQPS connection to the QHUB RabbitMQ broker. You publish documents to be sent as a SynchronizationMessage with the PeppolDocument entity (30); received documents and the results of your sends (PeppolDocumentEvent, 32) arrive in your delivery queue. Registration and credentials are set up by ASOL or your partner.
✅ Why choose this path
  • Asynchronous flow — your system does not need to be available 24/7, messages wait in your queue
  • The result of every send as an event (queued, delivered, rejected, dead letter, MLS)
  • One integration layer for multiple ASOL products, customers and branches (routing by tenant and branch)
  • Audit of every message in the QHUB transaction table
Open the Integration Guide →
💡
Tip: You can combine both paths. For example: send via the REST API (with immediate validation from a UI action) and receive via QHUB (asynchronously, from your queue).
Environments

Environments, URLs and credentials

Develop and test your integration on the sandbox; you move to production by switching the URL and credentials. Both environments have the same API, the same authentication and the same limits.

Testing
Sandbox
API (base URL)
https://api.sandbox.apex.qasida.sk
Swagger UI
OpenAPI
/api-docs/api, /api-docs/sapi
Token (ASOL IAM)
https://iam.qasida.sk/oauth2/token
Peppol network
no — documents are delivered locally between sandbox participants
🧪The sandbox sends nothing to the Peppol network. APEX delivers a sent document locally to the recipient, who must be a participant of a tenant in the same sandbox with a valid licence (your own tenant included): the document arrives in the recipient's inbox with a DOCUMENTS_RECEIVED notification and the sender then receives an MLS. Instead of AS4 events you will see LOOPBACK_DETECTED and LOOPBACK_PROCESSING_COMPLETED. SML/SMP is not called: the lookup knows only sandbox participants and the precheck always reports the recipient as reachable. SAPI rejects an unknown recipient immediately (RECEIVER_NOT_FOUND); the APEX API and batches accept it and the document ends up in DEAD_LETTER once the attempts are exhausted (approx. 7.5 min).
Live operation
Production
API (base URL)
provided at onboarding together with your credentials
Swagger UI / OpenAPI
/swagger-ui, /api-docs/api, /api-docs/sapi
Token (ASOL IAM)
/oauth2/token of the ASOL IAM server specified in your credentials
Peppol network
yes — the OpenPeppol production network (AS4, SML/SMP)
🔐Production credentials are issued by ASOL (or your partner) after the contract is signed. They are bound to your organization (tenant) and its Peppol participants, which ASOL registers in the SMP — you can send only on behalf of these participants and only with a valid licence. Sandbox and production have separate data and separate credentials.
Credentials
An integration client in ASOL IAM (clientId + clientSecret), bound to one organization (tenant). Sandbox access also includes a test participant 0245:<DIČ> (DIČ = Slovak tax ID) and a sandbox licence; request it through ASOL or your partner.
OpenAPI and Swagger UI
Each environment publishes two OpenAPI documents — /api-docs/api (APEX API) and /api-docs/sapi (SAPI) — and Swagger UI at /swagger-ui with an Authorize button (OAuth 2.0 client_credentials). You can generate a client from the OpenAPI (OpenAPI Generator, NSwag, Kiota…).
Related services
QHUB integration guide — asynchronous integration via RabbitMQ.
Customer portal — service activation, licences and support.
Batch sending, MQTT and webhooks are part of the APEX API — there is nothing else to install.
Overview

Who this documentation is for

APEX AP provides two API groups: the APEX API (/api/v1, /api/v2) for deep integration and APEX SAPI (/sapi/v1), a simplified interface according to the SAPI-SK v1.0 specification. Both run on the same server and share data and tokens.

ERP integrator
Needs authentication, recipient lookup, validation, invoice sending and delivery tracking, and possibly batches and receiving documents.
Key sections: Quickstart, Sending, Receiving, Batch sending, Participants.
Backend developer
Deals with the OAuth 2.0 flow, idempotency, retry strategy, webhooks and MQTT, parsing the SBDH payload and error codes.
Key sections: Authentication, Webhooks & MQTT, Errors and limits, SAPI.
Support / operations
Quickly finds out a document's status, reads events and counterparty responses, monitors batches, statistics and licence usage.
Key sections: Events, Responses, Batches, Reporting & licence.
Quickstart

First integration in 4 steps

The technical path from the first request to sending and receiving an invoice via the APEX API. You can try every step on the sandbox.

01
Token
Obtain an access token from ASOL IAM with the client_credentials grant (lifetime per expires_in) and send it in the Authorization: Bearer <token> header. Request a new one before it expires.
Result: access to /api/** and /sapi/**.
02
Send
POST /api/v1/documents/send with the Idempotency-Key header (e.g. the invoice number). The recipient is set by receiverParticipantId (Slovak companies 0245:DIČ). Response 202 with status QUEUED and a documentId. On a timeout, retry with the same key.
Result: the document is queued for AS4 transfer.
03
Track delivery
Register a webhook (POST /api/v1/subscriptions) or MQTT, or poll GET /api/v1/documents/{id}/status. /events shows the history and /responses the counterparty's responses.
Result: DELIVERED, or a clear reason for the failure.
04
Receive
On a DOCUMENTS_RECEIVED notification, or periodically, call GET /api/v2/documents/inbox?status=VALIDATED, download the detail and, once it is stored, confirm it with POST /api/v1/documents/inbox/{id}/acknowledged.
Result: the supplier's invoice in your ERP.
💡
Optional before sending — not required for sending, mostly useful during development:
  • Recipient lookup: GET /api/v1/participants/lookup/dic?schema=0245&dic=2024000001 (or /lookup/ico?ico=…) tells you whether the customer is in the Peppol network and has a usable certificate.
  • Precheck: POST /api/v1/documents/precheck with the same body as send checks the document (XSD, EN 16931, Peppol BIS), whether the recipient exists and whether it supports the document type, without sending — the result is networkReady: true or a list of errors.
Reference

REST API Reference

All APEX API and SAPI endpoints: purpose, when to call them, inputs, outputs, behaviour and typical errors. Paths are relative to the environment's base URL (e.g. https://api.sandbox.apex.qasida.sk); every request except /sapi/v1/auth/token, /renew and /revoke requires a Bearer token.

Sending a single document to the Peppol network and tracking what happens to it. Sending is asynchronous: POST /send checks the document, stores it and queues it; the AS4 transfer runs in the background. Read the status via /status, the history via /events and the counterparty's responses via /responses — or have notifications sent to you (the Webhooks & MQTT tab).

POST v1 outbound

/api/v1/documents/send

Checks the document and the recipient, stores the payload and queues the document for AS4 sending. Response 202 Accepted with status QUEUED.
Purpose
Accepts a single UBL document (invoice, credit note, order…) and puts it in the sending queue. Before the response is returned, the document is validated (XSD, EN 16931 Schematron, Peppol BIS rules), the recipient is looked up via SML/SMP and the payload is stored. The AS4 transfer itself is done by a background worker. 202 means accepted, not delivered.
When to call
  • when sending an individual invoice from the ERP (user action or scheduled job)
  • after a successful /precheck, if you want to catch errors before sending
  • for tens to thousands of documents at once, use POST /api/v1/batches instead
Idempotency — the Idempotency-Key header
Optional, recommended. At most 200 characters, unique per document (e.g. the invoice number). The same key with the same content within 24 hours returns the original 202 response (the original documentId and submittedAt, status always QUEUED) with the Idempotent-Replayed: true header and queues nothing new — so a retry after a timeout never sends an invoice twice. Keys are separate for each tenant; the short and the canonical participant form count as the same content. The same key with different content (document or metadata) → 409 IDEMPOTENCY_CONFLICT; a concurrent request with the same key → 409 IDEMPOTENCY_IN_PROGRESS. A rejected request (400/403/500) does not consume the key.
Lifecycle after sending
QUEUED → PROCESSING → DELIVERED. If an AS4 transfer attempt fails, the document moves to RETRYING and the next attempts follow after 30, 60, 120 and 240 s; the fifth failure (or a permanent error, e.g. a recipient certificate not issued by the OpenPeppol CA) → DEAD_LETTER, usually within 8 minutes. A negative Peppol MLS from the receiving AP switches the document to FAILED — even if it was already DELIVERED. Licence not valid at the time of sending → REJECTED.
Typical errors
  • 400 — missing field (e.g. Receiver participant ID is required), failed document validation or a party mismatch (body with validationErrors[])
  • 401 — missing or invalid Bearer token
  • 403 — the sender is missing, is not registered or does not belong to your tenant; invalid licence
  • 409 — IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_PROGRESS
  • 500 — the document could not be queued (Failed to queue document: …); this also covers a recipient that is not in Peppol or does not accept the given document type — so check the recipient via /precheck or the lookup
Payload and parties
ublDocument is XML text (not Base64) — a bare UBL document or UBL in an SBDH envelope; APEX strips a leading UTF-8 BOM. Type and process must match the content (cbc:CustomizationID, cbc:ProfileID). Sender and recipient must match the cbc:EndpointID of the document's parties: supplier → customer; a self-billing invoice is sent by the customer and an order by the buyer. A mismatch returns SENDER_SUPPLIER_MISMATCH / RECEIVER_CUSTOMER_MISMATCH (or SENDER_CUSTOMER_MISMATCH / RECEIVER_SUPPLIER_MISMATCH). Schematron is skipped for documents over 5 MB.
Input fields (JSON)
Name
Description
Required
senderParticipantId
Sender's Peppol ID, in short (0245:2024000001) or canonical form (iso6523-actorid-upis::0245:2024000001)
required
receiverParticipantId
Recipient's Peppol ID; must be registered in Peppol (SML/SMP)
required
documentTypeId
Peppol document type identifier (listed in the Key concepts tab)
required
processId
Peppol process identifier, e.g. urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
required
ublDocument
UBL document as an XML string
required
Idempotency-Key
HTTP header, 1–200 characters
optional
Output fields (202)
Name
Description
documentId
Document UUID in APEX — the key for /status, /events, /responses and notifications
status
QUEUED
submittedAt
Time of acceptance (ISO 8601)
GET v1 outbound

/api/v1/documents/{documentId}/status

Current status of a sent document. It always returns the current status — use it for polling and whenever a notification seems to be out of order.
Purpose
Returns the status of a document sent via /send, a batch or SAPI (the SAPI providerDocumentId is the same UUID). Notifications have no guaranteed order, so this endpoint is the authoritative source of the status.
When to call
  • in a polling model after sending (a reasonable interval: seconds to tens of seconds)
  • after a notification that seems older than the last known status
  • when showing the status in a document list in the ERP
status values
QUEUED — waiting in the queue
PROCESSING — a worker is sending it via AS4 right now
DELIVERED — the receiving AP confirmed receipt (AS4 receipt)
RETRYING — an attempt failed, the next one is scheduled
DEAD_LETTER — attempts exhausted or a permanent error; intervention needed (support can requeue it once the problem is fixed)
FAILED — the receiving AP returned a negative MLS (can also follow DELIVERED)
REJECTED — rejected before sending, licence not valid at the time of sending
SENT — part of the code list, not used for sending

A document may temporarily go back from PROCESSING to QUEUED (e.g. when sending to an unreachable destination is interrupted) without a notification and without using up an attempt.
Typical errors
  • 404 (empty body) — the document does not exist, does not belong to your tenant or is a received document (status exists only for sent documents)
  • 401 — invalid token
Parameters
Name
Description
Required
documentId
UUID from the /send response (path)
required
Output fields
Name
Description
documentId
Document UUID
status
Current status (values on the left)
submittedAt
Time of the original acceptance
POST v1 validation

/api/v1/documents/validate

Validation of a UBL document without sending it: XML Schema → EN 16931 Schematron → Peppol BIS 3.0 rules. Always 200 — the result is in the valid field.
Purpose
Checks the document's syntax and rule compliance before you send it. /send performs the same validation (and additionally checks the parties, the licence and the recipient). If you omit documentTypeId, it is derived from the root element: CreditNote → BIS 3.0 credit note, otherwise BIS 3.0 invoice. Supported are BIS Billing 3.0 invoices and credit notes (including self-billing, PINT and XRechnung), orders, order responses, despatch advices, catalogues, MLR, IMR and MLS; for an unknown type it returns the error SCHEMATRON_001.
When to call
  • when developing and debugging invoice XML templates
  • when generating a document, before it is even saved in the ERP
  • when migrating or importing batches of documents
Result
valid is true only if all three layers pass; per layer: schemaValid, schematronValid, businessRulesValid. Each error has a level (ERROR/WARNING/INFO), a code (e.g. BR-07, PEPPOL-EN16931-R004), a message and a location (XPath). When parsing succeeds, the document's metadata is returned as well.
Typical errors
  • 400 — the body is missing or ublDocument is empty
  • 415 — the body is not application/json
  • an invalid document is not an HTTP error: 200 with valid: false
Input fields
Name
Description
Required
ublDocument
UBL document as an XML string
required
documentTypeId
Peppol document type; defaults to BIS 3.0 Invoice
optional
Output fields
Name
Description
valid
Overall result
schemaValid / schematronValid / businessRulesValid
Result per layer
errors[] / warnings[]
level, code, message, location
documentType, documentId, issueDate
Document type, document number, issue date
supplierName, customerName
Supplier and customer
totalAmount, currencyCode
Amount (string) and currency
POST v1 validation

/api/v1/documents/precheck

All the checks /send would perform, but without storing and queuing: document validation, the recipient's existence in Peppol and support for the document type. The result is networkReady.
Three checks in order
1. Document → valid (XSD, EN 16931, Peppol BIS)
2. Recipient exists → participantExists (SML/DNS finds the SMP)
3. Routing → canSend (the recipient's SMP offers documentTypeId + processId, an AS4 endpoint and a valid certificate)

networkReady = valid && participantExists && canSend
When to call
  • behind a “Check” button before an invoice is sent
  • when onboarding a new business partner
  • in bulk processing, to filter out problematic documents
Error categories and warnings
VALIDATION — document error or party mismatch (rule code, XPath)
PARTICIPANT — the recipient could not be found in Peppol (PARTICIPANT_NOT_FOUND; also reported when SML/SMP is temporarily unavailable)
ROUTING — the recipient does not support the type/process (DOCTYPE_NOT_SUPPORTED)

Warnings do not block sending, e.g. CERT_EXPIRING_SOON (the recipient's certificate expires within 30 days).
Performance, cache and errors
  • steps 2–3 use the recipient's SMP data, which APEX keeps for up to 5 days (regardless of document type); when it is refreshed, expect +200–800 ms — a recipient that has changed its registration in the meantime may only fail at sending
  • on the sandbox SML/SMP is not called: participantExists and canSend are always true
  • 200 is always returned, even when the checks fail; precheck does not check the licence
  • 400 — a required field is missing; 403 — missing sender or a sender of another tenant
Input fields
Name
Description
Required
senderParticipantId, receiverParticipantId, documentTypeId, processId, ublDocument
Same body as /send
required
Output fields
Name
Description
networkReady
The document can be sent
valid / participantExists / canSend
Result of the three checks
errors[] / warnings[]
category, code, message, location (null for PARTICIPANT/ROUTING)
checkedAt
Time of the check
GET v1 audit

/api/v1/documents/{documentId}/events

The complete chronological event history of a document (sent or received), oldest first.
Purpose
The recorded processing history: creation, validation, queuing, sending attempts, delivery, MLS, storage, links to responses and SK TDD (C5) reports. Unlike notifications, it is ordered by createdAt.
When to call
  • when investigating a delivery complaint (timeline)
  • when analysing retries: QUEUE_PROCESSING_STARTED / QUEUE_PROCESSING_FAILED pairs
  • when auditing a received document (MESSAGE_RECEIVED → MESSAGE_VALIDATED → MESSAGE_ACKNOWLEDGED)
eventType categories
Lifecycle — MESSAGE_CREATED, _RECEIVED, _VALIDATED, _DELIVERED, _FAILED, _ACKNOWLEDGED
Validation — VALIDATION_SUCCESS, VALIDATION_SCHEMA_ERROR, VALIDATION_ERROR
Queue — MESSAGE_QUEUED, QUEUE_PROCESSING_STARTED, QUEUE_PROCESSING_FAILED, QUEUE_DEAD_LETTER, QUEUE_MANUAL_REQUEUE, AS4_SEND_COMPLETED
Self-send — LOOPBACK_DETECTED, LOOPBACK_PROCESSING_COMPLETED/_FAILED (recipient in the same APEX; always on the sandbox)
Storage — BLOB_STORAGE_INITIATED/_COMPLETED/_FAILED
Responses — RESPONSE_LINKED, RESPONSE_UNLINKED
MLS — MLS_QUEUED, MLS_SENT, MLS_SEND_FAILED, MLS_SKIPPED_BY_TYPE, MLS_SKIPPED_NO_RECEIVER, MLS_RECEIVED, MLS_RECEIVED_NEGATIVE, MLS_RECEIVED_FROM_C5
TDD / C5 — TDD_CONVERSION_*, TDD_SUBMISSION_*, TDD_DEADLINE_*, TDD_SKIPPED_*
Other — DUPLICATE_DETECTED, PROCESSING_ERROR, TRANSPORT_ERROR

Treat unknown values (including those from older versions) as informational.
Typical errors
  • 404 — the document does not exist
  • empty array [] — no events yet
Parameters
Name
Description
Required
documentId
UUID of a sent or received document (path)
required
Output fields (each event)
Name
Description
id
Event UUID
messageId
documentId of the document the event belongs to
eventType
Category (on the left)
eventCode
Error or protocol code (e.g. EBMS-0010, BR-07)
eventMessage
Human-readable description (do not parse it)
technicalDetails
Endpoint, HTTP status, excerpt of the SMP response…
stacktrace
Only for unexpected errors, otherwise null
createdAt
Event time (ISO 8601 with offset)
GET v1 responses

/api/v1/documents/{documentId}/responses

Application responses exchanged for a document (MLR, IMR, Order Response, Receipt Advice and Peppol MLS), oldest first.
Both directions
For a document you sent: the counterparty's responses, including the Message Level Status (MLS) from the receiving provider. For a document you received: the responses you sent, including the MLS that APEX returned on your behalf. Only the first valid MLS from a participating provider is reported as the document's response (Peppol rule OP-MLS-02); any others are only audited.
Types and codes
MLR (BIS 36a / T71; the Peppol MLS is also returned with this type, you can tell it apart by the documentTypeId …urn:peppol:edec:mls…), IMR (Invoice Response 3.0 / T111), ORDER_RESPONSE (BIS 28a), RECEIPT_ADVICE (BIS 30a)
Codes: AP accepted, AB accepted without confirmation (MLS), RE rejected, CA conditionally accepted, UQ under query, IP in process, PD paid (IMR)
Issues
For RE and CA, issues[] contains the individual reasons from the response: type (reason code, usually BV business / SV syntax), code, description, severity (FATAL for RE, otherwise WARNING) and xpath (usually null). AP with an empty list = clean acceptance.
Return values
  • [] — no response yet (common right after sending)
  • [] — also when the documentId is itself a response
  • 404 — the document is not yours or does not exist
Parameters
Name
Description
Required
documentId
UUID of a sent or received document (path)
required
Output fields (each response)
Name
Description
responseId
Response UUID (for downloading the XML)
responseType
MLR, IMR, ORDER_RESPONSE, RECEIPT_ADVICE
responseCode / responseCodeDescription
AP/RE/CA/UQ/IP and its description
documentTypeId
Document type of the response from the AS4 header
senderParticipantId
Who sent the response
note
Free text of the response (e.g. the reason for rejection)
issues[]
List of issues (on the left)
receivedAt
Time the response was received via AS4
GET v1 responses

/api/v1/documents/{documentId}/responses/{responseId}

Raw XML of a single response (UBL ApplicationResponse), Content-Type: application/xml.
Purpose
Downloads the XML of a response from the /responses list — a business response from the counterparty (IMR/MLR), a response you sent, or an MLR that APEX generated and sent to the sender's AP (C2).
Typical errors
  • 404 — the responseId does not belong to the given document (even if both IDs are the same)

Documents that reached you from the Peppol network via AS4. Pull model: list → detail → acknowledgement. You learn about a new document by polling the list or from a DOCUMENTS_RECEIVED notification (webhook / MQTT). The endpoints exist in version v1 (identifiers in the canonical form iso6523-actorid-upis::0245:…) and v2 (short Peppol ID 0245:…). We recommend v2 for new integrations; the acknowledgement exists only in v1.

GET v2 inbound

/api/v2/documents/inbox

Paginated list of received documents, newest first (receivedAt DESC). Sender and recipient in the short form 0245:2024000001.
Purpose
The integration surface for an ERP that pulls received documents. The list does not contain XML (ublDocument is empty) — you get the content from the detail. Reading the list does not change the status of documents.
Typical polling
  • call it regularly with status=VALIDATED — these are the documents waiting to be downloaded
  • download each new document via GET /api/v2/documents/inbox/{documentId}
  • once it is stored permanently in the ERP, call POST /api/v1/documents/inbox/{documentId}/acknowledged — it drops out of the next poll
Statuses of a received document
VALIDATED — the document passed validation and is waiting to be downloaded (DOCUMENTS_RECEIVED is sent)
ACKNOWLEDGED — your system has confirmed receipt
REJECTED — the document failed validation (it is in the list, but without a notification)
FAILED — processing error
RECEIVED — only an internal intermediate state during receipt, you will not see it in the list
Pagination and filter
offset / limit (default 0 / 20; we recommend at most 100); the response carries total — the number of records matching the filter. status is case-sensitive; an unknown value returns 400.
Query parameters
Name
Description
Required
status
VALIDATED, ACKNOWLEDGED, REJECTED, FAILED
optional
offset
Number of records to skip (default 0)
optional
limit
Page size (default 20)
optional
Output fields
Name
Description
items[]
documentId, senderParticipantId, receiverParticipantId, documentTypeId, processId, receivedAt, status
total
Number of documents matching the filter
offset, limit
Pagination values used
GET v2 inbound

/api/v2/documents/inbox/{documentId}

Detail of a received document including the whole payload — StandardBusinessDocument (an SBDH envelope with the UBL document inside).
Payload
The ublDocument field contains the complete StandardBusinessDocument exactly as it arrived over the Peppol network — the root is the SBDH envelope, not the invoice. Validating it directly against the UBL Invoice XSD will therefore fail; find the business document by its namespace anywhere in the tree. Identifiers inside the payload are not rewritten; the short form applies only to the JSON fields.
Parse with namespaces in mind
Prefixes (cac:, cbc:…) are not part of the contract and may change between versions and even within a single document. Bind to the namespace URIs and local names from UBL 2.1 and Peppol BIS Billing 3.0.
After downloading
Once the document is stored permanently in your system, acknowledge it via POST /api/v1/documents/inbox/{documentId}/acknowledged. Downloading by itself does not change the status. If the ublDocument field is missing from the response, the payload could not be loaded at that moment — do not acknowledge the document and try again later.
Typical errors
  • 404 (empty body) — the document does not exist, is not addressed to your tenant or is not a received document
  • 401 — invalid token
Parameters
Name
Description
Required
documentId
UUID of the received document (path)
required
Output fields
Name
Description
documentId
Document UUID
senderParticipantId
Sender, short form (0245:1234567890)
receiverParticipantId
Recipient, short form
documentTypeId, processId
Peppol document type and process
receivedAt
Time of receipt
status
Document status
ublDocument
The whole StandardBusinessDocument (XML string)
GET v1 inbound

/api/v1/documents/inbox  ·  /api/v1/documents/inbox/{documentId}

The original version of the list and the detail. The same parameters, pagination, statuses, payload and HTTP codes as v2 — only the identifier format differs.
Difference between v1 and v2
v1: senderParticipantId / receiverParticipantId in the full canonical ISO 6523 form — iso6523-actorid-upis::0088:1234567890123.
v2: short Peppol ID — 0088:1234567890123, i.e. the same form you send in requests and that /participants/lookup returns.
Compatibility
The v1 format is frozen and will not change. Both versions read the same data, so you can combine them. A new API version is always introduced under a new path; a published version never changes in a backward-incompatible way.
POST v1 inbound

/api/v1/documents/inbox/{documentId}/acknowledged

Confirms that your system has downloaded and processed a received document. The status changes to ACKNOWLEDGED. Response 204 No Content.
When to call
  • only once the document is stored permanently (ERP import, database commit)
  • if processing failed, do not acknowledge — the document stays in its original status for the next attempt
  • applies to both v1 and v2 integrations (the acknowledgement carries no participant identifier, so it has no v2 variant)
Idempotency and errors
Acknowledging an already acknowledged document again has no effect and returns 204 again. A received document can be acknowledged in any status (including REJECTED, if you want to mark it as handled); the acknowledgement is recorded in the events as MESSAGE_ACKNOWLEDGED and also applies to SAPI.
  • 404 — the document does not exist or does not belong to your tenant
  • 401 — invalid token

Tens to thousands of documents in a single request (monthly invoicing, migration). A batch accepts up to 5,000 documents and responds 202 immediately. Each document then goes through exactly the same checks in the background as POST /api/v1/documents/send and is queued individually. Batch documents are sent only after documents sent individually, and tenants take turns in the queue, so a large batch does not slow down normal operation.

POST v1 new batch

/api/v1/batches

Accepts a batch of documents — JSON with metadata or a ZIP archive of UBL files. Response 202 with a batchId and a Location header.
How it works
202 means that APEX has accepted the batch, not that the documents are valid. The documents are independent: an invalid document does not stop the others, it ends up REJECTED with a reason. Track the result via GET /api/v1/batches/{batchId} (no more often than once every 10 s) or the BATCH_PROCESSED notification.
Limits
  • 1 to 5,000 documents, at most 100 MB per request, 10 MB per document
  • ZIP: additionally at most 100 MB uncompressed and 10,000 archive entries
  • at most 3 open batches (RECEIVED/PROCESSING) per tenant
  • documents count towards the monthly volume of the licence
Idempotency
Always send an Idempotency-Key (1–200 characters, e.g. the ERP run ID). The same key and a byte-identical body → the original 202 with Idempotent-Replayed: true, no new batch is created. A different body → 409 IDEMPOTENCY_CONFLICT. A batch key does not expire. Store the body before sending and send the same bytes when retrying (curl --data-binary).
ZIP batch
Content-Type: application/zip (also application/x-zip-compressed); each file = one document in UTF-8 (bare UBL or UBL in SBDH). The file path in the archive is the document's clientReference (name the files after the document number). The batch name and notification mode go in the clientReference and notify query parameters. APEX derives the metadata from the document: the type from the root element + cbc:CustomizationID, the process from cbc:ProfileID, the parties from cbc:EndpointID (supplier → customer; reversed for self-billing; buyer → seller for an order). A file whose metadata cannot be derived ends up REJECTED with INVALID_METADATA.
Rejection of the whole batch
  • 400 BATCH_INVALID_REQUEST — not a JSON object/ZIP, documents or a required field is missing (the message gives the path, e.g. documents[17].processId), duplicate clientReference, unknown notify
  • 400 INVALID_IDEMPOTENCY_KEY
  • 403 PARTICIPANT_NOT_AUTHORIZED / LICENCE_INVALID
  • 409 IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_PROGRESS
  • 413 BATCH_TOO_LARGE
  • 429 TOO_MANY_OPEN_BATCHES / BATCH_INTAKE_BUSY (retry after Retry-After)
Input fields (JSON)
Name
Description
Required
clientReference
Your label for the batch (max 200 characters)
optional
notify
BATCH (default), DOCUMENT, BOTH
optional
documents[]
1 to 5,000 documents
required
documents[].clientReference
Document label (e.g. the invoice number), unique within the batch — we recommend always setting it
optional
documents[].senderParticipantId
Sender; must belong to your tenant
required
documents[].receiverParticipantId
Recipient
required
documents[].documentTypeId, processId
As for /documents/send
required
documents[].ublDocument
UBL document as XML text (max 10 MB)
required
Output fields (202)
Name
Description
batchId
Batch UUID
clientReference
As you sent it
status
Always RECEIVED
itemCount
Number of documents
receivedAt
Time of acceptance
💡Send the Expect: 100-continue header — a rejection (429, 403, 400) then arrives before the body is uploaded. Do not send batches of one tenant in parallel, and do not send the same invoice both in a batch and via /documents/send — two would go out.
GET v1 new batch

/api/v1/batches/{batchId}

Batch status: document counts by intake status (items) and live delivery counts of the resulting messages (messages).
Batch status
RECEIVED — accepted, processing has not started
PROCESSING — the documents are being validated and queued
PROCESSED — every document is ACCEPTED, REJECTED or CANCELLED; delivery of the accepted ones continues (see messages)
CANCELLED — cancelled by the client
items vs. messages
items = intake of the documents: pending, processing, accepted, rejected, cancelled.
messages = delivery of the messages created from accepted documents, the same statuses as /documents/{id}/status: queued, processing, delivered (including the transitional SENT), retrying, failed, rejected, deadLetter. The sum of messages = accepted.
Typical errors
  • 404 BATCH_NOT_FOUND — does not exist or belongs to another tenant
Output fields
Name
Description
batchId, clientReference, notify, itemCount
Batch identity and settings
status
Batch status (on the left)
items{}
Document counts by intake status
messages{}
Message counts by delivery status
receivedAt, processedAt, cancelledAt
Timestamps; processedAt is null until all documents have been processed
GET v1 new batch

/api/v1/batches/{batchId}/items

The batch documents in request order, optionally only those in one status — e.g. status=REJECTED for what needs fixing.
Rejection reasons (errorCode)
DOCUMENT_VALIDATION_FAILED — fix according to errors[]
INVALID_METADATA — metadata (for ZIP, what is missing in the document: DOCUMENT_NOT_UTF8, DOCUMENT_NOT_XML, DOCUMENT_TYPE_UNSUPPORTED, CUSTOMIZATION_ID_MISSING, PROFILE_ID_MISSING, SENDER_ENDPOINT_MISSING, RECEIVER_ENDPOINT_MISSING, METADATA_INVALID)
RECEIVER_NOT_FOUND — the recipient is not in Peppol
RECEIVER_DOCUMENT_TYPE_UNSUPPORTED — the recipient does not accept the type
PARTICIPANT_NOT_AUTHORIZED — the sender is not yours
LICENCE_INVALID — the licence has expired in the meantime
BATCH_ITEM_RETRIES_EXHAUSTED — a temporary error lasted longer than approx. 45 min (8 attempts), send it again
Batch document statuses
PENDING — waiting (with errorMessage filled in, it is waiting for another attempt after a temporary error)
PROCESSING — being validated and queued right now
ACCEPTED — queued; use the documentId for /documents/{id}/status, /events, /responses
REJECTED — rejected, see errorCode
CANCELLED — cancelled with the batch
Parameters
status (optional), page from 0, size default 100, at most 1,000. Errors: 400 invalid status/pagination, 404 BATCH_NOT_FOUND.
Output fields (content[])
Name
Description
seqNo
Position in the request (from 1)
clientReference
Your document label (the file path for ZIP)
receiverParticipantId
Recipient (missing if it could not be derived from the ZIP file)
status
Batch document status
documentId
Message UUID, if ACCEPTED
errorCode, errorMessage, errors[]
Rejection reason; errors[] = code, message, location
attempts, processedAt
Number of temporary failures, processing time
Page envelope
Name
Description
content[]
Documents on the page
page, size
Page number (from 0) and size
totalElements
Number of documents matching the filter
GET v1 new batch

/api/v1/batches

List of the tenant's batches, newest first; optionally only those accepted in the interval [from, to) or in one status.
Parameters
from, to (ISO 8601 date-time), status (RECEIVED, PROCESSING, PROCESSED, CANCELLED), page from 0, size default 20, at most 1,000.
Response
content[] with batchId, clientReference, status, notify, itemCount, receivedAt, processedAt, cancelledAt; plus page, size, totalElements. Document counts are in the batch detail. Error 400 for an invalid status or pagination.
POST v1 new batch

/api/v1/batches/{batchId}/cancel

Cancels documents that have not been queued yet. Documents already accepted (ACCEPTED) are on their way and are not recalled.
Behaviour
Both PENDING documents and a document currently being validated are cancelled (the latter ends up CANCELLED shortly, so the response may still show processing > 0). The batch moves to CANCELLED, no longer counts towards the limit of 3 open batches, and no BATCH_PROCESSED is sent for it. The response is the batch status (as for GET /batches/{batchId}).
Typical errors
  • 404 BATCH_NOT_FOUND
  • 409 BATCH_NOT_CANCELLABLE — the batch is already PROCESSED or CANCELLED
Batch notifications
You choose the mode with the notify field. BATCH_PROCESSED goes to every active webhook and MQTT queue of the tenant; webhook filters do not apply to it. It carries no list of documents, only counts — load the documents via /items.
notifyBATCH_PROCESSEDNotifications for individual documents
BATCH (default)yesproblems only: QUEUE_PROCESSING_FAILED, QUEUE_DEAD_LETTER, MLS_RECEIVED_NEGATIVE
DOCUMENTnoall, as for individual sending
BOTHyesall

Looking up a business partner in the Peppol network by DIČ (Slovak tax ID) or another identifier with a scheme, or by IČO (company registration number). The result tells you whether the partner is registered in Peppol, through which AS4 endpoint it receives and whether its access point's certificate is usable. For Slovak entities use the scheme 0245 (SK:DIČ) — the Peppol ID has the form 0245:2024000001.

GET v1 participants

/api/v1/participants/lookup/dic

Lookup by DIČ (Slovak tax ID) or any identifier with an ICD scheme. Local record; for an outdated or missing record, a live check in SML and SMP.
Composition of the Peppol ID
Peppol ID = <schema>:<dic>, full identifier iso6523-actorid-upis::<schema>:<dic>. The values are used exactly as you send them — no trimming, no case change, no removal of the SK prefix and no format check. 2024000001 and SK2024000001 are two different identifiers.
Order of evaluation
1. Local participant record.
2. On the sandbox the local record is returned (source=LOCAL), without one 404; SML/SMP is not called.
3. A record with SMP metadata younger than the TTL (5 days by default) → source=LOCAL.
4. An older, never verified or missing record → live check in SML and SMP:
  • SML reports that the participant is not registered → 404 (even if a local record exists)
  • SML/SMP does not respond → local record (LOCAL), without one 503
  • SMP publishes an AS4 endpoint for BIS Billing 3.0 Invoice → source=SMP with the endpoint and certificate
  • SMP has no Invoice record / AS4 endpoint for the participant → source=SMP with the stored endpoint or null
Certificate validity — certificate.valid
true — the certificate published in the SMP passed a full check during the request: validity period, issuer chain and revocation status, to which a CRL or OCSP actually responded.
false — the certificate is definitely unusable, the reason is in invalidReason: EXPIRED, NOT_YET_VALID, REVOKED, UNTRUSTED_ISSUER (not issued by the OpenPeppol CA). Such an endpoint cannot be delivered to.
null — the status was not determined in this request (certificate from the local record, or a soft-fail when CRL/OCSP is unavailable). A soft-fail never yields true.
valid and invalidReason are always present in the JSON, even with the value null.
Typical errors
  • 400 — schema or dic is missing
  • 404 — not registered in Peppol, or an empty value (no body)
  • 503 — SML/SMP does not respond and no local record exists — try again later
Query parameters
Name
Description
Required
schema
ICD scheme code, e.g. 0245 (SK DIČ), 9950 (SK VAT number, IČ DPH), 9929 (CZ VAT), 9914 (AT VAT), 0088 (GLN)
required
dic
Identifier value; a Slovak DIČ = 10 digits without the SK prefix
required
Output fields
Name
Description
source
LOCAL (local record) or SMP (live lookup in this request)
participantId
Full identifier iso6523-actorid-upis::0245:…
peppolId
Short form 0245:2024000001
organizationName
Name from the local record; null for a participant known only from the SMP
countryCode
ISO 3166-1 alpha-2; for SMP derived from the scheme (0245 → SK); ZZ = unknown
endpointUrl
AS4 endpoint of the partner's access point (BIS Billing 3.0 Invoice)
smpUrl
The partner's SMP server
enabled
Flag of the local record; null if no record exists
certificate
subject, issuer, serial, fingerprint (SHA-256), notBefore, notAfter, expired, valid, invalidReason
smpMetadataUpdatedAt
Time of the last successful verification in the SMP
GET v1 participants

/api/v1/participants/lookup/ico

Lookup by IČO (company registration number): IČO → DIČ (Slovak tax ID) via the Register of Financial Statements (registeruz.sk) → then exactly like /lookup/dic.
Procedure
1. The IČO (without surrounding whitespace, otherwise unchanged) is sent to registeruz.sk to obtain the DIČ. The result is not cached — every request queries the register.
2. The register does not know the accounting entity, the entity has no DIČ, or the register returns 4xx (except 429) → 404.
3. The register is unavailable, does not respond within 15 s, returns 5xx/429 or an unreadable response → 503.
4. The Peppol ID is built from the obtained DIČ and schema and evaluated in the same way as /lookup/dic.
When to call
  • your ERP knows only the business partner's IČO
  • when automatically filling in the Peppol identity in the partner directory
Typical errors
  • 400 — ico is missing
  • 404 — the IČO could not be translated to a DIČ or the participant is not in Peppol
  • 503 — registeruz.sk or SML/SMP is unavailable
Query parameters
Name
Description
Required
ico
IČO, usually 8 digits; no format check
required
schema
Scheme for building the Peppol ID from the DIČ; default 0245
optional
Output fields
Name
Description
—
The same object as /lookup/dic

Push notifications instead of polling: about a newly received document and about events of the documents you sent. Two channels — HTTPS webhook (filterable, multiple subscriptions) and MQTT topic (one per tenant, all notifications). A notification only identifies the document; you download the content via the API. Delivery is best-effort, at most once and with no guaranteed order: always take the document status from the status field, never from eventType.

Notification — format and events
One notification = one document and one event (documentIds always has exactly one element; the array form is kept for compatibility). Fields with the value null are omitted from the JSON.
FieldMeaning
eventTypeWhat triggered the notification — not the document status
subscriptionIdThe subscription it is delivered for (missing for MQTT)
documentId / documentIdsDocument UUID / the same as a single-element array
receivedCountAlways 1, only for DOCUMENTS_RECEIVED
directionIN received, OUT sent document
statusDocument status at the time the notification was sent
messageIdMessage ID; for a received document, the AS4 Message ID from the sending AP
conversationIdConversation ID of the document (if any)
senderParticipantId, receiverParticipantIdPeppol ID including the scheme (iso6523-actorid-upis::…)
documentTypeId, processIdPeppol document type and process
eventMessageHuman-readable detail, do not parse it
notifiedAtWhen the notification was built. Webhook: a number, Unix epoch seconds with a fraction (1776168005.123456000); MQTT: an ISO 8601 string
batchOnly for BATCH_PROCESSED: batchId, clientReference, status, itemCount, accepted, rejected, cancelled
eventTypeDirectionWhen
DOCUMENTS_RECEIVEDINA received document passed validation and is stored (VALIDATED); not sent for a rejected or duplicate document
MESSAGE_QUEUEDOUTYour document was accepted and queued
QUEUE_PROCESSING_STARTEDOUTA sending attempt started (for every attempt; not for self-send)
MESSAGE_DELIVEREDOUTDelivered to the receiving AP (or locally for self-send)
QUEUE_PROCESSING_FAILEDOUTAn attempt failed, the next one is scheduled
QUEUE_DEAD_LETTEROUTSending given up, the document is DEAD_LETTER
MLS_RECEIVEDOUTA positive Peppol MLS arrived
MLS_RECEIVED_NEGATIVEOUTA negative MLS arrived; the document is FAILED (even after DELIVERED)
BATCH_PROCESSED—The batch has been processed (according to notify)
Order and document status
Notifications are sent asynchronously and independently, even from different APEX instances, so their order is not guaranteed, not even for a single document — MESSAGE_DELIVERED may arrive before MESSAGE_QUEUED. status is the status read when the notification was sent; it may be newer than the event (e.g. MESSAGE_QUEUED with status DELIVERED), and a notification with an older status may arrive after a newer one. So do not treat the last notification received as the current status; when in doubt, call GET /api/v1/documents/{documentId}/status. Not every status change is notified (e.g. REJECTED for an invalid licence).
Delivery guarantees
Best-effort, at-most-once: each notification is sent once. A non-2xx response, a timeout or a connection error is only logged — no retry and no redelivery. Notifications waiting to be sent are held only in memory and may be lost when an instance stops. A failed webhook never affects sending or receiving a document. So build reliability on a combination of notifications and periodic polling (/inbox, /status).
POST v1 webhooks

/api/v1/subscriptions

Registers an HTTPS webhook. APEX sends a POST to it for every document and event that matches the filters. Response 201 Created.
Requirements for your endpoint
The URL must be https:// and reachable from the APEX server (reachability is not checked at registration, redirects are not followed). Connection timeout 2 s and response timeout 2 s — return 2xx as soon as possible (ideally < 500 ms) and do heavy processing asynchronously. A tenant's subscriptions are called one after another for a single event, so a slow endpoint delays the others. The request has Content-Type: application/json, Accept: application/json and User-Agent: Apex-Webhook/1.0. The body is not signed — use customHeaders (e.g. a secret token) to verify that the call comes from APEX.
Filters
documentTypeFilters — list of document types, exact match; applies to both received and sent documents.
senderParticipantFilters — list of senders, exact match on the canonical identifier as it appears in the notification's senderParticipantId (e.g. iso6523-actorid-upis::0245:1234567890; the short form does not match); received documents only.
Empty list = no restriction. There is no filter by event type; BATCH_PROCESSED ignores the filters.
Limits and customHeaders
At most 3 webhooks per tenant (active and paused combined). At most 10 headers; name max 100 characters, only [A-Za-z0-9-_], value max 1,000 characters without line breaks. Host, Content-Length, Content-Type and Transfer-Encoding cannot be overridden (Accept and User-Agent can). A header that does not meet the rules is silently dropped — check customHeaders in the response.
Typical errors
  • 400 — name/url is missing, the URL is not https://, too many headers (Problem Details with detail: "Invalid request content.")
  • 409 — the tenant already has 3 webhooks
  • 401 — invalid token
Input fields
Name
Description
Required
name
Subscription name (max 100 characters)
required
description
Description (max 500 characters)
optional
url
HTTPS URL (max 2,048 characters)
required
active
Active once saved (default true)
optional
documentTypeFilters[]
Document type filter
optional
senderParticipantFilters[]
Sender filter (received documents only)
optional
customHeaders{}
Custom HTTP headers
optional
Output fields
Name
Description
subscriptionId
Subscription UUID
name, description, url, active
Stored values
documentTypeFilters, senderParticipantFilters
Stored filters
customHeaders
Stored headers
createdAt, updatedAt
Time of creation and of the last change
GET v1 webhooks

/api/v1/subscriptions

Paginated list of the tenant's webhooks, most recently created first.
Parameters
offset (default 0), limit (default 20, 1–100).
Response
items[] (objects as on creation), total (all of the tenant's subscriptions regardless of the page), offset, limit. Errors 400 (limit < 1), 401.
GET PUT DELETE v1 webhooks

/api/v1/subscriptions/{subscriptionId}

Detail, full replacement and deletion of a single webhook.
GET — detail
Returns the subscription (the same object as on creation). 404 if it does not exist or belongs to another tenant.
PUT — full replacement
Body as for POST; all fields are overwritten (not a PATCH). To pause temporarily without losing the settings, send active: false. The change applies to notifications sent after it is saved. Response 200 with the updated subscription; 400 for an invalid body, 404.
DELETE — deletion
Irreversibly removes the subscription; no further notifications are sent to its URL. Response 204; 404 if it does not exist. If you only want to stop temporarily, use PUT with active: false.
POST v1 MQTT

/api/v1/subscriptions/queue

Creates the tenant's MQTT access: a broker user and a topic to which APEX publishes all of the tenant's notifications. The credentials are shown only once.
How it works
One MQTT access per tenant. The topic (queueName, e.g. /apex/inbox/apex-sk2024000001) receives notifications for both received and sent documents, even though its name contains inbox. Webhook filters and the active flag do not apply to it. A message has the same format as a webhook, without subscriptionId and with notifiedAt as an ISO 8601 string.
Connection
Any MQTT 3.1.1 client using the fields host, port, username, password (or mqttUrl, which always has the mqtt:// scheme — whether the broker requires TLS depends on the environment). Subscribe to the queueName topic literally, including the leading slash and without wildcards (#, +) — the broker user is authorized for exactly this topic only. APEX publishes with QoS 1, without retain; whether messages published during a connection outage are kept for you depends on the broker and on your MQTT session settings (clean / persistent session).
Security
password and the full mqttUrl are returned only in this response; the server stores only a hash of the password. If you lose the password, delete the access and create a new one — a new password is generated.
Typical errors
  • 409 — MQTT access already exists for the tenant (delete it first)
  • 400 — the access could not be set up on the broker; try again later
  • 401
Output fields (201)
Name
Description
queueId
UUID of the access record
mqttUrl
Full MQTT URL with credentials (one-time)
amqpUrl
Backward-compatible alias of mqttUrl — do not use it in new integrations
host, port
Broker
vhost
Logical namespace of the broker (informational only for MQTT)
username, password
Credentials (password one-time)
queueName
The tenant's MQTT topic
createdAt, securityWarning
Time of creation, security warning
DELETE /api/v1/subscriptions/queue
Deletes the tenant's MQTT access: the broker user is removed immediately (active connections are dropped) and publishing to the topic stops. You can then create a new access with new credentials. Response 204; 404 if no access exists.

An overview of the tenant's traffic (statistics of sent and received documents), the history of EUSR/TSR statistics reports to OpenPeppol and the licence status with monthly document usage.

🚧Reporting is being rolled out. In /reporting/statistics the live counts delivered, failed, topRecipients, topSenders and failedDocuments are reliable; the aggregated total, byDocumentType, byCountry and deliveryRate are still zero or empty. /reporting/submissions does not return any records yet. For exact figures about your documents, use /api/v1/licenses/info (monthly usage) and your own record of statuses.
GET v1 reporting

/api/v1/reporting/statistics

Aggregated statistics for a period: sent and received documents, delivery success rate, breakdown by document type and country, top partners and documents in an error state.
Period
period=month / quarter / year — the current calendar month, quarter, year.
from + to (both, YYYY-MM-DD) — a custom interval; period is ignored and the response has type=custom. If you send only one of them, it is ignored.
No parameters — the current month. The response always states the exact range used in period.
When to call
  • an operations dashboard in an ERP or portal
  • regular reports for management
  • monitoring trends and delivery failure rates
Meaning of the numbers
sent / received: total, delivered, failed (FAILED + DEAD_LETTER), deliveryRate (0.0–1.0, null when zero), byDocumentType (keys INVOICE, CREDIT_NOTE, ORDER, ORDER_RESPONSE, DESPATCH_ADVICE…), byCountry (country of the counterparty).
The top-level deliveryRate counts only sent documents.
topRecipients / topSenders — at most 10 partners (name, peppolId in canonical form, countryCode, count).
failedDocuments — documents currently in an error state (regardless of the period, both directions): total, deadLetters, retrying, rejected.
Typical errors
  • 400 — from after to or an unknown period
  • 401 / 403
GET v1 reporting

/api/v1/reporting/submissions

History of monthly statistics reports to OpenPeppol: EUSR (End User Statistics Report) and TSR (Transaction Statistics Report), newest first.
Background
Every Peppol Access Point must report statistics to OpenPeppol monthly: EUSR counts end-user transactions, TSR counts transactions by document type and direction. The reports are generated and submitted by APEX as the access point; nothing is required from you. The endpoint is ready for auditing submitted reports, but for now it returns an empty list.
Statuses
PENDING — generated, not submitted
SUBMITTED — submitted, waiting for confirmation
FAILED — submission failed (errorMessage), it will be retried
VALIDATED — OpenPeppol accepted the report
Parameters
reportType (EUSR/TSR), status, offset (default 0), limit (default 20, 1–100). E.g. ?reportType=EUSR&status=FAILED.
Response
items[]: id, reportType, periodStart, periodEnd, reporterId, status, errorMessage, submittedAt, validatedAt, createdAt; plus total, offset, limit. Error 400 for an unknown filter value.
GET v1 licence

/api/v1/licenses/info

The tenant's licence and usage of the monthly document count.
Counting documents
The maxDocuments limit is monthly (-1 = unlimited). All counts in documentUsage refer to the current calendar month in UTC (periodStart–periodEnd). usedDocuments = sent + received, every stored message regardless of status (including QUEUED and FAILED).
Exceeding the limit
With limitExceeded: true, sending and receiving keep working — it is a signal to upgrade the licence. The counters reset at the start of the next month. An invalid licence (valid: false), however, blocks sending (REJECTED or 403 LICENCE_INVALID).
Response
serialNumber, companyName, branchName, licenseTypeCode, validFrom, validTo (format 2026-01-01T00:00:00.000+0000), valid, autoProlong and documentUsage: maxDocuments, usedDocuments, remainingDocuments (-1 = unlimited), usagePercentage (null for an unlimited licence), limitExceeded, sentDocuments, receivedDocuments, periodStart, periodEnd.
Typical errors
  • 404 (empty body) — the tenant has no active licence; an expired licence returns 200 with valid: false
  • 401

Authentication according to SAPI-SK v1.0. APEX forwards the request to ASOL IAM (OAuth 2.0 client_credentials) and adds its own refresh token (30 days, rotated on every use) to the IAM access token, so the client does not have to send the client_secret on every renewal. An access token from SAPI is also valid for the APEX API (/api/**) and vice versa. The fields of these endpoints are in snake_case; /token, /renew and /revoke are public (no Bearer token).

POST v1 SAPI auth

/sapi/v1/auth/token

Exchanges client_id + client_secret for an access token (JWT RS256 from ASOL IAM) and an APEX refresh token.
Purpose
Sign-in of the integration client. The body is JSON (Content-Type: application/json; application/x-www-form-urlencoded returns 415). APEX forwards the credentials to IAM and returns its access token together with a new refresh token. The access token lifetime is given by the expires_in field.
When to call
  • when the integration client starts
  • when the refresh token has expired or been invalidated
  • do regular renewals via /renew
Security
Client credentials are bound to one organization (tenant) — do not share them between systems and do not store them in source code. Never send tokens in a URL. The scope parameter is optional and is forwarded to IAM; access to documents and participants is determined by the tenant the client credentials are bound to.
Typical errors
  • 400/401 SAPI-AUTH-001 — IAM rejected the credentials (the status comes from IAM)
  • 400 — unreadable JSON body (Problem Details)
  • 415 — a Content-Type other than JSON
  • 500 SAPI-AUTH-999 — IAM unavailable or an internal error; try again later
Input fields (JSON)
Name
Description
Required
client_id
Client identifier
required
client_secret
Client secret
required
grant_type
client_credentials
required
scope
Scope for IAM (access is determined by the tenant)
optional
Output fields (200)
Name
Description
access_token
JWT (RS256) from ASOL IAM for the Authorization: Bearer header
token_type
Bearer
expires_in
Access token lifetime in seconds
scope
Scope from IAM or null
refresh_token
APEX refresh token, valid for 30 days; always present
GET v1 SAPI auth

/sapi/v1/auth/token/status

Information about the current access token from the Authorization header: validity, remaining time and a recommendation to renew.
Purpose
Diagnostics and planning of token renewal. Requires a valid Bearer token (the X-Peppol-Participant-Id header is not needed). should_refresh is true when less than 3 minutes remain until expiry.
When to call
  • in the integration client's health check
  • when debugging authentication problems
  • it is simpler to plan renewal directly from expires_in
Typical errors
  • 401 SAPI-AUTH-002 — the token is missing, invalid or expired
Output fields
Name
Description
valid
With 200, always true
token_type
access
client_id
Token subject (the sub claim; a UUID for a client token)
issued_at, expires_at
Time of issue and expiry (UTC)
expires_in_seconds
Remaining seconds
should_refresh
Less than 3 minutes until expiry
refresh_recommended_at
Recommended renewal time (expiry − 3 min)
POST v1 SAPI auth

/sapi/v1/auth/renew

A new access token and a new refresh token in exchange for the current refresh token. Rotation — the used refresh token is invalidated immediately.
Rotation and validity
Each call invalidates the submitted refresh token and issues a new one valid for 30 days from the renewal (so a client that renews regularly never needs to sign in again). Always store the new refresh token — the old one no longer works. If the renewal fails in IAM, the submitted refresh token has already been consumed and you have to sign in via /token.
Reuse detection
Submitting a refresh token that has already been used or invalidated returns 401 SAPI-AUTH-006 and, for security reasons, invalidates all refresh tokens of that client (access tokens already issued remain valid until they expire). The client then has to sign in via /token. With multiple client instances, therefore, either share the refresh token or give each instance its own.
Typical errors
  • 401 SAPI-AUTH-007 — the refresh token is invalid, corrupted or expired
  • 401 SAPI-AUTH-006 — the refresh token has already been used or invalidated
  • SAPI-AUTH-001 — IAM rejected the stored client credentials (e.g. a changed client_secret)
  • 500 SAPI-AUTH-999 — internal error or IAM unavailable
Input fields (JSON)
Name
Description
Required
refresh_token
The current refresh token
required
Output fields
Name
Description
access_token, token_type, expires_in, scope, refresh_token
As for /token — a new pair of tokens
POST v1 SAPI auth

/sapi/v1/auth/revoke

Invalidates a refresh token (RFC 7009, idempotent). Based on possession of the token — no client_id/client_secret.
Behaviour
An APEX refresh token is invalidated immediately; a subsequent /renew with it returns 401. Revoking an already invalid token again also returns 200 ("Token was already invalid or expired"). Any other token is forwarded by APEX to IAM for revocation.
Access tokens
APEX verifies access tokens locally (signature and expiry), so an access token remains valid until it expires even after revocation. In a security incident, revoke the refresh token and have the client_secret changed in IAM.
When to call
  • when decommissioning an integration client
  • when a refresh token leak is suspected
  • when rotating credentials
Typical errors
  • SAPI-AUTH-001 — IAM rejected the revocation of another token
  • 500 SAPI-AUTH-999 — IAM unavailable
Input fields (JSON)
Name
Description
Required
token
Token to invalidate (APEX refresh token)
required
token_type_hint
refresh_token
optional
Output fields
Name
Description
success
With 200, always true
message
E.g. Refresh token revoked successfully
timestamp
Response time (ISO 8601)

Three operations according to SAPI-SK v1.0: send, receive, acknowledge. Every request carries the X-Peppol-Participant-Id header (organizational context — a participant of your tenant; short or canonical form). The fields of the document endpoints are in camelCase; unknown fields are silently ignored, so use the exact names.

POST v1 SAPI outbound

/sapi/v1/document/send

Hands over a document for delivery via Peppol. The checks run synchronously, delivery asynchronously: 200 ACCEPTED = taken into custody, 4xx/5xx REJECTED = nothing was queued.
Checks before ACCEPTED
1. Request — headers, the fields metadata, payload, payloadFormat (only XML), payloadEncoding (only UTF-8), optional checksum.
2. Authorization — the participant from the header and metadata.senderParticipantId belong to your tenant and are identical; the tenant has a valid licence.
3. Document — well-formed XML, XSD, Schematron (EN 16931 / Peppol BIS) and business rules for the documentTypeId.
4. Parties — the sender and recipient are the parties stated in the document (supplier → customer; a self-billing invoice is sent by the customer, an order by the buyer).
5. Recipient — is registered in Peppol (SML/SMP) and offers the documentTypeId + processId via AS4.
Delivery
SAPI does not report the delivery result (SAPI-SK v1.0 §9.5). Store the providerDocumentId — it is the document's UUID in APEX, so with the same token you can track delivery via GET /api/v1/documents/{providerDocumentId}/status, /events or webhooks.
Idempotency (Idempotency-Key, required)
A new unique key (e.g. a UUID, max 200 characters) for every document; keys are separate for each tenant. The same key + the same request (payload, sender, recipient, type, process) within 24 h → the original ACCEPTED response (the same providerDocumentId and receivedAt), nothing is queued a second time. A different request → 409 IDEMPOTENCY_CONFLICT. The first request is still running → 409 IDEMPOTENCY_IN_PROGRESS (retry with the same key). A rejected request does not consume the key. After 24 h the key behaves as a new one — so do not retry old requests after a day.
Retrying
Retry only if error.retryable is true (409 IDEMPOTENCY_IN_PROGRESS, 503, 500, 429), with exponential backoff and the same Idempotency-Key. When contacting support, quote the error.correlation_id.
Headers and body (JSON)
Name
Description
Required
Idempotency-Key
Header, 1–200 characters
required
X-Peppol-Participant-Id
Header; the same participant as metadata.senderParticipantId
required
metadata.senderParticipantId
Sender (0245:1234567890 or canonical form)
required
metadata.receiverParticipantId
Recipient
required
metadata.documentTypeId
Peppol document type
required
metadata.processId
Peppol process
required
metadata.documentId
Your label, informational only
optional
metadata.creationDateTime
Informational; if present, it must be ISO 8601 with an offset
optional
payload
The document as UTF-8 XML text (not Base64) — bare UBL or UBL in SBDH; we recommend up to 10 MB
required
payloadFormat
XML
required
payloadEncoding
UTF-8 (default)
optional
checksum
SHA-256 of the UTF-8 bytes of payload, hex (64 characters)
optional
Output fields (200)
Name
Description
providerDocumentId
Document UUID in APEX (for support and the APEX API)
status
Always ACCEPTED — taken over, not delivered
receivedAt
When APEX took over the document (UTC); the original value on a replay
timestamp
Time this response was created (UTC)
Rejection (4xx/5xx)
Name
Description
status
REJECTED
timestamp
Response time
errorCode
Reason (table below)
errorMessage
Description of the reason
error
SAPI error envelope: category, code, message, details[], retryable, correlation_id
HTTPerrorCodeerror.coderetryableCause
400INVALID_REQUESTSAPI-VAL-001nomissing/empty Idempotency-Key header (or > 200 characters) or X-Peppol-Participant-Id; the body is not valid JSON; invalid creationDateTime
400INVALID_METADATASAPI-VAL-001nometadata or one of the identifiers is missing, or contains control characters
400PAYLOAD_MISSINGSAPI-VAL-001nopayload is missing (e.g. a body following the specification's document{content})
400INVALID_DOCUMENT_FORMATSAPI-VAL-001nopayloadFormat is not XML, payloadEncoding is not UTF-8, the checksum does not match
400INVALID_DOCUMENT_FORMATSAPI-VAL-002nopayload is not well-formed XML
400UNSUPPORTED_DOCUMENT_TYPESAPI-VAL-002nono validation rules exist for the documentTypeId
400DOCUMENT_VALIDATION_FAILEDSAPI-VAL-002noXSD, Schematron, business rule or party errors; listed in error.details[] (field = location, issue = description, value = rule code)
400RECEIVER_NOT_FOUNDSAPI-VAL-021nothe recipient is not in Peppol (on the sandbox: is not a sandbox participant)
400UNSUPPORTED_DOCUMENT_TYPESAPI-VAL-021nothe recipient does not offer the document type and process via AS4
403AUTHORIZATION_FAILEDSAPI-AUTH-007nothe participant from the header or the sender does not belong to your tenant, or the two differ
403AUTHORIZATION_FAILEDSAPI-AUTH-008nothe tenant has no valid licence
409IDEMPOTENCY_CONFLICTSAPI-VAL-010nothe key was used for a different request — use a new key
409IDEMPOTENCY_IN_PROGRESSSAPI-VAL-010yesa request with the same key is being processed right now
503SERVICE_UNAVAILABLESAPI-PROC-002yesSML/SMP, validation or storage temporarily unavailable
500PROCESSING_ERRORSAPI-PROC-001yesunexpected error
ℹ️Deviations from SAPI-SK v1.0: the path /sapi/v1/document/send (specification: /sapi/v1/documents), the identifier providerDocumentId (specification: apDocumentId), the document in the flat fields payload, payloadFormat, payloadEncoding (specification: document{content, format, encoding} — such a body ends with PAYLOAD_MISSING), additionally the required headers X-Peppol-Participant-Id and Idempotency-Key, and checks 3–5 beyond the specification's minimum. 401 and 429 responses have only the error envelope (no status); 415, 405 and 406 use the Problem Details (RFC 9457) format — decide based on the HTTP code.
GET v1 SAPI inbound

/sapi/v1/document/receive

List of documents received by the participant from the header, oldest first, with cursor pagination.
Cursor pagination
The first call goes without pageToken. Send the nextPageToken value from the response as the pageToken query parameter of the next call; nextPageToken: null = last page. The cursor is an opaque string — do not parse or construct it. Keep status, limit and the header the same for all pages of one pass. A nextPageToken parameter is accepted as an alias of pageToken (if you send both, they must be equal).
Recommended polling
  • regularly (e.g. every minute) start from the beginning with status=RECEIVED
  • download each document via the detail, store it and acknowledge it
  • acknowledged documents drop out of the next pass; do not keep an old cursor for later
Statuses
RECEIVED — received and validated, not yet acknowledged
ACKNOWLEDGED — acknowledged (via SAPI or the APEX API — the status is shared)
Without a filter, both. Documents rejected during validation do not appear in the list. The list returns only metadata and does not change the status of documents.
Typical errors
  • 400 SAPI-VAL-001 — invalid pageToken, differing pageToken/nextPageToken, negative limit
  • 400 (Problem Details) — the header is missing or limit is not a number
  • 401 SAPI-AUTH-002, 403 SAPI-AUTH-007 — the participant does not belong to your tenant
  • 429 SAPI-RATE-001
Parameters
Name
Description
Required
X-Peppol-Participant-Id
Header — the recipient participant (short or canonical form)
required
pageToken
Cursor from the nextPageToken of the previous response
optional
limit
Page size, default 20; we recommend at most 100
optional
status
RECEIVED or ACKNOWLEDGED
optional
Output fields
Name
Description
documents[].documentId
Document UUID in APEX (for the detail and acknowledgement) — not the invoice number
documents[].documentTypeId, processId
Peppol document type and process
documents[].senderParticipantId, receiverParticipantId
Canonical form iso6523-actorid-upis::…
documents[].creationDateTime
When APEX received the document (not the invoice issue date)
nextPageToken
Cursor of the next page or null
GET v1 SAPI inbound

/sapi/v1/document/receive/{documentId}

Detail of a received document: metadata and the whole StandardBusinessDocument (an SBDH envelope with the business document) as an XML string.
Payload
payload is the StandardBusinessDocument as it arrived over the Peppol network, re-serialized — namespace prefixes and formatting may differ from the original. Find the business document by its namespace, parse with namespaces in mind and do not validate the root directly against the UBL XSD. Downloading does not change the status.
Typical errors
  • 404 (empty body) — the ID is not a UUID or the document does not exist
  • 403 SAPI-AUTH-009 — the document is not addressed to the participant from the header
  • 400 SAPI-DOC-002 — not a received document of your tenant
  • 401 SAPI-AUTH-002, 429 SAPI-RATE-001
Output fields
Name
Description
receivedAt
When APEX received the document
metadata
The same object as in the list (documentId, documentTypeId, processId, senderParticipantId, receiverParticipantId, creationDateTime)
payload
StandardBusinessDocument as a UTF-8 XML string
payloadFormat
XML
POST v1 SAPI inbound

/sapi/v1/document/receive/{documentId}/acknowledge

Confirms that your system has safely taken over the document. The status changes to ACKNOWLEDGED. Idempotent.
When to call
  • only after downloading the detail, processing the XML and storing the document permanently
  • if any step failed, do not acknowledge — the document stays in status=RECEIVED
  • a repeated acknowledgement returns 200 with the original acknowledgement time
Typical errors
  • 404 (empty body) — the document does not exist or is not yours
  • 403 SAPI-AUTH-009 — the document is not addressed to the participant from the header
  • 409 SAPI-DOC-002 — it is a sent document, not a received one
Parameters
Name
Description
Required
documentId
documentId from the list (path); no body is sent
required
X-Peppol-Participant-Id
Header — the document's recipient
required
Output fields
Name
Description
documentId
The acknowledged document
status
ACKNOWLEDGED
acknowledgedDateTime
Time of the (first) acknowledgement, ISO 8601 with an offset
Key concepts, statuses and identifiers
The basic concepts you need for every integration: the difference between the interfaces, identifier formats, document lifecycles, validation and responses in the Peppol network.
APEX API (/api/v1, /api/v2) is the full REST API: sending (including batches), receiving, validation, precheck, status, events, responses (MLR/IMR/MLS), webhooks and MQTT, participant lookup, statistics and licence. Token from ASOL IAM (client_credentials).

APEX SAPI (/sapi/v1) is a simplified interface according to the Slovak SAPI-SK v1.0 specification: send, receive, acknowledge. Sending is asynchronous here too — ACCEPTED means taken over, not delivered — but all checks (document, recipient in Peppol) run synchronously within the request. It has its own auth endpoints with a refresh token; the same access token also works for the APEX API.

QHUB is an asynchronous integration via RabbitMQ for systems in the ASOL ecosystem and ERPs with a messaging layer — see the Integration Guide.

The interfaces can be combined: e.g. sending via SAPI and tracking delivery via /api/v1/documents/{id}/status or webhooks.
Peppol participant ID uniquely identifies a network participant. The short form <ICD>:<value> (e.g. 0245:2024000001) and the canonical form iso6523-actorid-upis::0245:2024000001 are interchangeable in requests. v1 inbox responses and notifications use the canonical form, the v2 inbox and the lookup the short form.

Slovakia: scheme 0245 (SK:DIČ) — the DIČ (Slovak tax ID) has 10 digits without the SK prefix. Other common schemes: 9950 SK VAT number (IČ DPH), 9929 CZ VAT, 9914 AT VAT, 9919 AT company register, 9930 DE VAT, 9910 HU VAT, 0088 GLN, 0007 SE organization number, 0200 LT company code.

IČO (company registration number) is not a Peppol identifier; /participants/lookup/ico translates it to a DIČ via the Register of Financial Statements.
QUEUED → PROCESSING → DELIVERED (→ possibly FAILED after a negative MLS)
on a transfer error: PROCESSING → RETRYING → PROCESSING → … → DELIVERED or DEAD_LETTER

QUEUED — accepted, waiting in the queue • PROCESSING — AS4 transfer in progress • DELIVERED — the receiving AP confirmed receipt (AS4 receipt) • RETRYING — waiting for the next attempt • DEAD_LETTER — attempts exhausted or a permanent error, intervention needed • FAILED — negative Peppol MLS from the receiving AP • REJECTED — licence not valid at the time of sending. The value SENT is in the code list but is not used for sending.

A validation error, an unknown recipient or a foreign sender is returned by the API immediately in the response — such a document is never created.

DELIVERED means delivery to the recipient's access point (Peppol corner 3), not processing in the recipient's ERP — that is signalled only by application responses (IMR) and the MLS.
APEX API: VALIDATED (validation passed, waiting to be downloaded, DOCUMENTS_RECEIVED notification) → ACKNOWLEDGED (your system confirmed it). REJECTED = the document failed validation (it is in the list, no notification is sent), FAILED = processing error. RECEIVED is only an internal intermediate state during receipt.

SAPI: a simplified model RECEIVED (received and validated, waiting for acknowledgement) → ACKNOWLEDGED; SAPI does not show rejected documents. The acknowledgement is shared between the APEX API and SAPI.

APEX recognizes an AS4 message delivered twice and does not store it a second time.

The technical receipt status (Peppol MLS, Message Level Status) is sent to the sending provider by APEX on your behalf — always when negative, when positive only if the sender requested it. You only take care of the business response (e.g. IMR), if your process requires one.
1. XML Schema (XSD) — structure according to UBL 2.1
2. EN 16931 Schematron — the European e-invoicing standard (mandatory fields, code lists, calculations)
3. Peppol BIS rules — Peppol rules beyond EN 16931 (e.g. PEPPOL-EN16931-R*)

The validation set is selected by documentTypeId and cbc:CustomizationID: Peppol BIS Billing 3.0 (invoice, credit note, self-billing), national PINT variants, German XRechnung, orders (Order, Order Response) and other Peppol BIS documents. When there are several versions of the same set, the highest one is used. The same validation runs in /validate, /precheck, /send, batches and SAPI.
MLS (Message Level Status, urn:peppol:edec:mls) — the technical status of a message, sent by the receiving access point to the sending one. When sending, APEX requests an MLS for every document; positive (AP/AB) → MLS_RECEIVED notification, negative (RE) → the document moves to FAILED and MLS_RECEIVED_NEGATIVE arrives. In the Peppol network, receiving MLS is mandatory from 28 February 2027 and sending it from 31 March 2027 — until then some access points do not return it yet, so a missing MLS is not an error.
MLR (Message Level Response, BIS 36a / T71) — the older application response for acceptance/rejection.
IMR (Invoice Response 3.0 / T111) — the customer's business decision about an invoice.
ORDER_RESPONSE (BIS 28a), RECEIPT_ADVICE (BIS 30a).

Codes: AP accepted, RE rejected, CA conditionally accepted, UQ under query, IP in process. You find all of them via GET /api/v1/documents/{id}/responses.
A received document (APEX API ublDocument, SAPI payload) is the whole StandardBusinessDocument — an SBDH envelope (http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader) with the UBL document inside, exactly as it arrived over the network. Look for the business document by its namespace (e.g. urn:oasis:names:specification:ubl:schema:xsd:Invoice-2), not as the root.

Prefixes are not a contract. cac:, cbc: and other prefixes need not match between versions or even within a single document — use a namespace-aware parser (XPath with registered namespaces, JAXB, XmlReader with NamespaceURI…).
POST /api/v1/documents/send — Idempotency-Key header optional; the key is valid for 24 h; the content (document and routing metadata) is compared.
POST /api/v1/batches — optional, recommended; the key does not expire; the body bytes are compared (SHA-256).
POST /sapi/v1/document/send — required; the key is valid for 24 h; the payload and metadata (sender, recipient, type, process) are compared.

Common rules: the same key + the same request → the original response (no new sending); the same key + a different request → 409 IDEMPOTENCY_CONFLICT; a concurrently running request with the same key → 409 IDEMPOTENCY_IN_PROGRESS (retry later). A rejected request does not consume the key. On a timeout, always retry with the same key.
If the AS4 transfer fails (network, timeout, error of the receiving AP), the document moves to RETRYING and the worker tries again. A document has 5 attempts; after the 1st to 4th failure, the next attempt follows after 30, 60, 120 and 240 s, and the fifth failure means DEAD_LETTER — roughly 7.5 minutes in total plus transfer time (the AS4 timeout is 120 s). Permanent errors (e.g. a recipient certificate not issued by the OpenPeppol CA) go to DEAD_LETTER immediately. Every attempt creates QUEUE_PROCESSING_STARTED events and, on failure, QUEUE_PROCESSING_FAILED (Attempt n/5 failed, retry in …s: <cause>). A document in DEAD_LETTER requires intervention — contact support with the documentId; once the problem is fixed, support can requeue it (QUEUE_MANUAL_REQUEUE event, no notification).

APEX also protects receiving access points: when a destination keeps failing, sending to it is paused for 30 s (circuit breaker) and documents are rescheduled without using up an attempt. Tenants take turns in the queue, so one large sender does not block the others.
SML (Service Metadata Locator) is the DNS registry of the Peppol network: from a Peppol ID it determines which SMP manages the participant's data.
SMP (Service Metadata Publisher) publishes which document types and processes the participant accepts, the URL of its AS4 endpoint and the access point's certificate.

APEX stores the result of a recipient lookup and refreshes it after the TTL expires (5 days by default) or after a failed transfer. The stored result is not tied to a specific document type — if the recipient has changed its registration in the meantime, the error may only show up during the transfer. Before sending, APEX verifies that the recipient access point's certificate was issued by the OpenPeppol CA and is valid.
The version is part of the path (/api/v1, /api/v2). A published version never changes in a backward-incompatible way — a new format is introduced under a new version and the previous one keeps working. Today only receiving has a v2 version (GET /api/v2/documents/inbox and the detail) — it differs only in the short identifier format. All other endpoints exist only in v1. Clients should ignore unknown fields in responses and unknown eventType values.
The most common Peppol document type and process identifiers
DocumentdocumentTypeIdprocessId
Invoice (BIS Billing 3.0)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.1urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
Credit note (BIS Billing 3.0)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.1urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
Self-billing invoice (Self-billing 3.0)urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:selfbilling:3.0::2.1urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0
Order (BIS Order Only 3)urn:oasis:names:specification:ubl:schema:xsd:Order-2::Order##urn:fdc:peppol.eu:poacc:trns:order:3::2.1urn:fdc:peppol.eu:poacc:bis:order_only:3
Order response (BIS Ordering 3)urn:oasis:names:specification:ubl:schema:xsd:OrderResponse-2::OrderResponse##urn:fdc:peppol.eu:poacc:trns:order_response:3::2.1urn:fdc:peppol.eu:poacc:bis:ordering:3
Invoice response (IMR, Invoice Response 3)urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:poacc:trns:invoice_response:3::2.1urn:fdc:peppol.eu:poacc:bis:invoice_response:3
MLR (Message Level Response 3)urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:poacc:trns:mlr:3::2.1urn:fdc:peppol.eu:poacc:bis:mlr:3
MLS (Message Level Status) — sent by APEXurn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:peppol:edec:mls:1.0::2.1urn:peppol:edec:mls
Authentication

OAuth 2.0 — Bearer token

All endpoints except /sapi/v1/auth/token, /renew and /revoke require the header Authorization: Bearer <access_token>. The access token is a JWT signed (RS256) by the ASOL IAM server; you obtain it with the client_credentials grant (server-to-server, no user sign-in) — either directly from IAM or via the SAPI endpoint, which additionally issues a refresh token. The resulting token is the same and is valid for both the APEX API and SAPI.

Two ways to obtain a token
A) Directly from ASOL IAMB) Via SAPI
EndpointPOST https://iam.qasida.sk/oauth2/tokenPOST /sapi/v1/auth/token
Formatapplication/x-www-form-urlencodedapplication/json
Access tokenJWT, lifetime per expires_inthe same JWT from IAM
Refresh tokenno — always get a new token via client_credentialsyes, 30 days, rotation via /sapi/v1/auth/renew
Recommended forAPEX API, Swagger UI (Authorize)SAPI integrations according to SAPI-SK v1.0

1. Token

client_credentials from IAM or /sapi/v1/auth/token

2. Use

Authorization: Bearer <token> in every request

3. Renewal

before expiry, a new client_credentials grant or /renew

4. Revoke

/sapi/v1/auth/revoke for the refresh token

🔑Important: the credentials (clientId, clientSecret) are bound to one organization (tenant) in APEX — the token determines which documents and participants you have access to. Renew the access token with a margin (e.g. 1–3 min before expires_in), use one token for all requests while it is valid, and request a new one on 401. APEX verifies the token locally (signature and validity), so revoking an access token does not cancel it before it expires. The scope parameter is not required — access is determined by the tenant.
Authentication example
Token request and usage
A) Token directly from ASOL IAM
POST https://iam.qasida.sk/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=your-client-id
&client_secret=your-client-secret

HTTP/1.1 200 OK
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 900
}
B) Token via SAPI (with a refresh token)
POST /sapi/v1/auth/token
Content-Type: application/json

{
  "client_id": "your-client-id",
  "client_secret": "your-client-secret",
  "grant_type": "client_credentials"
}

HTTP/1.1 200 OK
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": null,
  "refresh_token": "eyJhbGciOiJIUzI1NiJ9..."
}
Renewal via SAPI
POST /sapi/v1/auth/renew
Content-Type: application/json

{ "refresh_token": "eyJhbGciOiJIUzI1NiJ9..." }
Using the token
GET /api/v2/documents/inbox?status=VALIDATED
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Accept: application/json
Errors and limits

Errors, limits and security

What error responses look like, which errors are worth retrying, which limits apply and what to watch out for when designing a client. Always decide based on the HTTP code and the machine-readable error code, not on the message text.

APEX API — /api/**
Error format
A JSON object with the fields timestamp, status, error, message and usually path. A machine-readable code is present for batches and idempotency (INVALID_IDEMPOTENCY_KEY, IDEMPOTENCY_CONFLICT, IDEMPOTENCY_IN_PROGRESS, BATCH_*, PARTICIPANT_NOT_AUTHORIZED, LICENCE_INVALID, TOO_MANY_OPEN_BATCHES); for other errors message is informational text only. On failed validation, /documents/send returns error: "Validation Error" with validationErrors[] and validationWarnings[] (code, message, location). Framework errors (unreadable JSON, wrong parameter type, missing parameter, 405/415) use the Problem Details format (type, title, status, detail, instance). A 404 for a non-existent or foreign resource has an empty body. timestamp has no time zone (UTC).
{
  "timestamp": "2026-10-09T10:15:00.123",
  "status": 409,
  "error": "Conflict",
  "code": "IDEMPOTENCY_CONFLICT",
  "message": "Idempotency-Key 'FA2026-0001' was already used for a different request (document or routing metadata)",
  "path": "/api/v1/documents/send"
}
401 without a token
{
  "timestamp": "2026-10-09T19:31:24.544",
  "status": 401,
  "error": "Unauthorized",
  "message": "Missing or malformed Authorization: Bearer header",
  "path": "/api/v1/documents/send"
}
SAPI — /sapi/**
Error envelope
An error object: category (AUTH, VALIDATION, PROCESSING, TEMPORARY), code (SAPI-…), message, details[] (or null), retryable and correlation_id (quote it to support). A send rejection wraps it in status: REJECTED with an errorCode. 415, 405 and 406 responses and a missing header on receive use the Problem Details (RFC 9457) format; a 404 for the detail and the acknowledgement has an empty body.
{
  "error": {
    "category": "AUTH",
    "code": "SAPI-AUTH-002",
    "message": "Access token is invalid or has expired",
    "details": null,
    "retryable": false,
    "correlation_id": "c3d4e5f6-a7b8-9012-cdef-345678901234"
  }
}
HTTP codes and what to do about them
HTTPMeaningWhat to do
200 / 201 / 202 / 204Success; 202 = accepted for asynchronous processingFor 202, track the status (/status, notifications)
400Invalid request or document. Special case: Invalid UUID string: anonymousUser means a token without a tenant (e.g. a user token instead of an integration client token)Fix the request; do not retry unchanged
401Missing, invalid or expired token (the header must start with exactly Bearer )Obtain a new token and retry the request once
403The participant does not belong to your tenant, invalid licence, the tenant from the token is not set up in APEX (Authenticated tenant not found)Check the Peppol ID, the licence (/api/v1/licenses/info) and the credentials
404The resource does not exist or belongs to another tenant (deliberately not distinguished)Check the ID and the environment
409Idempotency conflict, the status does not allow the operationRetry IDEMPOTENCY_IN_PROGRESS later; resolve the others
413Batch too largeSplit the batch
415Wrong Content-TypeSend application/json (ZIP batch: application/zip)
429Rate limit or batch intake capacity exceededWait according to Retry-After, then back off exponentially
500 / 503Unexpected or temporary errorRetry with exponential backoff and the same Idempotency-Key
Rate limiting
EndpointsLimit
POST /sapi/v1/document/send100 requests / min / IP (separate limit)
all other /api/** and /sapi/** (including /sapi/v1/auth/*)200 requests / min / IP (shared limit)
The limit is counted per source IP address (a token bucket with continuous refill — a short burst is allowed, then approx. limit/60 per second). Every response carries the X-RateLimit-Limit and X-RateLimit-Remaining headers; a 429 also carries Retry-After (seconds until the next free request). The rate limit is evaluated before authentication, so unsuccessful requests count too. Batch sending additionally has its own limits (3 open batches, BATCH_INTAKE_BUSY).
HTTP 429
Response when the limit is exceeded
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
Retry-After: 1

{
  "error": {
    "category": "TEMPORARY",
    "code": "SAPI-RATE-001",
    "message": "Rate limit exceeded. Maximum 100 requests per minute per IP address.",
    "retryable": true,
    "correlation_id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890"
  }
}
For /api/** the 429 body has the APEX API error shape: {"timestamp", "status": 429, "error": "Too Many Requests", "message", "path"}. The limits apply to each service instance separately — treat them as an indicative upper limit, not as guaranteed capacity.
Recommendations for a robust client
Retries and idempotency
On 429, 500, 503 and timeouts, retry with exponential backoff (e.g. 2, 4, 8, 16 s + random jitter). Always send sending requests with an Idempotency-Key and retry with the same key — that way you never send an invoice twice.
Tolerant parsing
Ignore unknown fields in the JSON and unknown values of eventType or statuses — the API is extended in a backward-compatible way. Parse times as ISO 8601 with an offset and compare instants, not strings (the number of decimal places varies). Parse the XML payload with namespaces in mind.
Security
HTTPS only. Store the clientSecret, the refresh token and the MQTT password in a secrets vault, not in code or logs. Webhooks are not signed — verify them with a secret in customHeaders and always load the content from the API by documentId. Treat notifications as a signal, not as the source of truth.
Examples

Request / Response examples

Concrete requests and responses for the key scenarios. $APEX is the environment's base URL (e.g. https://api.sandbox.apex.qasida.sk), $TOKEN the access token. XML documents are shortened in the examples.

POST /api/v1/documents/send
Sending an invoice
Request
curl -X POST "$APEX/api/v1/documents/send" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: FA2026-0001" \
  -d @invoice.json

{
  "senderParticipantId": "0245:2024000001",
  "receiverParticipantId": "0245:1234567890",
  "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
  "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
  "ublDocument": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\" ...>...</Invoice>"
}
Response (HTTP 202)
{
  "documentId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "QUEUED",
  "submittedAt": "2026-10-09T10:30:00"
}
Response (HTTP 400 — invalid document)
{
  "timestamp": "2026-10-09T10:30:00",
  "status": 400,
  "error": "Validation Error",
  "message": "Document validation failed: 1 error(s)",
  "validationErrors": [
    {
      "code": "BR-02",
      "message": "An Invoice shall have an Invoice number (BT-1).",
      "location": "/Invoice/cbc:ID"
    }
  ],
  "validationWarnings": []
}
POST /api/v1/documents/precheck
Precheck before sending
Response — ready to send
{
  "networkReady": true,
  "valid": true,
  "participantExists": true,
  "canSend": true,
  "errors": [],
  "warnings": [
    {
      "category": "ROUTING",
      "code": "CERT_EXPIRING_SOON",
      "message": "Recipient AP certificate expires in 18 days (2026-10-27). Sending is still possible.",
      "location": null
    }
  ],
  "checkedAt": "2026-10-09T11:00:00+02:00"
}
Response — recipient is not in Peppol
{
  "networkReady": false,
  "valid": true,
  "participantExists": false,
  "canSend": false,
  "errors": [
    {
      "category": "PARTICIPANT",
      "code": "PARTICIPANT_NOT_FOUND",
      "message": "Participant '0245:9999999999' was not found in the Peppol network.",
      "location": null
    }
  ],
  "warnings": [],
  "checkedAt": "2026-10-09T11:00:00+02:00"
}
GET /api/v1/participants/lookup/dic?schema=0245&dic=2024000001
Participant lookup
Response
{
  "source": "SMP",
  "participantId": "iso6523-actorid-upis::0245:2024000001",
  "peppolId": "0245:2024000001",
  "organizationName": null,
  "countryCode": "SK",
  "endpointUrl": "https://ap.partner.example.com/as4/receive",
  "smpUrl": "https://smp.partner.example.com/",
  "enabled": true,
  "certificate": {
    "subject": "CN=PSK000202,OU=PEPPOL PRODUCTION AP,O=Partner SK s.r.o.,C=SK",
    "issuer": "CN=PEPPOL ACCESS POINT CA - G2,O=OpenPEPPOL AISBL,C=BE",
    "serial": "7f3a1b2c9d0e4f56",
    "fingerprint": "FF:EE:DD:CC:...:00",
    "notBefore": "2025-03-01T00:00:00Z",
    "notAfter": "2027-03-01T23:59:59Z",
    "expired": false,
    "valid": true,
    "invalidReason": null
  },
  "smpMetadataUpdatedAt": "2026-10-09T10:15:30Z"
}
GET /api/v1/documents/{documentId}/events
Document history
Response (shortened)
[
  {
    "id": "d3a1e2f0-11aa-42bb-9c3d-000000000003",
    "messageId": "550e8400-e29b-41d4-a716-446655440000",
    "eventType": "MESSAGE_QUEUED",
    "eventCode": null,
    "eventMessage": "Document queued for async AS4 sending to https://receiver.example.com/as4",
    "technicalDetails": null,
    "stacktrace": null,
    "createdAt": "2026-10-09T10:30:01+02:00"
  },
  {
    "id": "d3a1e2f0-11aa-42bb-9c3d-000000000004",
    "messageId": "550e8400-e29b-41d4-a716-446655440000",
    "eventType": "QUEUE_PROCESSING_FAILED",
    "eventCode": null,
    "eventMessage": "Attempt 1/5 failed, retry in 30s: AS4 send returned failure",
    "technicalDetails": null,
    "stacktrace": null,
    "createdAt": "2026-10-09T10:30:04+02:00"
  },
  {
    "id": "d3a1e2f0-11aa-42bb-9c3d-000000000006",
    "messageId": "550e8400-e29b-41d4-a716-446655440000",
    "eventType": "MESSAGE_DELIVERED",
    "eventCode": null,
    "eventMessage": "Message delivered successfully",
    "technicalDetails": null,
    "stacktrace": null,
    "createdAt": "2026-10-09T10:30:40+02:00"
  }
]
POST /api/v1/subscriptions
Webhook and notification
Request
{
  "name": "ERP Invoice Inbox",
  "description": "Notifications about new invoices",
  "url": "https://erp.example.com/webhooks/peppol",
  "active": true,
  "documentTypeFilters": [
    "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"
  ],
  "senderParticipantFilters": [],
  "customHeaders": { "X-Webhook-Secret": "shared-secret" }
}
Notification that APEX sends to the URL
POST https://erp.example.com/webhooks/peppol
Content-Type: application/json
User-Agent: Apex-Webhook/1.0
X-Webhook-Secret: shared-secret

{
  "eventType": "DOCUMENTS_RECEIVED",
  "subscriptionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "documentId": "550e8400-e29b-41d4-a716-446655440001",
  "documentIds": ["550e8400-e29b-41d4-a716-446655440001"],
  "receivedCount": 1,
  "direction": "IN",
  "status": "VALIDATED",
  "messageId": "c3a1e7f2-5b9d-4d2e-8f41-2a6b9e0d7c15@sender-ap.example.com",
  "senderParticipantId": "iso6523-actorid-upis::0245:1234567890",
  "receiverParticipantId": "iso6523-actorid-upis::0245:2024000001",
  "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##...::2.1",
  "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
  "eventMessage": "Incoming Peppol document received",
  "notifiedAt": 1791282005.123456000
}
GET /api/v2/documents/inbox/{documentId}
Downloading a received invoice
Response
{
  "documentId": "550e8400-e29b-41d4-a716-446655440001",
  "senderParticipantId": "0245:1234567890",
  "receiverParticipantId": "0245:2024000001",
  "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##...::2.1",
  "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
  "receivedAt": "2026-10-09T09:15:00",
  "status": "VALIDATED",
  "ublDocument": "<StandardBusinessDocument xmlns=\"http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader\"><StandardBusinessDocumentHeader>...</StandardBusinessDocumentHeader><Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">...</Invoice></StandardBusinessDocument>"
}
Acknowledgement after storing in the ERP
curl -X POST "$APEX/api/v1/documents/inbox/550e8400-e29b-41d4-a716-446655440001/acknowledged" \
  -H "Authorization: Bearer $TOKEN"

HTTP/1.1 204 No Content
POST /api/v1/batches
Batch submission
Request (JSON)
curl -X POST "$APEX/api/v1/batches" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ERP-run-2026-10-09-17" \
  -H "Expect: 100-continue" \
  --data-binary @batch.json

{
  "clientReference": "ERP-run-2026-10-09-17",
  "notify": "BATCH",
  "documents": [
    {
      "clientReference": "FA2026-0001",
      "senderParticipantId": "0245:2024000001",
      "receiverParticipantId": "0245:1234567890",
      "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##...::2.1",
      "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
      "ublDocument": "<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">...</Invoice>"
    }
  ]
}
Response (HTTP 202) and BATCH_PROCESSED notification
{
  "batchId": "6f1c2a9e-0b7d-4f3a-9c55-2b8e1f0d4a11",
  "clientReference": "ERP-run-2026-10-09-17",
  "status": "RECEIVED",
  "itemCount": 1000,
  "receivedAt": "2026-10-09T10:15:00Z"
}

{
  "eventType": "BATCH_PROCESSED",
  "subscriptionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "eventMessage": "Batch processed: 990 accepted, 10 rejected, 0 cancelled",
  "notifiedAt": 1791281700.482913000,
  "batch": {
    "batchId": "6f1c2a9e-0b7d-4f3a-9c55-2b8e1f0d4a11",
    "clientReference": "ERP-run-2026-10-09-17",
    "status": "PROCESSED",
    "itemCount": 1000,
    "accepted": 990,
    "rejected": 10,
    "cancelled": 0
  }
}
POST /sapi/v1/document/send
SAPI sending
Request
POST /sapi/v1/document/send
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json
Idempotency-Key: 3f6c1d2e-8a4b-4c5d-9e0f-112233445566
X-Peppol-Participant-Id: 0245:2024000001

{
  "metadata": {
    "documentId": "FA2026-0001",
    "documentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
    "processId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
    "senderParticipantId": "0245:2024000001",
    "receiverParticipantId": "0245:1234567890",
    "creationDateTime": "2026-10-09T10:30:00Z"
  },
  "payload": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\" ...>...</Invoice>",
  "payloadFormat": "XML",
  "payloadEncoding": "UTF-8",
  "checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
Response (HTTP 200)
{
  "providerDocumentId": "7c9d0e1f-2a3b-4c5d-8e6f-0a1b2c3d4e5f",
  "status": "ACCEPTED",
  "receivedAt": "2026-10-09T08:30:05.123456Z",
  "timestamp": "2026-10-09T08:30:05.130Z"
}
Response (HTTP 400 — recipient is not in Peppol)
{
  "status": "REJECTED",
  "timestamp": "2026-10-09T08:30:05Z",
  "errorCode": "RECEIVER_NOT_FOUND",
  "errorMessage": "The receiver is not registered in the Peppol network for this document type (The SML has no SMP address for the participant)",
  "error": {
    "category": "VALIDATION",
    "code": "SAPI-VAL-021",
    "message": "The receiver is not registered in the Peppol network for this document type (The SML has no SMP address for the participant)",
    "details": null,
    "retryable": false,
    "correlation_id": "e5f6a7b8-c9d0-1234-ef01-567890123456"
  }
}