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.
/api-docs/api and /api-docs/sapi.
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.
client_credentials). JSON body, the UBL document as XML text. Sending is asynchronous (202 / ACCEPTED); you track the status by polling, webhooks or MQTT.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.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.
https://api.sandbox.apex.qasida.sk/api-docs/api, /api-docs/sapihttps://iam.qasida.sk/oauth2/tokenDOCUMENTS_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)./swagger-ui, /api-docs/api, /api-docs/sapi/oauth2/token of the ASOL IAM server specified in your credentialsclientId + 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./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…).
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.
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.
client_credentials grant (lifetime per expires_in) and send it in the Authorization: Bearer <token> header. Request a new one before it expires./api/** and /sapi/**.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.POST /api/v1/subscriptions) or MQTT, or poll GET /api/v1/documents/{id}/status. /events shows the history and /responses the counterparty's responses.DELIVERED, or a clear reason for the failure.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.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.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.
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).
202 Accepted with status QUEUED.202 means accepted, not delivered./precheck, if you want to catch errors before sendingPOST /api/v1/batches insteadIdempotency-Key header202 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.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.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 token403 — the sender is missing, is not registered or does not belong to your tenant; invalid licence409 — IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_PROGRESS500 — 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 lookupublDocument 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.0245:2024000001) or canonical form (iso6523-actorid-upis::0245:2024000001)urn:fdc:peppol.eu:2017:poacc:billing:01:1.0/status, /events, /responses and notificationsQUEUED/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.status valuesDELIVERED)PROCESSING to QUEUED (e.g. when sending to an unreachable destination is interrupted) without a notification and without using up an attempt.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/send response (path)200 — the result is in the valid field./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.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.400 — the body is missing or ublDocument is empty415 — the body is not application/json200 with valid: falselevel, code, message, location/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.valid (XSD, EN 16931, Peppol BIS)participantExists (SML/DNS finds the SMP)canSend (the recipient's SMP offers documentTypeId + processId, an AS4 endpoint and a valid certificate)networkReady = valid && participantExists && canSendPARTICIPANT_NOT_FOUND; also reported when SML/SMP is temporarily unavailable)DOCTYPE_NOT_SUPPORTED)CERT_EXPIRING_SOON (the recipient's certificate expires within 30 days).participantExists and canSend are always true200 is always returned, even when the checks fail; precheck does not check the licence400 — a required field is missing; 403 — missing sender or a sender of another tenant/sendcategory, code, message, location (null for PARTICIPANT/ROUTING)createdAt.QUEUE_PROCESSING_STARTED / QUEUE_PROCESSING_FAILED pairsMESSAGE_RECEIVED → MESSAGE_VALIDATED → MESSAGE_ACKNOWLEDGED)eventType categories404 — the document does not exist[] — no events yetdocumentId of the document the event belongs toEBMS-0010, BR-07)nulldocumentTypeId …urn:peppol:edec:mls…), IMR (Invoice Response 3.0 / T111), ORDER_RESPONSE (BIS 28a), RECEIPT_ADVICE (BIS 30a)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.[] — no response yet (common right after sending)[] — also when the documentId is itself a response404 — the document is not yours or does not existMLR, IMR, ORDER_RESPONSE, RECEIPT_ADVICEAP/RE/CA/UQ/IP and its descriptionApplicationResponse), Content-Type: application/xml./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).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.
receivedAt DESC). Sender and recipient in the short form 0245:2024000001.ublDocument is empty) — you get the content from the detail. Reading the list does not change the status of documents.status=VALIDATED — these are the documents waiting to be downloadedGET /api/v2/documents/inbox/{documentId}POST /api/v1/documents/inbox/{documentId}/acknowledged — it drops out of the next pollDOCUMENTS_RECEIVED is sent)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.VALIDATED, ACKNOWLEDGED, REJECTED, FAILEDdocumentId, senderParticipantId, receiverParticipantId, documentTypeId, processId, receivedAt, statusStandardBusinessDocument (an SBDH envelope with the UBL document inside).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.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.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.404 (empty body) — the document does not exist, is not addressed to your tenant or is not a received document401 — invalid token0245:1234567890)StandardBusinessDocument (XML string)senderParticipantId / receiverParticipantId in the full canonical ISO 6523 form — iso6523-actorid-upis::0088:1234567890123.0088:1234567890123, i.e. the same form you send in requests and that /participants/lookup returns.ACKNOWLEDGED. Response 204 No Content.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 tenant401 — 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.
202 with a batchId and a Location header.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.RECEIVED/PROCESSING) per tenantIdempotency-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).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.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 notify400 INVALID_IDEMPOTENCY_KEY403 PARTICIPANT_NOT_AUTHORIZED / LICENCE_INVALID409 IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_PROGRESS413 BATCH_TOO_LARGE429 TOO_MANY_OPEN_BATCHES / BATCH_INTAKE_BUSY (retry after Retry-After)BATCH (default), DOCUMENT, BOTH/documents/sendRECEIVEDExpect: 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.items) and live delivery counts of the resulting messages (messages).ACCEPTED, REJECTED or CANCELLED; delivery of the accepted ones continues (see 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.404 BATCH_NOT_FOUND — does not exist or belongs to another tenantprocessedAt is null until all documents have been processedstatus=REJECTED for what needs fixing.errorCode)errors[]DOCUMENT_NOT_UTF8, DOCUMENT_NOT_XML, DOCUMENT_TYPE_UNSUPPORTED, CUSTOMIZATION_ID_MISSING, PROFILE_ID_MISSING, SENDER_ENDPOINT_MISSING, RECEIVER_ENDPOINT_MISSING, METADATA_INVALID)errorMessage filled in, it is waiting for another attempt after a temporary error)documentId for /documents/{id}/status, /events, /responseserrorCodestatus (optional), page from 0, size default 100, at most 1,000. Errors: 400 invalid status/pagination, 404 BATCH_NOT_FOUND.ACCEPTEDerrors[] = code, message, location[from, to) or in one status.from, to (ISO 8601 date-time), status (RECEIVED, PROCESSING, PROCESSED, CANCELLED), page from 0, size default 20, at most 1,000.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.ACCEPTED) are on their way and are not recalled.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}).404 BATCH_NOT_FOUND409 BATCH_NOT_CANCELLABLE — the batch is already PROCESSED or CANCELLEDnotify 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.notify | BATCH_PROCESSED | Notifications for individual documents |
|---|---|---|
| BATCH (default) | yes | problems only: QUEUE_PROCESSING_FAILED, QUEUE_DEAD_LETTER, MLS_RECEIVED_NEGATIVE |
| DOCUMENT | no | all, as for individual sending |
| BOTH | yes | all |
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.
<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.source=LOCAL), without one 404; SML/SMP is not called.source=LOCAL.404 (even if a local record exists)LOCAL), without one 503source=SMP with the endpoint and certificatesource=SMP with the stored endpoint or nullcertificate.validtrue — 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.400 — schema or dic is missing404 — not registered in Peppol, or an empty value (no body)503 — SML/SMP does not respond and no local record exists — try again later0245 (SK DIČ), 9950 (SK VAT number, IČ DPH), 9929 (CZ VAT), 9914 (AT VAT), 0088 (GLN)SK prefixLOCAL (local record) or SMP (live lookup in this request)iso6523-actorid-upis::0245:…0245:2024000001null for a participant known only from the SMP0245 → SK); ZZ = unknownnull if no record existssubject, issuer, serial, fingerprint (SHA-256), notBefore, notAfter, expired, valid, invalidReason/lookup/dic.404.503.schema and evaluated in the same way as /lookup/dic.400 — ico is missing404 — the IČO could not be translated to a DIČ or the participant is not in Peppol503 — registeruz.sk or SML/SMP is unavailable0245/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.
documentIds always has exactly one element; the array form is kept for compatibility). Fields with the value null are omitted from the JSON.| Field | Meaning |
|---|---|
| eventType | What triggered the notification — not the document status |
| subscriptionId | The subscription it is delivered for (missing for MQTT) |
| documentId / documentIds | Document UUID / the same as a single-element array |
| receivedCount | Always 1, only for DOCUMENTS_RECEIVED |
| direction | IN received, OUT sent document |
| status | Document status at the time the notification was sent |
| messageId | Message ID; for a received document, the AS4 Message ID from the sending AP |
| conversationId | Conversation ID of the document (if any) |
| senderParticipantId, receiverParticipantId | Peppol ID including the scheme (iso6523-actorid-upis::…) |
| documentTypeId, processId | Peppol document type and process |
| eventMessage | Human-readable detail, do not parse it |
| notifiedAt | When the notification was built. Webhook: a number, Unix epoch seconds with a fraction (1776168005.123456000); MQTT: an ISO 8601 string |
| batch | Only for BATCH_PROCESSED: batchId, clientReference, status, itemCount, accepted, rejected, cancelled |
| eventType | Direction | When |
|---|---|---|
| DOCUMENTS_RECEIVED | IN | A received document passed validation and is stored (VALIDATED); not sent for a rejected or duplicate document |
| MESSAGE_QUEUED | OUT | Your document was accepted and queued |
| QUEUE_PROCESSING_STARTED | OUT | A sending attempt started (for every attempt; not for self-send) |
| MESSAGE_DELIVERED | OUT | Delivered to the receiving AP (or locally for self-send) |
| QUEUE_PROCESSING_FAILED | OUT | An attempt failed, the next one is scheduled |
| QUEUE_DEAD_LETTER | OUT | Sending given up, the document is DEAD_LETTER |
| MLS_RECEIVED | OUT | A positive Peppol MLS arrived |
| MLS_RECEIVED_NEGATIVE | OUT | A negative MLS arrived; the document is FAILED (even after DELIVERED) |
| BATCH_PROCESSED | — | The batch has been processed (according to notify) |
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)./inbox, /status).POST to it for every document and event that matches the filters. Response 201 Created.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.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.BATCH_PROCESSED ignores the filters.[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.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 webhooks401 — invalid tokentrue)offset (default 0), limit (default 20, 1–100).items[] (objects as on creation), total (all of the tenant's subscriptions regardless of the page), offset, limit. Errors 400 (limit < 1), 401.404 if it does not exist or belongs to another tenant.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.204; 404 if it does not exist. If you only want to stop temporarily, use PUT with active: false.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.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).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.409 — MQTT access already exists for the tenant (delete it first)400 — the access could not be set up on the broker; try again later401mqttUrl — do not use it in new integrations204; 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/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.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.period.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).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.400 — from after to or an unknown period401 / 403errorMessage), it will be retriedreportType (EUSR/TSR), status, offset (default 0), limit (default 20, 1–100). E.g. ?reportType=EUSR&status=FAILED.items[]: id, reportType, periodStart, periodEnd, reporterId, status, errorMessage, submittedAt, validatedAt, createdAt; plus total, offset, limit. Error 400 for an unknown filter value.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).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).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.404 (empty body) — the tenant has no active licence; an expired licence returns 200 with valid: false401
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).
client_id + client_secret for an access token (JWT RS256 from ASOL IAM) and an APEX refresh token.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./renewscope parameter is optional and is forwarded to IAM; access to documents and participants is determined by the tenant the client credentials are bound to.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 JSON500 SAPI-AUTH-999 — IAM unavailable or an internal error; try again laterclient_credentialsAuthorization: Bearer headerBearernullAuthorization header: validity, remaining time and a recommendation to renew.X-Peppol-Participant-Id header is not needed). should_refresh is true when less than 3 minutes remain until expiry.expires_in401 SAPI-AUTH-002 — the token is missing, invalid or expired200, always trueaccesssub claim; a UUID for a client token)/token.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.401 SAPI-AUTH-007 — the refresh token is invalid, corrupted or expired401 SAPI-AUTH-006 — the refresh token has already been used or invalidatedSAPI-AUTH-001 — IAM rejected the stored client credentials (e.g. a changed client_secret)500 SAPI-AUTH-999 — internal error or IAM unavailable/token — a new pair of tokensclient_id/client_secret./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.client_secret changed in IAM.SAPI-AUTH-001 — IAM rejected the revocation of another token500 SAPI-AUTH-999 — IAM unavailablerefresh_token200, always trueRefresh token revoked successfully
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.
200 ACCEPTED = taken into custody, 4xx/5xx REJECTED = nothing was queued.ACCEPTEDmetadata, payload, payloadFormat (only XML), payloadEncoding (only UTF-8), optional checksum.metadata.senderParticipantId belong to your tenant and are identical; the tenant has a valid licence.documentTypeId.documentTypeId + processId via AS4.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-Key, required)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.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.metadata.senderParticipantId0245:1234567890 or canonical form)XMLUTF-8 (default)payload, hex (64 characters)ACCEPTED — taken over, not deliveredREJECTEDcategory, code, message, details[], retryable, correlation_id| HTTP | errorCode | error.code | retryable | Cause |
|---|---|---|---|---|
| 400 | INVALID_REQUEST | SAPI-VAL-001 | no | missing/empty Idempotency-Key header (or > 200 characters) or X-Peppol-Participant-Id; the body is not valid JSON; invalid creationDateTime |
| 400 | INVALID_METADATA | SAPI-VAL-001 | no | metadata or one of the identifiers is missing, or contains control characters |
| 400 | PAYLOAD_MISSING | SAPI-VAL-001 | no | payload is missing (e.g. a body following the specification's document{content}) |
| 400 | INVALID_DOCUMENT_FORMAT | SAPI-VAL-001 | no | payloadFormat is not XML, payloadEncoding is not UTF-8, the checksum does not match |
| 400 | INVALID_DOCUMENT_FORMAT | SAPI-VAL-002 | no | payload is not well-formed XML |
| 400 | UNSUPPORTED_DOCUMENT_TYPE | SAPI-VAL-002 | no | no validation rules exist for the documentTypeId |
| 400 | DOCUMENT_VALIDATION_FAILED | SAPI-VAL-002 | no | XSD, Schematron, business rule or party errors; listed in error.details[] (field = location, issue = description, value = rule code) |
| 400 | RECEIVER_NOT_FOUND | SAPI-VAL-021 | no | the recipient is not in Peppol (on the sandbox: is not a sandbox participant) |
| 400 | UNSUPPORTED_DOCUMENT_TYPE | SAPI-VAL-021 | no | the recipient does not offer the document type and process via AS4 |
| 403 | AUTHORIZATION_FAILED | SAPI-AUTH-007 | no | the participant from the header or the sender does not belong to your tenant, or the two differ |
| 403 | AUTHORIZATION_FAILED | SAPI-AUTH-008 | no | the tenant has no valid licence |
| 409 | IDEMPOTENCY_CONFLICT | SAPI-VAL-010 | no | the key was used for a different request — use a new key |
| 409 | IDEMPOTENCY_IN_PROGRESS | SAPI-VAL-010 | yes | a request with the same key is being processed right now |
| 503 | SERVICE_UNAVAILABLE | SAPI-PROC-002 | yes | SML/SMP, validation or storage temporarily unavailable |
| 500 | PROCESSING_ERROR | SAPI-PROC-001 | yes | unexpected error |
/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.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).status=RECEIVED400 SAPI-VAL-001 — invalid pageToken, differing pageToken/nextPageToken, negative limit400 (Problem Details) — the header is missing or limit is not a number401 SAPI-AUTH-002, 403 SAPI-AUTH-007 — the participant does not belong to your tenant429 SAPI-RATE-001nextPageToken of the previous responseRECEIVED or ACKNOWLEDGEDiso6523-actorid-upis::…nullStandardBusinessDocument (an SBDH envelope with the business document) as an XML string.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.404 (empty body) — the ID is not a UUID or the document does not exist403 SAPI-AUTH-009 — the document is not addressed to the participant from the header400 SAPI-DOC-002 — not a received document of your tenant401 SAPI-AUTH-002, 429 SAPI-RATE-001documentId, documentTypeId, processId, senderParticipantId, receiverParticipantId, creationDateTime)StandardBusinessDocument as a UTF-8 XML stringXMLACKNOWLEDGED. Idempotent.status=RECEIVED200 with the original acknowledgement time404 (empty body) — the document does not exist or is not yours403 SAPI-AUTH-009 — the document is not addressed to the participant from the header409 SAPI-DOC-002 — it is a sent document, not a received onedocumentId from the list (path); no body is sentACKNOWLEDGED/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)./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./api/v1/documents/{id}/status or webhooks.<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.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./participants/lookup/ico translates it to a DIČ via the Register of Financial Statements.QUEUED → PROCESSING → DELIVERED (→ possibly FAILED after a negative MLS)PROCESSING → RETRYING → PROCESSING → … → DELIVERED or DEAD_LETTERSENT is in the code list but is not used for sending.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.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.RECEIVED (received and validated, waiting for acknowledgement) → ACKNOWLEDGED; SAPI does not show rejected documents. The acknowledgement is shared between the APEX API and SAPI.PEPPOL-EN16931-R*)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.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.GET /api/v1/documents/{id}/responses.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.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.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.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)./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.| Document | documentTypeId | processId |
|---|---|---|
| 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.1 | urn: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.1 | urn: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.1 | urn: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.1 | urn: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.1 | urn: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.1 | urn: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.1 | urn:fdc:peppol.eu:poacc:bis:mlr:3 |
| MLS (Message Level Status) — sent by APEX | urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:peppol:edec:mls:1.0::2.1 | urn:peppol:edec:mls |
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.
| A) Directly from ASOL IAM | B) Via SAPI | |
|---|---|---|
| Endpoint | POST https://iam.qasida.sk/oauth2/token | POST /sapi/v1/auth/token |
| Format | application/x-www-form-urlencoded | application/json |
| Access token | JWT, lifetime per expires_in | the same JWT from IAM |
| Refresh token | no — always get a new token via client_credentials | yes, 30 days, rotation via /sapi/v1/auth/renew |
| Recommended for | APEX API, Swagger UI (Authorize) | SAPI integrations according to SAPI-SK v1.0 |
client_credentials from IAM or /sapi/v1/auth/token
Authorization: Bearer <token> in every request
before expiry, a new client_credentials grant or /renew
/sapi/v1/auth/revoke for the refresh token
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.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
}
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..."
}
POST /sapi/v1/auth/renew
Content-Type: application/json
{ "refresh_token": "eyJhbGciOiJIUzI1NiJ9..." }
GET /api/v2/documents/inbox?status=VALIDATED Authorization: Bearer eyJhbGciOiJSUzI1NiIs... Accept: application/json
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.
/api/**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"
}
{
"timestamp": "2026-10-09T19:31:24.544",
"status": 401,
"error": "Unauthorized",
"message": "Missing or malformed Authorization: Bearer header",
"path": "/api/v1/documents/send"
}
/sapi/**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 | Meaning | What to do |
|---|---|---|
| 200 / 201 / 202 / 204 | Success; 202 = accepted for asynchronous processing | For 202, track the status (/status, notifications) |
| 400 | Invalid 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 |
| 401 | Missing, invalid or expired token (the header must start with exactly Bearer ) | Obtain a new token and retry the request once |
| 403 | The 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 |
| 404 | The resource does not exist or belongs to another tenant (deliberately not distinguished) | Check the ID and the environment |
| 409 | Idempotency conflict, the status does not allow the operation | Retry IDEMPOTENCY_IN_PROGRESS later; resolve the others |
| 413 | Batch too large | Split the batch |
| 415 | Wrong Content-Type | Send application/json (ZIP batch: application/zip) |
| 429 | Rate limit or batch intake capacity exceeded | Wait according to Retry-After, then back off exponentially |
| 500 / 503 | Unexpected or temporary error | Retry with exponential backoff and the same Idempotency-Key |
| Endpoints | Limit |
|---|---|
| POST /sapi/v1/document/send | 100 requests / min / IP (separate limit) |
| all other /api/** and /sapi/** (including /sapi/v1/auth/*) | 200 requests / min / IP (shared limit) |
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/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"
}
}
/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.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.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.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.
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.
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>"
}
{
"documentId": "550e8400-e29b-41d4-a716-446655440000",
"status": "QUEUED",
"submittedAt": "2026-10-09T10:30:00"
}
{
"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": []
}
{
"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"
}
{
"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"
}
{
"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"
}
[
{
"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"
}
]
{
"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" }
}
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
}
{
"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>"
}
curl -X POST "$APEX/api/v1/documents/inbox/550e8400-e29b-41d4-a716-446655440001/acknowledged" \ -H "Authorization: Bearer $TOKEN" HTTP/1.1 204 No Content
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>"
}
]
}
{
"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
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"
}
{
"providerDocumentId": "7c9d0e1f-2a3b-4c5d-8e6f-0a1b2c3d4e5f",
"status": "ACCEPTED",
"receivedAt": "2026-10-09T08:30:05.123456Z",
"timestamp": "2026-10-09T08:30:05.130Z"
}
{
"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"
}
}