SAPI-SK connector
The SAPI-SK connector is an alternative front door for integrators who already
generate their own UBL. Instead of posting structured JSON to
/api/v1/invoices and letting eFakturuj build the UBL, you send a ready-made
UBL 2.1 document and eFakturuj validates, delivers, and files it — while
remaining your Access Point and Service Provider.
Use it if your ERP or accounting system already emits compliant UBL. If you want eFakturuj to generate the UBL from your data, use the native API instead — start at Getting started.
SAPI routes are mounted at the API host root (/sapi), not under /api/v1:
- Production:
https://api.efakturuj.sk/sapi - Sandbox:
https://api.sandbox.efakturuj.sk/sapi
The base_url shown on the connector install screen is the API host —
append /sapi to it when configuring your ERP.
Sandbox delivery is a closed loop: documents sent in sandbox never reach a
real recipient's Access Point, even though the status reaches SENT. To test
delivery end-to-end, send between two sandbox companies — see
End-to-end delivery in sandbox.
Auth is a Bearer access token obtained from client credentials — not the
efk_ API key used by the native API.
The flow at a glance
POST /sapi/auth/token → access_token (15 min)
POST /sapi/document/send → 202 { providerDocumentId, status: ACCEPTED }
GET /sapi/document/status/{providerDocumentId} → ACCEPTED → PROCESSING → SENT
GET /sapi/document/receive → inbound documents
POST /sapi/document/receive/{id}/acknowledge → mark inbound as ACKNOWLEDGED
A single send performs the same full Slovak 5-corner exchange as the native API: the invoice to the buyer's Access Point, plus the SK Tax Data Document leg and the Finančná správa copy. You never build the TDD.
1. Get credentials
Install the SAPI-SK Connector for your company in the dashboard (Firmy → Apps → SAPI-SK Connector). Installing issues:
client_idclient_secret— shown once; store it securely- the connector
base_url - the participant ID the credentials are locked to (taken from the company's configured Peppol participant ID)
Credentials are scoped to one company and its participant ID. A send whose
metadata or X-Peppol-Participant-Id header names a different participant is
rejected with SAPI-AUTH-PARTICIPANT.
2. Get an access token
POST /sapi/auth/token takes a JSON body (not form encoding).
curl -X POST https://api.efakturuj.sk/sapi/auth/token \
-H 'Content-Type: application/json' \
-d '{
"client_id": "your-client-id",
"client_secret": "…",
"grant_type": "client_credentials"
}'
# → { "access_token": "…", "token_type": "Bearer", "expires_in": 900,
# "refresh_token": "…", "scope": "document:send document:receive" }
| Fact | Value |
|---|---|
| Access token lifetime | 900 s (15 minutes) |
| Refresh token lifetime | 30 days |
| Scopes | document:send, document:receive (both granted unless you request narrower) |
Optionally pass scope to narrow the grant. document:send covers both sending
and reading status; document:receive covers the inbound endpoints.
Renew with POST /sapi/auth/renew ({"refresh_token": "…"}) and revoke with
POST /sapi/auth/revoke ({"token": "…"}).
GET /sapi/auth/token/status reports whether the current token is still valid
and when to refresh — it sets should_refresh: true once fewer than 180
seconds remain.
3. Send a document
POST /sapi/document/send returns 202 Accepted. Two headers are
required:
Idempotency-Key— your unique key for this submissionX-Peppol-Participant-Id— must match your credentials' participant
The body carries the UBL as a plain UTF-8 XML string in payload — it is
not base64. Wire field names are camelCase.
curl -X POST https://api.efakturuj.sk/sapi/document/send \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 5f2b…' \
-H 'X-Peppol-Participant-Id: 9925:sk2020111111' \
-d '{
"metadata": {
"documentId": "2026-0001",
"documentTypeId": "busdox-docid-qns::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": "cenbii-procid-ubl::urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
"senderParticipantId": "9925:sk2020111111",
"receiverParticipantId": "9925:sk2020222222",
"creationDateTime": "2026-07-16T10:30:00Z"
},
"payload": "<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">…</Invoice>",
"payloadFormat": "XML",
"checksum": "…"
}'
# → 202 { "providerDocumentId": "efk_sapi_doc_…", "status": "ACCEPTED",
# "receivedAt": "…", "timestamp": "…" }
Save providerDocumentId — it is your handle for status lookups.
| Field | Notes |
|---|---|
payload | Plain UTF-8 XML string. Max 10 MB. |
payloadFormat | "XML" — the only accepted value. |
payloadEncoding | Optional, defaults to UTF-8. Only UTF-8 is supported. |
checksum | Optional. Lowercase SHA-256 hex of the payload bytes. If present it must match, or you get SAPI-VAL-CHECKSUM. |
metadata.documentTypeId | Must agree with the UBL root element — an Invoice payload declared as a CreditNote is rejected. |
Only UBL Invoice and CreditNote roots are accepted. The declared
senderParticipantId / receiverParticipantId must match the EndpointID
values inside the UBL.
Idempotency
Idempotency-Key is required and scoped to your SAPI client. Replaying the same
key with the same body returns the original response. Replaying it with a
different body fails with SAPI-IDEMPOTENCY-CONFLICT.
4. Track delivery
curl https://api.efakturuj.sk/sapi/document/status/efk_sapi_doc_… \
-H 'Authorization: Bearer <access_token>' \
-H 'X-Peppol-Participant-Id: 9925:sk2020111111'
# → { "providerDocumentId": "…", "clientDocumentId": "2026-0001",
# "status": "SENT", "direction": "outbound",
# "peppolMessageId": "…", "fs": { "status": "ACKNOWLEDGED", "submissionId": "…" },
# "updatedAt": "…", "timestamp": "…" }
Outbound status walks:
ACCEPTED → PROCESSING → SENT (or FAILED)
The fs object reports the Finančná správa leg separately:
NOT_SUBMITTED, SUBMITTED, ACKNOWLEDGED, or FAILED, with
submissionId, reportDeadline, acknowledgedAt, and errorMessage.
5. Receive inbound documents
curl 'https://api.efakturuj.sk/sapi/document/receive?limit=20' \
-H 'Authorization: Bearer <access_token>' \
-H 'X-Peppol-Participant-Id: 9925:sk2020222222'
# → { "documents": [ { "documentId": "efk_sapi_in_…", "status": "RECEIVED",
# "metadata": {…}, "receivedAt": "…" } ],
# "nextPageToken": "…" }
Paginate with pageToken; filter with status. Fetch one document — the
response carries the full UBL in payload:
curl https://api.efakturuj.sk/sapi/document/receive/efk_sapi_in_… \
-H 'Authorization: Bearer <access_token>' \
-H 'X-Peppol-Participant-Id: 9925:sk2020222222'
Acknowledge it. The request takes no body:
curl -X POST https://api.efakturuj.sk/sapi/document/receive/efk_sapi_in_…/acknowledge \
-H 'Authorization: Bearer <access_token>' \
-H 'X-Peppol-Participant-Id: 9925:sk2020222222'
# → { "documentId": "…", "status": "ACKNOWLEDGED", "acknowledgedDateTime": "…" }
Acknowledgement is idempotent — re-acknowledging keeps the original timestamp.
Errors
SAPI errors use their own envelope — not the native API's error shape:
{
"error": {
"category": "VALIDATION",
"code": "SAPI-VAL-001",
"message": "Document payload is not well-formed XML.",
"retryable": false,
"correlation_id": "…",
"details": [{ "field": "payload", "issue": "…" }]
}
}
category is one of AUTH, VALIDATION, PROCESSING, QUOTA, TEMPORARY,
PERMANENT. Retry only when retryable is true. Quote correlation_id in
support requests.
| Code | Meaning |
|---|---|
SAPI-AUTH-001 | Authentication failed (401) |
SAPI-AUTH-LOCKED | Client locked after repeated auth failures (15 minutes) |
SAPI-AUTH-PARTICIPANT | Participant not permitted for these credentials |
SAPI-AUTH-SCOPE / SAPI-VAL-SCOPE | Token lacks the required scope |
SAPI-AUTH-IP | Request from an IP outside the client's allowlist |
SAPI-VAL-001 | Generic validation failure (bad XML, unsupported root, metadata mismatch, missing header) |
SAPI-VAL-CHECKSUM | checksum does not match the payload |
SAPI-VAL-VALIDATION-UNAVAILABLE | Validator bundles unavailable and strict validation is on — fail-closed |
SAPI-IDEMPOTENCY-CONFLICT | Idempotency-Key reused with a different body |
SAPI-DOC-NOT-FOUND | Unknown document id (404) |
SAPI-QUOTA-INVOICE-LIMIT | Monthly plan invoice quota exceeded (402) |
SAPI-DISABLED | SAPI facade disabled (503, retryable) |
Quotas
Sends count against your plan's monthly invoice limit, the same counter the
native API uses. Exceeding it returns 402 with SAPI-QUOTA-INVOICE-LIMIT
and the plan tier and limit in details — unless your plan permits overage, in
which case the send proceeds and is billed.
Testing
Use the sandbox host (https://api.sandbox.efakturuj.sk/sapi). It has the same
shape and runs against isolated test data without delivering to the real Peppol
network or billing.
Next steps
- Getting started — the native API alternative
- Sandbox · Webhooks · Errors