Introduction
The Verteco API is REST over HTTPS. Both request and response are JSON (UTF-8). Start by creating an account, then an API token, and make your first call within a minute. All paths are relative to the base URL below.
The complete test environment (sandbox) is available at test.peppol.verteco.digital. It behaves the same as production and additionally offers test tools (webhook of the Financial Administration of the Slovak Republic (Finančná správa, FS), verification token (Verifikačný údaj), company removal) and a simulated provider selection instead of the FS portal (VPDS).
- Base URL
https://peppol.verteco.digital/api/v1- Version
- v1 (in the path). Backward-incompatible changes will ship under a new version.
- Protocol
- HTTPS only · TLS 1.2+ · TLS A+
- Format
- application/json (UTF-8)
- Auth
- cookie session (portal) or Bearer token (server-to-server)
- Timestamps
- ISO-8601 (UTC), e.g. 2026-06-03T09:40:27Z
Certified AP
A real Peppol Access Point (Seat ID PSK001128), not a reseller. We passed OpenPeppol conformance (19/19) and are in production.
Valid documents
Server-side EN 16931 + Peppol BIS 3.0 Schematron validation of every received AND sent document. You can also validate a document in advance: the public validator runs at /validator (and as an API at POST /api/v1/public/peppol-validate); errors are returned with the specific rule (e.g. BR-CO-15).
Multi-tenant
One account and one token for N companies, ideal for SaaS, ERP and accountants.
SK TDD automatically
We generate and send the tax report (corner-5 / TDD) to the Financial Administration (FS) on your behalf.
openapi-generator. It covers both the portal API and SAPI-SK.Quick start
You create both the account and the API token in the portal (via the browser). The integration then runs exclusively through the API token. Your app does not need to handle registration, login or passwords.
- 1
Create an account in the portal
Sign up with your e-mail and confirm it via the link in the e-mail. - 2
Generate an API token
In the portal, go to API tokeny → Vytvoriť (API tokens → Create). Thevpt_…token is shown only once. Store it securely. - 3
Make your first call
Use the token in theAuthorizationheader:javascriptconst res = await fetch('https://peppol.verteco.digital/api/v1/companies', { headers: { Authorization: 'Bearer vpt_8f2a…' }, }); const companies = await res.json();
Test environment (sandbox)
In addition to the SAPI mock sandbox (below), we also operate a full-featured test environment, a complete copy of this portal with separate data, where you can try the entire flow (registration → company → sending → receiving → notifications) end-to-end without any impact on production.
- Registration
- no confirmation e-mail; the account is usable immediately
- Company approval
- automatic, directly in the UI or via the API; there is no provider selection on the portal of the Financial Administration of the Slovak Republic (Finančná správa, FS) and no FS portal login; right after creating a company with a VAT ID (IČ DPH) you can both send and receive
- Network registration
- automatic, into the Peppol test network. Note the two layers of the identifier: the API and the portal use the same format as production (
peppolParticipantId = 0245:<DIČ digits>, where DIČ is the Slovak tax identification number; your integration code does not change); in the test SMP/SML the company is technically registered as9950:SK<DIČ>, because the 0245 scheme requires a Financial Administration verification code that does not exist in the test environment. In production,0245:<DIČ digits>is registered in the live Peppol network only after the provider selection on the FS portal (login with eID or the portal credentials) - Validation
- real: EN 16931 + Peppol BIS 3.0 rules, the same as in production
- Delivery
- real, over the Peppol test network (AS4, test certificate, test SML/SMP): a recipient registered in the test network receives the invoice as a received invoice (including the e-mail, PDF and webhook); nothing leaves for the live network
- Delivery receipt (MLS)
- a real MLS receipt from the test network
- E-mails
- are really sent (to the addresses you enter), with the
[TEST]prefix - Price
- free, with no limits for testing
Everything that works here works there: the portal, the REST API, SAPI-SK 1.0, e-shop plugins and webhooks. Just switch the domain in your integration to test.peppol.verteco.digital and use the tokens created in the test environment. Ideal for integration development, CI tests and training accountants before the production rollout.
Note: the edge protection of the test domain blocks the generic headers User-Agent: Python-urllib and User-Agent: Java/1.8.x (the default User-Agent of HttpsURLConnection in Java 8) with HTTP 403 "error code: 1010" before the request even reaches our API; the production domain peppol.verteco.digitallets them through. Set your own User-Agent: in Java either with the JVM flag -Dhttp.agent=my-app/1.0 (no code change; Java appends "Java/1.8", and the resulting "my-app/1.0 Java/1.8.0_xxx" passes, only a header starting with "Java/1.8" is blocked) or on the connection with conn.setRequestProperty("User-Agent", "my-app/1.0"). Common clients (requests, httpx, Java 11+, Apache HttpClient, okhttp, axios, Go, PHP, curl) work unchanged.
Why companies are approved automatically
The Financial Administration has no test environment for the VPDS portal: provider selection on vpds.financnasprava.sk runs only in production and is verified by logging in to the FS portal (eID or FS portal credentials), so in production you cannot “select” an arbitrary company that is not your own. So that you can still test your integration, we simulate this step in the test environment: every company you create is approved automatically (without selection at FS), so you can set up both a sender and a recipient and go through the entire flow.
For testing FS webhooks (provider selection) we have our own counterpart to the Financial Administration tool: test.peppol.verteco.digital/sandbox-nastroje. There you can generate a valid FS-style verification token (Verifikačný údaj) and send a complete webhook to your endpoint, exactly as the FS portal does during a real selection. Companies created for testing can in turn be deregistered (from the portal and from the test SMP), precisely because of the missing test mode on the Financial Administration side.
Authentication
Your integration authenticates with an API token in the header Authorization: Bearer vpt_…. You create the token in the portal (API tokens); it has the format vpt_ + 40 hex characters, we store only its SHA-256 hash, and it has the same access as your account. Test it via /auth/me:
curl https://peppol.verteco.digital/api/v1/auth/me -H 'Authorization: Bearer vpt_8f2a…'
# 200 → { "id": "…", "email": "vy(at)firma.sk" } (token works)With a missing or invalid token the API returns:
// 401 Unauthorized
{ "error": "unauthorized", "message": "Authentication required" }portal_session, JWT, 7 days); for a server-to-server integration you do not need it. Public without auth are /ping, /openapi.json and the endpoints under /public/* (the peppol-check lookup, the peppol-validate validator, see the section Public tools for details, and the service status via status); everything else requires a session or a Bearer token.Conventions & formats
| Type | Format | Example |
|---|---|---|
id | UUID (string) | 08dd6c1e-… |
timestamps | ISO-8601 Instant (UTC) | 2026-06-03T09:40:27Z |
issueDate | date (YYYY-MM-DD) | 2026-06-03 |
totalAmount | decimal number | 120.00 |
missing values | null (not an omitted field) | "dic": null |
Pagination & idempotency
/companies and /companies/{id}/documents support optional ?page&limit (the response remains a JSON array; the total count is in the X-Total-Count / X-Total-Pages headers). Without parameters they return the whole array, documents sorted by createdAt descending; /tokens always returns the whole array. The Idempotency-Key header is supported by the national interface SAPI-SK on POST /sapi/document/send; for programmatic sending, use exactly that.Errors
Errors have a uniform shape with a machine-readable error code. Validation errors add a fields map (first error per field).
// business error
{ "error": "invalid_credentials", "message": "Invalid email or password" }
// validation error (400); field messages are returned in Slovak ("IČO musí byť 8 číslic" = "IČO must be 8 digits")
{ "error": "validation_failed", "message": "Some fields are invalid",
"fields": { "ico": "IČO musí byť 8 číslic" } }HTTP statuses
- 200 / 201 / 204
- success (OK / Created / No Content)
- 400
- invalid input (see error / fields)
- 401
- missing or invalid authentication
- 403
- insufficient permission (role)
- 404
- the resource does not exist or you have no access to it
- 405
- HTTP method not supported for this path
- 409
- conflict (IČO / e-mail already exists)
- 415
- unsupported Content-Type (XML endpoints expect application/xml)
- 429
- rate limit exceeded (RateLimit-* and Retry-After headers, JSON body rate_limited)
error field. A response without a JSON body (HTML or text such as error code: 1010 with status 403, or 502/504 during a deployment) did not come from the API but from the infrastructure in front of it (edge protection, gateway). The typical cause of 403/1010 is a generic library User-Agent (Python-urllib, Java/1.8.x on the test domain); the fix is described in the Test environment section.Complete error catalog
| Code | HTTP | When |
|---|---|---|
validation_failed | 400 | the body failed validation (see fields) |
unauthorized | 401 | missing or invalid API token |
forbidden | 403 | the action requires the owner/admin role |
company_not_found | 404 | the company does not exist or you are not a member of it |
ico_taken | 409 | a company with this IČO (company registration number) already exists |
ico_immutable | 400 | the IČO cannot be changed |
company_dic_missing | 400 | sending verification without the company DIČ (Slovak tax identification number) |
token_invalid | 400 | the verification token (Verifikačný údaj, signature) does not match |
document_not_found | 404 | the document does not exist |
token_not_found | 404 | the API token does not exist / is not yours |
rate_limited | 429 | too many requests |
Rate limits
The API is protected by rate limiting in a 60-second window (per instance). The limit and the remaining quota are returned in every response via headers; when exceeded, the API returns 429 Too Many Requests with Retry-After.
- Regular calls
- dynamic limit with a large headroom, per API token (or session, otherwise IP); the current value is returned in the RateLimit-Limit header
- Auth (/auth/*)
- stricter limit per IP (anti brute-force; except /auth/me and /auth/logout)
RateLimit-Limit- limit in the window
RateLimit-Remaining- how many requests remain
RateLimit-Reset- seconds until the window resets
Retry-After- seconds until the next attempt (on 429)
HTTP/2 429 Too Many Requests
RateLimit-Limit: <limit in the window>
RateLimit-Remaining: 0
RateLimit-Reset: 37
Retry-After: 37
{ "error": "rate_limited", "message": "Too many requests. Slow down." }- Limits are bound to the credential, not the IP: 300 calls per minute per API token (SAPI: per access token), auth endpoints 20 per minute per IP and a protective ceiling of 600 per minute per IP on /api/v1. A server-side integration behind one IP is not affected; higher limits are set on request, tell us your expected peak.
- Polling is a fully supported path: GET /sapi/document/sent and /receive with ?since and ?until every 1 to 5 minutes; webhooks are a complement, not a requirement.
- Timestamps: time sent = statusDateTime of the delivered status in /sapi/document/sent (confirmed by the MLS receipt), time received = creationDateTime in /sapi/document/receive; FS report hand-over time = fsReportedAt (webhook invoice.reported or the fsReportedAt field of invoice.* events); every webhook carries occurredAt and eventId.
- PDF: the API returns printable HTML (…/html, portal ?format=html) from which a PDF prints in a browser or headless Chrome; there is no separate PDF endpoint.
- C#/.NET and other languages: generate the client from OpenAPI (NSwag, Kiota, openapi-generator); we do not ship our own NuGet package.
API tokens
Opaque vpt_… tokens for server-to-server access. The plaintext is shown only once at creation. Save it. We store only the SHA-256 hash and a 12-character prefix for display.
/tokenssession / tokenList of your active tokens (without the secret), createdAt descending.
/tokenssession / tokenCreates a token; returns the plaintext (once only).
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | token name, max 128 |
curl -X POST https://peppol.verteco.digital/api/v1/tokens -b cookies.txt \
-H 'Content-Type: application/json' -d '{"name":"moja-appka"}'
# 201 Created
{ "id":"…","name":"moja-appka","token":"vpt_8f2a…","prefix":"vpt_8f2a3b…","createdAt":"…" }/tokens/{id}session / tokenRevokes the token. Returns 204.
Errors: token_not_found (404)
Companies
Companies, identified by company registration number (IČO) / VAT ID (IČ DPH), that you manage. Access is tied to membership: you only see companies you are a member of. The creator of a company becomes its owner.
/companiessession / tokenList of companies you are a member of (with your role).
/companiessession / tokenAdds a company; you become its owner, status = pending_verification.
| Field | Type | Required | Description |
|---|---|---|---|
ico | string | yes | exactly 8 digits |
dic | string | yes | VAT ID as SK + 10 digits (e.g. SK2121358349); a non-VAT payer sends the bare 10-digit DIČ and we prepend SK |
legalName | string | yes | legal (business) name, max 500 |
registeredAddress | string | no | registered office address, max 1000 |
curl -X POST https://peppol.verteco.digital/api/v1/companies \
-H 'Authorization: Bearer vpt_8f2a…' -H 'Content-Type: application/json' \
-d '{"ico":"53412834","dic":"SK2121358349","legalName":"Verteco digital services, s. r. o."}'
# 201 Created
{ "id":"08dd…","ico":"53412834","dic":"SK2121358349","legalName":"…",
"registeredAddress":null,"peppolParticipantId":null,
"status":"pending_verification","role":"owner","createdAt":"2026-06-03T09:40:27Z" }Errors: ico_taken (409) · validation_failed (400)
/companies/{id}session / tokenCompany detail (you must be a member).
/companies/{id}owner / adminUpdates a company. The IČO is immutable (it must equal the existing value).
Errors: forbidden (403) · ico_immutable (400) · company_not_found (404)
The status field
pending_verification- after creation; sending is not yet unlocked
active- verified, sending unlocked
Member role
owner- company creator (full access)
admin- administrator (edits, webhooks, verification)
member- member (read)
viewer- read-only
owner and admin roles (company manager).Sending verification (verification token, Verifikačný údaj)
Before sending, the company must be verified using the verification token (Verifikačný údaj, VÚ), a signed token issued by the Financial Administration of the Slovak Republic (Finančná správa, FS). After successful verification, the status of the company changes to active and sending is unlocked.
/companies/{id}/verificationowner / adminSelf-verification of the verification token (Verifikačný údaj). On success, sets status to active.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | VÚ = hex signature (prod/pPFS 1024 hex, test/tPFS 768 hex) |
curl -X POST https://peppol.verteco.digital/api/v1/companies/{id}/verification \
-H 'Authorization: Bearer vpt_8f2a…' -H 'Content-Type: application/json' \
-d '{"token":"<1024-hex VÚ>"}'
# 200 OK
{ "companyId":"08dd…","status":"active","sendingVerified":true,
"verificationMethod":"self","verifiedAt":"2026-06-15T…Z" }Errors: company_dic_missing (400) · token_invalid (400) · forbidden (403)
Documents
Log of Peppol documents (received and sent) for a company. It fills up as invoices flow through the Access Point. Tied to company membership, sorted by createdAt descending.
/companies/{id}/documentssession / tokenList of the company's documents. Optional filter ?direction=sent|received.
curl 'https://peppol.verteco.digital/api/v1/companies/{id}/documents?direction=received' \
-H 'Authorization: Bearer vpt_8f2a…'
# 200 OK
[ { "id":"…","direction":"received","peppolMessageId":"…","docTypeId":"…",
"senderId":"0088:7300010000001","receiverId":"0245:2121358349",
"invoiceNumber":"2026001","issueDate":"2026-06-03",
"currency":"EUR","totalAmount":120.00,"status":"received","createdAt":"…" } ]/companies/{id}/documents/{docId}session / tokenDetail of a single document.
Errors: document_not_found (404)
/companies/{id}/documents/{docId}/downloadsession / tokenStandalone printable HTML of the invoice. ?inline=1 → displays in the browser, otherwise downloads.
Returns text/html, with Content-Disposition naming the file faktura-<číslo>.html (číslo = the invoice number).
SAPI-SK 1.0 (national interface)
SAPI-SK is the standardized national REST interface between a client/ERP system and an Access Point (sapi-sk.sk). We implement it in full. This means you are not tied to our proprietary API shape, and you write the integration once for any SAPI-SK Access Point.
- Base URL
https://peppol.verteco.digital/sapi- Authentication
- OAuth2 client_credentials → short-lived access token (JWT)
- client_id
- UUID of your API token (listed in the API tokens section / dashboard)
- client_secret
- the
vpt_…token itself from the portal - Version
- 1.3 (10 operations: 4× auth, 6× documents)
vpt_ token nor a session). Revoking the API token in the portal immediately invalidates both /auth/token and /auth/renew.Sandbox (trial environment)
Want to try SAPI-SK without registering and without any risk? Use the public sandbox credentials. The sandbox validates requests exactly like production, but never sends anything to the Peppol network and does not work with real data; it returns realistic mock responses. Ideal for development, CI and integration onboarding.
- client_id
sandbox- client_secret
sandbox- send
- full contract validation + mock 202 (nothing is delivered)
- receive
- 1 sample document
sandbox-doc-0001for testing parsing and acknowledge
# 1) sandbox token (no registration required)
curl -X POST https://peppol.verteco.digital/sapi/auth/token \
-H 'Content-Type: application/json' \
-d '{ "client_id": "sandbox", "client_secret": "sandbox",
"grant_type": "client_credentials" }'
# 2) mock send: it is validated, but NOTHING is actually delivered
curl -X POST https://peppol.verteco.digital/sapi/document/send \
-H 'Authorization: Bearer <sandbox access_token>' \
-H 'X-Peppol-Participant-Id: 0245:0000000000' \
-H 'Content-Type: application/json' \
-d '{ "metadata": { "documentId": "TEST-1",
"documentTypeId": "urn:…::Invoice##…::2.1",
"senderParticipantId": "0088:sandbox-sender",
"receiverParticipantId": "0088:sandbox-receiver" },
"payload": "<Invoice>…</Invoice>", "payloadFormat": "XML" }'
# 202 { "providerDocumentId": "sandbox-…", "status": "ACCEPTED", … }
# 3) sample inbox + detail of the sample document
curl https://peppol.verteco.digital/sapi/document/receive \
-H 'Authorization: Bearer <sandbox access_token>'
curl https://peppol.verteco.digital/sapi/document/receive/sandbox-doc-0001 \
-H 'Authorization: Bearer <sandbox access_token>'client_id/client_secret from the portal (below).Authentication
/sapi/auth/tokenclient_credentialsExchanges client_id + client_secret for an access token (15 min) and a refresh token (30 days). Store the token and use it for the full 15 minutes: requesting a new token on every call is unnecessary overhead (the request limit also counts calls to /auth/token).
curl -X POST https://peppol.verteco.digital/sapi/auth/token \
-H 'Content-Type: application/json' \
-d '{ "client_id": "<token UUID>", "client_secret": "vpt_8f2a…",
"grant_type": "client_credentials" }'
# 200 OK
{ "access_token": "eyJhbGciOi…", "token_type": "Bearer",
"expires_in": 900, "refresh_token": "eyJhbGciOi…" }Errors: SAPI-AUTH-001 (401) · SAPI-AUTH-003 (401: IP outside the key allowlist) · SAPI-VAL-001 (400)
/sapi/auth/token/statusBearer (access)Validity and expiry of the access token; should_refresh = true when less than 3 min remain.
/sapi/auth/renewrefresh tokenIssues a new access + refresh token. Fails if the underlying API token has been revoked.
// body
{ "refresh_token": "eyJhbGciOi…" }/sapi/auth/revoke–Always returns success (RFC 7009). The permanent kill switch is revoking the API token in the portal.
Sending a document
/sapi/document/sendBearer (access)Sends a Peppol business document (UBL / BIS 3.0) to the recipient through our Access Point. Sending is fail-closed: the company must have a verified verification token (Verifikačný údaj).
Required headers:
| Header | Description |
|---|---|
Authorization | Bearer <access_token> |
X-Peppol-Participant-Id | the participant you are sending on behalf of (e.g. 0245:2121358349, the digits of the DIČ (Slovak tax identification number) without the "SK" prefix) |
Idempotency-Key | unique key per send; a repeated call returns the original result and never delivers twice. Exception: if the first attempt was rejected before anything left the access point (recipient not registered in the Peppol network, validation error, incomplete request), the same key performs the send again, so "fix and resend" works under the same invoice number |
curl -X POST https://peppol.verteco.digital/sapi/document/send \
-H 'Authorization: Bearer eyJ…' \
-H 'X-Peppol-Participant-Id: 0245:2121358349' \
-H 'Idempotency-Key: 7b1f0e2a-…' \
-H 'Content-Type: application/json' \
-d '{ "metadata": {
"documentId": "INV-2026-001",
"documentTypeId": "urn:…::Invoice##…::2.1",
"processId": "urn:…:bis:billing:3.0",
"senderParticipantId": "0245:2121358349",
"receiverParticipantId": "0088:7300010000001",
"creationDateTime": "2026-06-17T10:00:00Z" },
"payload": "<Invoice …>…</Invoice>",
"payloadFormat": "XML" }'
# 202 Accepted
{ "providerDocumentId": "…", "status": "ACCEPTED",
"receivedAt": "2026-06-17T10:00:01Z", "timestamp": "…" }
# when status is "REJECTED", the response also carries a "detail" field
# with the rejection reason (validation rules, e.g. BR-CO-15)metadata are routing and technical fields: documentId is your internal identifier, not the invoice number. Business data (invoice number cbc:ID, issue date, due date, delivery date, currency, amount) is taken directly from the UBL payload; you do not send any of it in metadata, and UBL is always the source of truth for display in the portal and for webhooks.Errors: SAPI-AUTH-002 (401) · SAPI-AUTH-003 (403) · SAPI-VAL-001 (400) · SAPI-PROC-001 (502) · SAPI-PROC-002 (503) · SAPI-PROC-500 (500: do not retry, report the correlation_id)
202and the verdict is in the body (status ACCEPTED or REJECTED). If you want the HTTP status to carry the verdict too, send Prefer: handling=strict (RFC 7240): a synchronous rejection then comes back as 422 with the SAPI error envelope (SAPI-VAL-002 for a validation failure, SAPI-RES-003 when the recipient cannot be delivered to; details[] carry providerDocumentId and detail) and the response says Preference-Applied: handling=strict. ACCEPTED is unchanged. Before handing the document to the Access Point we run the same SML/SMP lookup it would. A recipient that is not in the Peppol network at all means ACCEPTED with undeliverable: true: we took the document and report it to the Financial Administration regardless of delivery (§ 85o ods. 11, FS FAQ 9/DPH/2025/IM ex. 9), but nobody receives it; it shows status undeliverable on GET /sapi/document/sent, the webhook gets invoice.undeliverable, no e-mail is sent and the same Idempotency-Key sends again once the recipient registers. A recipient that is in the network but does not publish the document type gets REJECTED immediately, with no delivery attempt. retrying: true on an ACCEPTED response means the first delivery attempt failed (for example a temporarily unavailable SMP) and the Access Point retries on its own, typically within 20 minutes; detail carries the reason and the final verdict arrives via GET /sapi/document/sent or the webhook./sapi/document/validateBearer (access)Validates a document without sending it: the same EN 16931 + Peppol BIS 3.0 rules the Access Point applies before dispatch. Nothing is stored or sent; works with the sandbox token too. Body = the JSON pair { payload, payloadFormat } used by /document/send, or the raw XML (Content-Type: application/xml). Limit 10 MB.
curl -X POST https://peppol.verteco.digital/sapi/document/validate \
-H 'Authorization: Bearer eyJ…' \
-H 'Content-Type: application/xml' \
--data-binary @invoice.xml
# 200 OK
{ "valid": false,
"errors": [ "BR-CO-15: Invoice total amount with VAT (BT-112) = … " ],
"warnings": [],
"checkedAt": "2026-09-10T12:00:00Z" }Errors: SAPI-AUTH-002 (401) · SAPI-VAL-001 (400: empty body, invalid JSON, payloadFormat other than XML, > 10 MB) · SAPI-SYS-002 (502: validator temporarily unavailable, retry)
Receiving documents
/sapi/document/receiveBearer (access)List of received documents (oldest first); the metadata also carries invoiceNumber, so you can identify a document without downloading the payload. Query: ?pageToken, ?limit (max 200), ?status (received / acknowledged; case-insensitive, any other value returns error SAPI-VAL-001), ?invoiceNumber (exact match on cbc:ID), ?since and ?until (ISO-8601 instant; window by time of receipt, e.g. documents from the last 5 days, bulk download for external archiving or reconstruction of accounting records), ?deliveryDateFrom and ?deliveryDateTo (ISO-8601 date; filter by the delivery date stated in the document). The X-Peppol-Participant-Id header is required.
// 200 OK
{ "documents": [ { "documentId":"…","documentTypeId":"…",
"senderParticipantId":"0088:…","receiverParticipantId":"0245:…",
"creationDateTime":"2026-06-17T…Z" } ],
"nextPageToken": "50" }/sapi/document/receive/{documentId}Bearer (access)Detail including the payload (raw XML, exactly as it arrived via Peppol).
Errors: SAPI-RES-001 (404) · SAPI-RES-002 (404: payload not archived)
/sapi/document/receive/{documentId}/acknowledgeBearer (access)Confirms that your system has taken over the document. Idempotent.
/sapi/document/receive/{documentId}/xmlBearer (access)Archive link (metadata.links.xml): the business document itself as an XML file, same content as the payload in the detail.
/sapi/document/receive/{documentId}/htmlBearer (access)Archive link (metadata.links.html): generic printable rendering of the invoice (HTML).
/sapi/document/receive/{documentId}/pdfBearer (access)Archive link (metadata.links.pdf): the PDF of the document. When the supplier embedded their own invoice PDF in the XML (BT-125) you get that original; otherwise a PDF rendered on demand from the stored XML (no PDF is stored). Header X-Verteco-Pdf-Source: supplier | generated. 404 SAPI-RES-002 once the content was wiped, 503 while the renderer is unavailable (retry or fall back to /html).
/sapi/document/receive/{documentId}/attachments/{index}Bearer (access)Archive link (metadata.links.attachments[].url): the bytes of one attachment embedded in the document, e.g. the supplier's PDF original. Forced download.
/sapi/document/receive/{documentId}/public-linkBearer (access)No-sign-in link (optional): issues a NEW random key for the document ({"rotate":true} replaces the existing one). Returns url (landing page), xmlUrl, htmlUrl, pdfUrl and expiresAt; the key is shown once. The company must have "download without sign-in" enabled (403 SAPI-AUTH-004 otherwise). DELETE revokes it.
Status of sent documents
/sapi/discovery?receiverId=0245:2121358349Preflight before sending: is the recipient registered in the Peppol network, and which document types can they receive? The same SML/SMP lookup the Access Point performs; receiverId accepts a Peppol ID, a VAT ID (IČ DPH) or the bare DIČ. Response: {registered, participantId, smp, documentTypes, checkedAt}. The X-Peppol-Participant-Id header is not needed here.
/sapi/document/sentBearer (access)Delivery status of sent documents: pending (being handed over to the network) → submitted → delivered / rejected (confirmed by the delivery receipt (MLS) from the recipient's AP); failed = transport error (a retry with the same Idempotency-Key sends again). Query: ?pageToken, ?limit, ?status (pending / submitted / sent / delivered / rejected / failed; case-insensitive, any other value returns error SAPI-VAL-001), ?invoiceNumber (exact match on cbc:ID: the status of a specific invoice in a single call), ?since and ?until (ISO-8601 instant; time window), ?deliveryDateFrom and ?deliveryDateTo (ISO-8601 date; filter by delivery date).
// 200 OK
{ "documents": [ {
"documentId": "…",
"receiverParticipantId": "0245:1084695645",
"peppolMessageId": "befc9112-…",
"status": "delivered",
"statusDateTime": "2026-07-09T14:52:31Z",
"creationDateTime": "2026-07-09T14:52:12Z" } ],
"nextPageToken": null }Errors: SAPI-AUTH-002 (401) · SAPI-AUTH-003 (403) · SAPI-VAL-001 (400: invalid since)
Batch sending
/sapi/document/batchBearer (access)Up to 100 documents in a single call. Each item goes through the full single-send logic (idempotency via itemId + idempotencyKey, reservation, submit, verdict) and returns its own result; a failure of one item does not stop the others. Items are processed sequentially; retry failed items with the same idempotencyKey.
// request
{ "documents": [ {
"itemId": "fa-2026-001",
"idempotencyKey": "fa-2026-001",
"metadata": { "documentId": "2026001", "documentTypeId": "…", "processId": "…",
"senderParticipantId": "0245:2121358349", "receiverParticipantId": "0245:1084695645" },
"payload": "<Invoice …>", "payloadFormat": "XML" } ] }
// 202 Accepted
{ "total": 2, "accepted": 1, "rejected": 1, "failed": 0,
"results": [
{ "itemId": "fa-2026-001", "ok": true, "providerDocumentId": "…", "status": "ACCEPTED" },
{ "itemId": "fa-2026-002", "ok": false, "errorCode": "SAPI-AUTH-003", "errorMessage": "…" } ] }Errors: SAPI-AUTH-002 (401) · SAPI-AUTH-003 (403) · SAPI-VAL-001 (400: empty or oversized list)
Error model
All SAPI errors share a uniform envelope with a category, a stable code, a retryable flag and a correlation_id for support.
{ "error": {
"category": "AUTH",
"code": "SAPI-AUTH-001",
"message": "Invalid client credentials.",
"retryable": false,
"correlation_id": "b4191dfc-…" } }Notifications & webhooks
For a received invoice we can notify the company by e-mail or by webhook (POST to your URL). Delivery is durable: it is stored in an outbound queue and delivered asynchronously (15 s timeout); on failure we retry up to 8× with exponential backoff (30 s → max 1 h), then dead-letter. An outage of your server therefore does not lose the notification; we deliver it on the next attempt. SSRF protection blocks loopback/local addresses; the webhook URL must be public.
/companies/{id}/notificationssession / tokenNotification settings: { webhookUrl, notificationEmail, hasSecret }.
/companies/{id}/notificationsowner / adminSets both channels (an empty string disables the given channel).
| Field | Type | Required | Description |
|---|---|---|---|
webhookUrl | string | no | empty or http(s) URL, max 512 |
notificationEmail | string | no | valid e-mail, max 256 |
/companies/{id}/webhooksession / tokenWebhook configuration: { url, hasSecret } (without the secret).
/companies/{id}/webhookowner / adminSets the webhook URL (auto-generates a signing secret if none exists yet).
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | http(s) URL, max 512 |
curl -X PUT https://peppol.verteco.digital/api/v1/companies/{id}/webhook \
-H 'Authorization: Bearer vpt_8f2a…' -H 'Content-Type: application/json' \
-d '{"url":"https://vas-system.sk/peppol/webhook"}'
# 200 OK { "url":"https://vas-system.sk/peppol/webhook", "hasSecret": true }/companies/{id}/webhookowner / adminRemoves both the webhook URL and the secret. Returns 204.
/companies/{id}/webhook/secretowner / adminGenerates a new signing secret and returns it ONCE; store it for verifying X-Verteco-Signature.
// 200 OK
{ "secret": "vpt_8f2a…" }/companies/{id}/webhook/testowner / adminSynchronously sends a test notification (event webhook.test, signed, SSRF-protected) to the stored URL and returns WHAT it sent and WHAT came back. The same is triggered by the „Otestovať webhook“ (Test webhook) button in the company detail.
// 200 OK
{
"sent": { "url": "https://vas-system.sk/peppol/webhook", "event": "webhook.test",
"signed": true, "payload": "{…}" },
"received": { "status": 200, "body": "OK", "durationMs": 142, "error": null }
}
// 400 webhook_not_configured if no webhook URL is storedWebhook payload
For both a received (invoice.received) and a sent (invoice.sent) invoice we send a POST with this body; the event field distinguishes the type.
Two further events announce the network's verdict on an invoice you have sent: invoice.delivered (the recipient's Access Point confirmed delivery with a delivery receipt (MLS)) and invoice.rejected (rejected, either by the network or during validation before sending). They carry the identification of the document and the company (documentId, invoiceNumber, receiverId, peppolMessageId, companyDic, peppolParticipantId) plus status, statusDetail with the rejection reason, and statusDateTime. If the first delivery attempt failed and the Access Point retries it on its own (usually within an hour), the document stays submitted and statusDetail starts with network_retrying: ; a delivery then yields delivered, giving up yields rejected with a network_gave_up: prefix. Thanks to them you do not need to poll for the status of sent invoices.
The company.activated event arrives when a customer completes provider selection on the portal (VPDS) of the Financial Administration of the Slovak Republic (Finančná správa, FS); body: event, companyId, companyDic, peppolParticipantId, status, verifiedAt. company.deactivated in turn arrives when a company is deregistered from the network (same body without verifiedAt). company.smp_registered_elsewhere arrives when a company completed the FS selection but its national SMP record is held by another provider (body as company.activated plus action: "migration_code_required" and migrateUrl). The partner procedure is in the intermediary documentation. Until the customer enters a migration code the network delivers to the old provider. Details in the guide /saas:
{
"event": "invoice.received",
"eventId": "7f1c2d9e-4b1a-4e3d-9c1f-0a2b3c4d5e6f",
"occurredAt": "2026-09-03T15:04:05Z",
"companyId": "08dd…",
"documentId": "…",
"invoiceNumber": "2026001",
"senderId": "0088:7300010000001",
"supplierName": "Dodávateľ s.r.o.",
"receiverId": "0245:2121358349",
"issueDate": "2026-06-03",
"dueDate": "2026-06-17",
"deliveryDate": "2026-06-03",
"currency": "EUR",
"totalAmount": "120.00",
"peppolMessageId": "…",
"companyDic": "SK2121358349",
"peppolParticipantId": "0245:2121358349"
}The companyDic and peppolParticipantId fields identify the company the event relates to (important for partners that use a single webhook URL for all of their clients).
Network verdict on a sent invoice:
{
"event": "invoice.delivered", // or "invoice.rejected"
"occurredAt": "2026-09-03T15:04:05Z",
"companyId": "08dd…",
"companyDic": "SK2121358349",
"peppolParticipantId": "0245:2121358349",
"documentId": "…",
"invoiceNumber": "2026001",
"receiverId": "0245:2120049096",
"peppolMessageId": "…",
"status": "delivered", // or "rejected"
"statusDetail": null, // for rejected: the rejection reason
"statusDateTime": "2026-06-03T10:15:42Z"
}Company activation after provider selection on the FS portal (VPDS):
{
"event": "company.activated", // company.deactivated has the same body without verifiedAt
"occurredAt": "2026-09-03T15:04:05Z",
"companyId": "08dd…",
"companyDic": "SK2121358349",
"peppolParticipantId": "0245:2121358349",
"status": "active",
"verifiedAt": "2026-06-03T10:02:11Z"
}The event name is also carried in the X-Verteco-Event header. Full list of events: invoice.received, invoice.sent, invoice.delivered, invoice.rejected, invoice.undeliverable, invoice.reported, company.activated, company.deactivated, company.smp_registered_elsewhere and the test event webhook.test. We recommend setting an unknown event type aside and logging it; we always announce a new type in advance. Every event also carries occurredAt (event time, ISO-8601 UTC) for ordering, and eventId (the same value as the X-Verteco-Delivery-Id header): unique per event and unchanged on every retry, so it is the right de-duplication key (peppolMessageId repeats across invoice.sent, invoice.delivered and invoice.reported). invoice.* events also carry the times sentAt, deliveredAt, receivedAt and fsReportedAt (ISO-8601 UTC, null until the fact exists). invoice.reported arrives when the tax report to the Financial Administration (TDD) for the document was handed to the delivery network (§ 85o par. 11), possibly days later during a C5 outage. Formal schemas of all events are in the webhooks section of the OpenAPI specification.
Signature verification
If the company has a secret, we send the X-Verteco-Signature header in the form sha256=HMAC-SHA256(secret, raw body) (lowercase hex). Always compute the HMAC over the exact bytes of the body:
import crypto from 'node:crypto';
// rawBody = the exact bytes of the request body (not re-serialized JSON)
function verify(rawBody, header, secret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Headers of every delivery: X-Verteco-Event (event name), X-Verteco-Delivery-Id (= eventId in the body, identical on every retry), X-Verteco-Signature (the HMAC above) and, in parallel, the Standard Webhooks headers: webhook-id (= eventId), webhook-timestamp (unix seconds of this attempt) and webhook-signature = v1,base64(HMAC-SHA256(secret, id + "." + timestamp + "." + body)). The key is the UTF-8 bytes of your secret; a standardwebhooks library expects it as whsec_ + base64(secret). Recommended handling: verify the signature, refuse a delivery whose webhook-timestamp is older than 5 minutes (replay protection), process idempotently by eventId, answer 2xx within 15 seconds and defer heavy work to your own queue. A failed delivery is retried 8 times, 30 s to 1 h apart; then it is dead-lettered with an e-mail alert and stays visible in the Realtime log. A production webhook URL must be https://; the test environment also accepts http://.
POST /companies/{id}/webhook/secret; it returns it once (rotation generates a new one). Without a secret we do not send the X-Verteco-Signature header.Bulk setup: one token, multiple companies
A single API token (bound to your account/e-mail) manages all companies it owns: whoever creates a company via POST /companies becomes its owner and can configure its webhook. The webhook is per company (its own URL and secret), so with the same token you can connect any number of companies:
# 1) list of your companies (paginated, see Companies)
curl 'https://peppol.verteco.digital/api/v1/companies?page=0&limit=100' -H 'Authorization: Bearer vpt_8f2a…'
# 2) for EACH company {id}: set the webhook (and/or e-mail)
curl -X PUT https://peppol.verteco.digital/api/v1/companies/{id}/notifications \
-H 'Authorization: Bearer vpt_8f2a…' -H 'Content-Type: application/json' \
-d '{"webhookUrl":"https://vas-system.sk/peppol/webhook","notificationEmail":"faktury@firma.sk"}'
# 3) fetch the signing secret (returned ONLY ONCE) and store it for signature verification
curl -X POST https://peppol.verteco.digital/api/v1/companies/{id}/webhook/secret -H 'Authorization: Bearer vpt_8f2a…'
# → { "secret": "…" }- The token must be
owner/adminof the given company; for a company where it is only member/viewer, it returns403 forbidden(no cross-tenant access). - You can give each company a different URL or the same one for all; in the payload you distinguish them by
companyId(andreceiverId). - With hundreds of companies, respect the per-token rate limit (the current value is returned in the
RateLimit-Limitheader); batch with backoff on429(Retry-Afterheader). - The webhook is actually fired only once the company is active in Peppol (after selecting Verteco as provider with the Financial Administration) and therefore actually receives documents.
Data models
Fields of the objects returned by the API.
Company
| Field | Type | Required | Description |
|---|---|---|---|
id | UUID | – | company identifier |
ico | string | – | company registration number (IČO, 8 digits) |
dic | string|null | – | VAT ID (IČ DPH) |
legalName | string | – | legal name |
registeredAddress | string|null | – | registered office address |
peppolParticipantId | string|null | – | Peppol participant (after registration) |
status | string | – | pending_verification | active |
role | string | – | owner | admin | member | viewer (your role) |
createdAt | Instant | – | creation time |
Document
| Field | Type | Required | Description |
|---|---|---|---|
id | UUID | – | document identifier |
direction | string | – | sent | received |
peppolMessageId | string|null | – | Peppol message ID |
docTypeId | string|null | – | document type (Peppol) |
senderId | string|null | – | sender (scheme:id) |
receiverId | string|null | – | receiver (scheme:id) |
invoiceNumber | string|null | – | invoice number |
issueDate | date|null | – | issue date |
currency | string|null | – | currency (e.g. EUR) |
totalAmount | number|null | – | total amount |
status | string | – | processing status |
createdAt | Instant | – | record time |
User · Token · Webhook
| Field | Type | Required | Description |
|---|---|---|---|
User | object | – | { id: UUID, email: string } |
Token | object | – | { id, name, prefix, lastUsedAt|null, createdAt } |
Webhook | object | – | { url: string|null, hasSecret: boolean } |
Slovak conventions from implementer practice
Beyond Peppol BIS Billing 3.0, Slovak ERP and invoicing software vendors are gradually converging on a shared interpretation of optional fields (the discussion takes place in a Slack channel run by the Financial Administration of the Slovak Republic, Finančná správa, FS). Below are the conventions our portal already respects today: all of them are valid BIS constructs, they pass through our API unchanged, and received documents display them in the human-readable preview and PDF as well.
- Deduction of a taxed advance payment on a line: a negative line with
cac:DocumentReference, wherecbc:IDcarries the number of the tax document for the received payment andcbc:DocumentTypeCodeis130(BIS: invoice line object identifier, max. 1 per line). A received invoice with such a line is shown in our preview with the note „odpočet zálohy – daňový doklad č. …" (advance deduction, tax document no. …). - Tax document for a received payment: according to the Financial Administration FAQ, the code
InvoiceTypeCode 388(Tax invoice) is used. It passes both our API and validation; the deduction of an unpaid (untaxed) advance is expressed viacbc:PrepaidAmount. - Invoice cancellation: a credit note
381(CreditNote) withcac:BillingReferenceto the original invoice, not a negative 380 invoice. Note: the network rejects code 384 for Slovak parties (rule PEPPOL-EN16931-P0112 allows it only between German entities); for an upward correction use debit note 383. - BT-83 PaymentID: Slovak practice is converging on the payer reference format
/VS…/SS…/KS…; a bare variable symbol (variabilný symbol) is also common. Our processing passes the value through unchanged and displays it with the payment details. - Additional item data (batches, serial numbers, expiration dates) via
cac:AdditionalItemPropertywith established names such asBatchNumber,SerialNumber,ExpirationDate.
Document attachments (BT-125)
Attachments (PDF, images) can be attached to an e-invoice as base64 in the cac:AdditionalDocumentReference element (BT-125). Our limit is 25 MB per attachment. Peppol does not define a single network-wide limit; individual providers set their own (FS FAQ 9/DPH/2025/IM, example no. 67), so for very large attachments also check the limit of the other party's provider.
Public tools (validation, recipient check)
Two helper endpoints without authentication: the same validation core and the same SML/SMP lookup our Access Point uses. They are suitable for CI and for pre-send checks; a stricter public rate limit applies to them.
/public/peppol-validatepublicValidation of an e-invoice against EN 16931 + Peppol BIS Billing 3.0 (including Slovak rules), the same as the UI validator at /validator. Request body = the UBL 2.1 XML directly (Invoice / CreditNote, or the whole Peppol SBD), Content-Type application/xml, limit 3 MB.
curl -X POST https://peppol.verteco.digital/api/v1/public/peppol-validate \
-H "Content-Type: application/xml" \
--data-binary @faktura.xml// 200 OK
{
"valid": false,
"errors": [
"BR-CO-09: [BR-CO-09]-The Seller VAT identifier (BT-31) … shall have a prefix in accordance with ISO code…"
],
"warnings": []
}
// 400 = empty body (empty_document) or document over 3 MB (document_too_large)/public/peppol-check?id=0245:2121358349publicRecipient check in the live Peppol network (SML/SMP lookup): is it registered and which document types can it receive? The id parameter accepts a Peppol ID (0245:…), a VAT ID (IČ DPH, SK…) or the bare DIČ (Slovak tax identification number). Authenticated equivalent for ERP pipelines: GET /sapi/discovery.
// 200 OK
{
"registered": true,
"participantId": "0245:2121358349",
"smp": "sml.peppol-smp.sk",
"capabilities": ["Faktúra (BIS Billing)", "Dobropis", "Self-billing", "MLS doručenky"], // Slovak labels: Invoice (BIS Billing), Credit note, Self-billing, MLS delivery receipts
"lastCheckedAt": "2026-08-31T…Z"
}MCP server (AI assistants)
The portal runs its own server for the Model Context Protocol, the open standard through which AI assistants (Claude Code, Claude Desktop, Cursor, VS Code Copilot and others) connect to external systems. The assistant signs in with an ordinary API key of the account and gets six tools; it never sees more than that account and every call appears in the key's API log.
Address: https://peppol.verteco.digital/api/mcp. Transport: Streamable HTTP, JSON-RPC 2.0, stateless (POST /api/mcp, no SSE stream, GET answers 405). Sign-in with the header Authorization: Bearer <API key>. Supported methods: initialize, ping, tools/list, tools/call, resources/list, prompts/list.
Tools
list_companies: the account's companies with Peppol ID, status and role (start of every conversation)list_documents: sent/received invoices of a company with delivery and tax-report state, pagedget_document: one invoice in detail including the MLS receipt and tax report, optionally the UBL XML (up to 1 MB)check_participant: live recipient check in the Peppol network (SML/SMP)validate_document: UBL validation against EN 16931 + BIS 3.0 including the Slovak rulessend_document: send an invoice on behalf of the company, requires confirm = true and the Verifikačný údaj gate applies
Connecting in Claude Code
claude mcp add --transport http verteco-peppol https://peppol.verteco.digital/api/mcp \
--header "Authorization: Bearer vpt_..."Claude Desktop (through the mcp-remote bridge, needs Node.js)
{
"mcpServers": {
"verteco-peppol": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://peppol.verteco.digital/api/mcp", "--header", "Authorization: Bearer vpt_..."]
}
}
}Sending an invoice is legally binding: the tool refuses a call without confirm = true and the server instructions tell the assistant to obtain the user's explicit consent to the exact invoice and recipient. Ready-made configurations for Cursor and VS Code and a button that creates the key are in the portal: API keys → MCP server tab. API keys → MCP server
Status (ping)
Public health-check endpoint, suitable for monitoring.
/pingpublicBackend status.
// 200 OK
{ "service": "peppol-portal-backend", "status": "ok", "timestamp": "2026-06-17T…Z" }A live overview of all components is available on the system status page.
Coming soon
- POST
/companies/{id}/peppol/register· Manual registration in the Peppol SMP via the API. Today this happens automatically upon provider selection on the FS portal (VPDS) of the Financial Administration of the Slovak Republic (Finančná správa, FS).
POST /companies/{id}/documents/send is already available (beta: the response shape may still change); for stable programmatic sending we recommend SAPI-SK POST /sapi/document/send with Idempotency-Key.Want early access to the integration, a sandbox, or have a question? Get in touch directly:
