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). Validated samples for Slovak scenarios (non-VAT payer, foreign currency, reverse charge, credit note, advances, embedded PDF) are at /en/vzory.
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
Which interface is for you
SAPI-SK 1.0 (/sapi)
Accounting or invoicing software, ERP, multi-company platforms: the national interface for sending, receiving, statuses, receipts and FS reports. Recommended for every integration that sends or reads documents.
OAuth2 client credentials: client_id = API key UUID, client_secret = the vpt_ key, 15-minute access token.
Portal API (/api/v1)
Companies, webhooks, notifications, exports and file downloads; partner console (/resellers/me). Complements SAPI, does not replace it.
Authorization: Bearer vpt_… (the account’s API key).
MCP server (/api/mcp)
An AI assistant (Claude Code, Cursor, Claude Desktop) should read documents, check recipients and send after explicit confirmation.
Bearer vpt_… through the MCP client.
E-mail to the company inbox
No code: the XML invoice as an e-mail attachment to the company’s secret address; validated and sent as from the portal.
Sender address on the allow-list + SPF/DKIM.
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(); - 4
Select the provider on the Financial Administration portal
In production, selecting Verteco on the FS portal (vpds.financnasprava.sk) is needed even for send-only use: it unlocks sending (the verification token) and activates the company. Receiving may stay with your accounting program's provider: a company has exactly one receiver in the network but may have several senders.
Quick start with curl (test environment first)
Six calls from an account to the first delivered invoice, all against test.peppol.verteco.digital where a company is active the moment it is created and documents go to the Peppol test network. Expected answers are abbreviated.
export BASE=https://test.peppol.verteco.digital # production: https://peppol.verteco.digital
export KEY=vpt_… # step 1- 1
Account and API key
Register at test.peppol.verteco.digital/register (no confirmation e-mail in the test environment) and create a key on the API keys page. It is shown once and is used in full: the key list shows only its beginning (vpt_xxxxxxxx…) and a key copied from there with the dots is refused by the API with a 400. $KEY below.
- 2
Verify the key
shellcurl -s $BASE/api/v1/auth/me -H "Authorization: Bearer $KEY"json{ "id": "7f1d…", "email": "dev@firma.sk", "emailVerified": true, "persona": "developer" } - 3
Create a company
shellcurl -s -X POST $BASE/api/v1/companies -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"ico":"12345678","dic":"SK2120000001","legalName":"Test s. r. o.","street":"Hlavná 1","city":"Žilina","postalCode":"01001"}'json{ "id": "5c0e…", "legalName": "Test s. r. o.", "dic": "SK2120000001", "status": "active", "peppolParticipantId": "0245:2120000001", "verified": true } - 4
SAPI access token
shellcurl -s -X POST $BASE/sapi/auth/token -H "Content-Type: application/json" -d '{"grant_type":"client_credentials","client_id":"<key UUID from the API keys page>","client_secret":"'$KEY'"}'json{ "access_token": "eyJhbGciOiJIUzI1NiJ9…", "token_type": "Bearer", "expires_in": 900, "refresh_token": "eyJhbGciOiJIUzI1NiJ9…" } - 5
Validate without sending
shellcurl -s -X POST $BASE/sapi/document/validate -H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" -d '{"payload":"<Invoice …>","payloadFormat":"XML"}'json{ "valid": true, "errors": [], "warnings": [] } - 6
Send and follow the status
shellcurl -s -X POST $BASE/sapi/document/send -H "Authorization: Bearer $ACCESS" -H "X-Peppol-Participant-Id: 0245:2120000001" -H "Idempotency-Key: FA-2026-0001" -H "Content-Type: application/json" -d @send.json, then curl -s "$BASE/sapi/document/sent?since=2026-10-01T00:00:00Z" -H "Authorization: Bearer $ACCESS" -H "X-Peppol-Participant-Id: 0245:2120000001"jsonHTTP/2 202 { "providerDocumentId": "6a3f…", "status": "ACCEPTED", "peppolMessageId": "f1c7…", "receivedAt": "2026-10-03T12:00:01Z" } { "documents": [ { "documentId": "6a3f…", "status": "delivered", "sentAt": "2026-10-03T12:00:01Z", "deliveredAt": "2026-10-03T12:00:14Z", "hasMls": true, "fsReport": { "state": "reported" } } ] }
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 - Fault simulation
- the header
X-Sandbox-Simulate: 503(or502,500,429) on any/sapi/document/*call with a valid token returns the matching error; nothing is processed or stored. For500(SAPI-PROC-500) and429(SAPI-SYS-003 withRetry-After) you get the SAPI error envelope;503and502pass through the network edge (DigitalOcean, Cloudflare) asHTTP 504with an HTML body and no envelope, exactly how a real outage looks from outside, so you also test handling of a non-JSON response. Validation errors come from invalid XML, 429 also from exceeding 300 calls per minute. Production ignores the header - 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. Test documents belong there only: in production every domestic document between VAT payers is reported to the Financial Administration and the report cannot be withdrawn. In the production portal test only with the „Poslať skúšobnú faktúru“ button (send a test invoice, 0,00 €); from 1 January 2027 production refuses TEST-/VITAJTE- numbers (test_document_in_production).
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.Versioning and backward compatibility
- The contract only grows: new fields (omitted when null), new optional parameters, new endpoints and events. Existing field names, enum values and status codes do not change.
- A new enum value or webhook event may appear at any time; ignore what you do not know and answer an unknown event with 2xx.
- Timestamps are ISO 8601 UTC with a variable number of fractional digits; parse them, never compare them as text. Amounts arrive as strings with two decimals.
- Should a change ever be non-additive, we announce it in the changelog (e-mail subscription available) at least 90 days ahead and keep the old shape during a transition period.
- API-key security model (since 3 October 2026): a key reads and sends documents and manages companies, notifications and webhooks. Account and key management (creating, scoping or revoking keys, MFA, password, sessions, invitations, company transfer, contract signatures) is done by a signed-in person only; a key gets 403 token_not_allowed there. GET /api/v1/tokens stays open to keys.
- The contract version is info.version in openapi.json and every change has a changelog line tagged API or Webhooks.
Errors
statusReason codes of GET /sapi/document/sent) is published as JSON at /docs/peppol-errors.json (sk, en, de, hu). Use it for a table of common errors in your accounting software: match the rule id anywhere in the detail and show unknown rules together with their id. Since 7 Oct 2026 every JSON response carries Content-Type: application/json;charset=UTF-8.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.Most common error codes
| 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 |
Access Point outage and retries
- If the API answers
502,503or504, or does not answer at all, we did not take the document over: repeat the call later with the sameIdempotency-Key, nothing is sent twice. - A document the API accepted (
202ACCEPTED, also withretrying: true) leaves on its own after recovery: the Access Point retries the delivery and you see the state viaGET /sapi/document/sentor a webhook; if it finally gives up, the document ends as failed and the same key sends it again. - Receiving at network level does not depend on the portal: a delivery that did not get through during a portal outage is retried by the sending Access Point under the Peppol rules and the document appears in the portal after recovery. FS reports that did not leave because of the outage are back-filled by an automatic recovery run (a transport failure is retried, a content rejection is not).
- Status and availability history are on /status; in the test environment you simulate an outage with the
X-Sandbox-Simulateheader (section Test environment).
SAPI-SK codes (field error.code)
| Code | Meaning |
|---|---|
SAPI-AUTH-001 | invalid client_id/client_secret or a blocked account (401) |
SAPI-AUTH-002 | missing or malformed Authorization: Bearer header (401) |
SAPI-AUTH-003 | key not authorised for the participant, sending not unlocked or paused (403) |
SAPI-AUTH-004 | IP address not on the key’s allow-list (403) |
SAPI-VAL-001 | invalid request body (400) |
SAPI-VAL-002 | X-Peppol-Participant-Id missing (400) |
SAPI-PROC-001 | temporary: a send with this key is in flight, retry shortly (502, retryable) |
SAPI-PROC-002 | document rejected before sending (422 with Prefer: handling=strict) |
SAPI-PROC-003 | without Idempotency-Key: a different document was already transmitted under this number (409, details: documentId, status) |
SAPI-RES-001 | document does not exist or belongs to another participant (404) |
SAPI-RES-002 | document content no longer stored (company retention) or never archived (404; the code stays for compatibility) |
SAPI-RES-003 / 004 | attachment or MLS receipt does not exist yet (404) |
SAPI-SYS-001 / 002 | temporary infrastructure or validator error (503/502, retryable) |
SAPI-SYS-003 | rate limit exceeded (429, Retry-After) |
SAPI-PROC-500 | unexpected error, send the correlation_id to support (500) |
Complete error catalogue of the portal API (326)
Every code the backend returns in the error field with the text the portal shows. Generated from the same dictionary the UI uses, so it cannot go stale.
Complete error catalogue of the portal API
account_exists | An account already exists for this e-mail. Sign in and accept the invitation in the portal. |
account_inactive | Your account is not active. Please contact us. |
account_under_review | Your account is being reviewed. Call us or write to [email protected]. |
acknowledge_required | Confirm that you understand the risks of downloads without login. |
acknowledgement_required | Confirm that you understand the consequences of terminating the contract. |
address_book_full | The address book is full. Remove unused clients and try again. |
admin_email_unverified | Administration access requires a verified e-mail address. Click the link in the message we sent you at sign-up. |
admin_mfa_required | Administration access requires two-factor authentication. Turn it on in Settings. |
ai_budget | The AI draft budget is used up or you reached the daily limit. Write the reply manually. |
ai_failed | The reply draft could not be generated. Please try again. |
ai_unavailable | AI reply drafts are not available right now. |
already_completed | The company transfer has already been completed. |
already_member | This user already has access (member of the company or your team). |
attachment_external | This attachment is stored at the sender as a link, not inside the invoice itself. |
attachment_invalid | An attachment could not be sent (type not allowed, corrupted content or size exceeded). Check the attachments and try again. |
attachment_not_found | The attachment was not found. |
attachment_not_readable | The attachment could not be read. |
billing_unconfigured | Subscription payments are not available yet. |
blocked | The test webhook cannot be sent to this address (local or internal addresses are not allowed). |
body_required | Please write the text of your message. |
body_too_long | The message can have at most 4000 characters. |
cannot_change_self | You cannot change your own role. Ask the other company administrator. |
cannot_invite_self | You cannot invite yourself or accept your own invitation. |
cannot_release_to_self | The company is already under this account. |
cannot_remove_owner | The company owner cannot be removed. Ownership changes through a company transfer. |
cannot_restrict_admin | A portal administrator account cannot be restricted this way. |
cannot_transfer_self | The company already belongs to this e-mail. |
captcha_failed | The bot check failed. Please try again. |
captcha_required | The bot check failed; reload the page and try again. |
channel_unknown | An old record without a known channel cannot be replayed. Re-POST it manually to the right endpoint. |
chat_not_found | The conversation was not found or is empty. |
client_not_found | The company does not belong to this partner account. |
client_not_removable | The company cannot be removed: it is verified by the Financial Administration, active, registered in the SMP or has documents. Use release instead. |
company_active | An active company cannot be deleted. |
company_address_required | Before sending an invoice, add the company address (street, city, postal code) in the company details. |
company_already_active | The company is already active; activation instructions are not needed. |
company_dic_missing | Add the company DIČ (tax ID) first, then try the verification again. |
company_incomplete | Before signing, complete the company details: legal name, IČO, Peppol identifier and full address. |
company_limit | You reached the maximum number of companies per account. Contact support and we will raise the limit. |
company_name_mismatch | The legal name does not match the official register (ŽR/OR SR) for this IČO. |
company_not_active | A migration code can only be issued for an active company registered in the Peppol network. |
company_not_found | The company was not found. |
company_not_registered | The company is not activated yet: complete the Verteco selection on the Financial Administration portal, then you can send. |
company_not_verified | Migration is possible only after selecting Verteco on the Financial Administration portal (the company must be active and verified). |
company_verified_locked | The legal name of a verified company can only be changed to the official register entry. Add the IČO first or write to us. |
confirm_name_mismatch | The entered name does not match the registered reseller name. |
confirmation_required | Signing requires confirming your authority and your agreement with the wording. |
conflict | This record already exists. |
content_unavailable | The content of this document is no longer stored or cannot be read as an invoice. |
contract_pending | The contractual relationship arises on the first entry into the portal (Art. 4 of the Terms); none has been recorded for this company yet, so the contract cannot be issued. |
country_invalid | Country must be a two-letter ISO code (SK, CZ, AT, DE…), not a name. |
country_outside_eu | The online application is open to entities established in an EU member state. Entities outside the EU: write to [email protected]. |
current_ip_missing | The list must include your current IP address, otherwise you would lock yourself out after signing out. |
daily_limit | You reached the daily message limit. Try again tomorrow or write to [email protected]. |
delivery_failed | The test webhook could not be delivered; your endpoint did not respond. |
delivery_not_found | The webhook delivery record was not found. |
deregister_cooldown | Only one company per IP address can be removed every 60 seconds. Try again in a moment. |
dic_immutable | The Peppol identifier cannot be changed: it is the identity of the company in the Peppol network. |
dic_required | Enter the company DIČ (tax ID: SK + 10 digits). |
dic_taken | A company with this DIČ already exists in the portal. |
directory_unavailable | The central SMP is not responding or refused the change. Please try again in a moment. |
document_no_xml | The stored original XML is gone; the report cannot be rebuilt. |
document_not_found | The document was not found. |
document_too_large | The document is too large (limit 3 MB). |
domestic_subject | A Slovak entity submits the application after signing in at /sprostredkovatel/ziadost. |
duplicate_invoice_number | A document with this number was already sent. A new document needs a new number; the same document is never sent twice. |
email_change_cooldown | We already sent the confirmation e-mail. You can request another one in 10 minutes. |
email_invalid | Enter a valid e-mail address. |
email_not_verified | Confirm your e-mail first. We sent you the link. |
email_send_company_not_active | Sending by e-mail can be switched on only for an active company with a Peppol identifier. |
email_send_unavailable | Sending by e-mail is not available in this environment. |
email_taken | This e-mail address is already used by another account. |
email_unchanged | This is your current e-mail address. |
empty_document | Paste the invoice XML. |
empty_idea | Please tell us what we should improve. |
empty_message | The message is empty. |
empty_reply | The reply is empty. |
entry_not_found | The address book client was not found. |
export_format_unknown | Unknown export format. |
forbidden | You are not allowed to do this. |
forwarder_unreachable | The document processing service is not responding. Try again in a moment. |
fs_change_already_sent | The request has already gone to the Financial Administration and cannot be cancelled. File a new one if needed. |
fs_change_confirm_required | Confirm that you request all the changes at once. |
fs_change_cooldown | The next change request can be filed 48 hours after the previous one. |
fs_change_field_readonly | The company ID and tax ID cannot change; a new identity means a new registration. |
fs_change_no_changes | You have not entered any change against the data at the Financial Administration. |
fs_change_not_found | The change request was not found. |
fs_change_not_open | The request is already closed. |
fs_change_not_queued | Only a request waiting to be sent can be sent now. |
fs_change_pending | One change request is already waiting or with the Financial Administration. File the next one once it is closed. |
fs_secret_invalid | The key has 32 characters (digits and the letters a to f). Copy the whole content of the .txt file from the zip sent by the Financial Administration. |
fs_secret_mismatch | The key does not match the signature of calls the Financial Administration has already sent to your endpoint. Check the zip and the copied key; if your proxy alters the call body, write to us. |
has_documents | The company has documents in the archive; deletion is blocked (statutory archiving). |
holding_unavailable | Releasing the client is not possible right now. Contact support. |
ico_immutable | The IČO cannot be changed. |
ico_required | The IČO is required (8 digits). |
ico_taken | A company with this IČO already exists under another account. Sign in with it or write to us. |
instructions_cooldown | We already sent the instructions for this company. They can be sent again after 24 hours. |
integration_not_found | Unknown integration. |
internal | An unexpected error occurred. Please try again. |
internal_error | An unexpected server error occurred. Please try again; the error has been logged. |
invalid_action | Unknown action. |
invalid_after | The after parameter must be an ISO 8601 date and time (e.g. 2026-09-05T10:00:00Z). |
invalid_amount | The amount must be a number. |
invalid_billing_model | Unknown billing model. Choose one of the offered models. |
invalid_country | Select the country of the registered office. |
invalid_credentials | Incorrect e-mail or password. |
invalid_current_password | The current password is not correct. |
invalid_date | The date must be in the format YYYY-MM-DD. |
invalid_default_company | The default company must be one of your companies. |
invalid_dic | Enter a valid DIČ (10 digits, optionally with the SK prefix) or VAT ID. |
invalid_direction | The document direction must be sent or received. |
invalid_due_days | The due period must be 0 to 365 days. |
invalid_email | Enter a valid e-mail address. |
invalid_feed_url | The feed URL must start with https://. |
invalid_ico | Enter a valid IČO (8 digits) or company registration number. |
invalid_id | Enter a Slovak VAT ID or DIČ (e.g. SK2121358349) or a Peppol ID (e.g. 0245:2121358349). |
invalid_ids | The list of documents is not valid. |
invalid_invite | The invitation is invalid or has already been used. |
invalid_ip_entry | The list contains an invalid entry. Allowed are IP addresses (e.g. 185.76.8.211) and CIDR ranges (e.g. 185.76.8.0/24). |
invalid_key | The reseller key must have 3 to 40 characters: lowercase letters, digits and hyphens. |
invalid_limit | The limit must be at least 1, or empty (default value). |
invalid_link | The sign-in link is invalid or has expired. Request a new one. |
invalid_locale | Unsupported language. |
invalid_month | The month must be in the format YYYY-MM. |
invalid_notification_email | One of the addresses is not a valid e-mail: |
invalid_notify_event | Unknown notification event. |
invalid_notify_locale | The e-mail language must be Slovak, English, German or Hungarian. |
invalid_notify_mode | Unknown notification mode. Choose: every event, daily digest, weekly digest or none. |
invalid_password | Incorrect password. |
invalid_pattern | The numbering format must have at most 64 characters and contain the sequence {N}, {NNN} or {NNNN} exactly once. |
invalid_payload | The invoice (XML) is missing or too large. |
invalid_persona | Unknown account role. |
invalid_phone | Enter the phone number as +421 9xx xxx xxx (digits, spaces, optionally hyphens). |
invalid_prefs | The display settings contain an invalid value and could not be saved. |
invalid_receiver | The recipient must have a Peppol identifier in the form scheme:value (e.g. 0245:2121358349). |
invalid_request | Required data is missing (e.g. company name, 10-digit DIČ, valid e-mail or endpoint URL). |
invalid_role | The role must be administrator, member or read-only. |
invalid_seq | The next sequence number must be at least 1. |
invalid_session | Invalid conversation. Reload the page and try again. |
invalid_signature | The payment notification could not be verified. |
invalid_signup | The sign-up is no longer valid or has expired. Start the registration again. |
invalid_sort | Unknown sort order. |
invalid_state | Unknown state. |
invalid_status | Unknown status. |
invalid_tag | The tag cannot be empty. |
invalid_token | The link is invalid or has already been used. |
invalid_vpds_url | The selection link must start with https://vpds.financnasprava.sk/. |
invalid_web | The website must be a valid address (https://...). |
invalid_webhook_url | The webhook URL must be a valid https:// address (at most 512 characters). |
invalid_window | The time window is not valid: from must be before to and the window at most 31 days. |
invalid_xml | The document XML cannot be read (corrupted file or a DOCTYPE declaration, which is not allowed). |
invite_email_mismatch | This invitation is for a different e-mail address. Sign in with the account it was sent to. |
invite_expired | The invitation has expired. Ask for a new one. |
ip_not_allowed | Signing in from this IP address is not allowed for this account. |
lead_no_email | The lead has no e-mail address and cannot be contacted. |
lead_not_contactable | Only a lead in the state new can be contacted. |
lead_not_found | The lead was not found. |
link_gone | This link is no longer valid (expired, revoked, or the document content is no longer stored). |
lookup_unavailable | The register or verification service is currently unavailable. Try again in a moment. |
malformed_request | The request is not in a valid format. Reload the page and try again. |
manager_required | Only the company owner or an administrator can make this change. |
member_is_owner | This user is the company owner. Ownership changes through a company transfer. |
member_not_found | The member was not found. |
members_full | The company reached the maximum number of members. |
method_not_allowed | This operation is not supported for this address. |
mfa_already_enabled | Two-factor authentication is already turned on. |
mfa_code_invalid | The code does not match. Try the current code from your app or a backup code. |
mfa_not_enabled | Backup codes belong to two-factor authentication. Turn it on first. |
mfa_not_started | Start the two-factor authentication setup first. |
mfa_required | Enter the code from your authenticator app. |
migration_code_live | The previous migration code is still valid. Cancel it first. |
migration_code_none | There is no active migration code request. |
migration_code_pending | A migration code request is already in progress. |
migration_code_rejected | The SMP rejected the migration code. Check it with the previous provider; codes are single-use and time-limited. |
migration_code_required | Enter the migration code from the previous provider. |
migration_rejected | The central SMP rejected the takeover request. Try again or write to us. |
missing_fields | The name and the owner e-mail are required. |
missing_parameter | A required parameter is missing from the request. |
mls_not_available | No delivery receipt (MLS) is available for this document yet. |
name_required | Enter the name (client name or legal name). |
no_billing_account | You do not have a billing account yet. Set up a subscription first. |
no_body | The record has no stored request body and cannot be replayed. |
no_content | The document content is no longer available; the link cannot be issued. |
no_dic | The company has no DIČ (tax ID). |
no_documents | There are no documents (with stored XML) for the selected period. |
no_evidence | Neither the original XML nor a delivery receipt is stored for this document. |
no_ids | Select at least one document. |
no_owner | The company has no owner. Use direct reassignment instead. |
no_secret | The signing key does not exist yet. Set the notification webhook URL first. |
no_verification_token | The company has no stored verification token from the Financial Administration; SMP registration requires it. |
not_a_reseller | This account is not the owner of a reseller registration. |
not_acceptable | The server answers in JSON format only. |
not_available_for_white_label | Not available for companies under a reseller brand. |
not_failed | Only failed webhooks can be retried. |
not_found | Record not found. |
not_reseller | This account is not the owner of a reseller registration. |
not_sandbox_row | This reseller registration is managed by an administrator and cannot be switched off from the account. |
note_too_long | The note can have at most 200 characters. |
notification_emails_too_long | The addresses are too long together (256 characters at most). |
owner_not_found | No owner account exists for this e-mail. Create it first. |
owner_of_companies | To delete the account, first request a migration code or deregister the company from the Peppol network. After that you can delete the account permanently. |
owner_required | Only the owner of the company account can do this. |
participant_not_registered | The company has no record in the central SMP under Verteco yet; set the listing after registration. |
partner_managed | This company is managed by its intermediary; the contract confirmation is issued by them. |
party_missing_in_text | The text being signed does not contain the company identification. Reload the page and try again. |
password_already_set | You already have a password. Use the change password option. |
password_not_set | The account has no password. Set one in Settings and try again. |
password_required | Set a password first (Security); then you can confirm this change. |
payment_mismatch | The payment could not be matched to the application. Please contact us. |
payment_unconfigured | Payment is not available right now. Try later or contact us. |
pdf_unavailable | PDF generation is temporarily unavailable. Try again in a moment or use Print / PDF. |
profile_too_long | One of the profile fields is too long. Shorten the text. |
public_link_exists | The document already has an active link. Choose to replace it if you want a new one. |
public_links_disabled | The company has not enabled invoice downloads without login. The owner can turn it on in the company details (Data archive). |
public_links_unavailable | Public document links are currently switched off. |
rate_limited | Too many requests. Try again in a moment. |
read_only_member | The read-only role cannot change document states or tags. |
received_only | A public link is issued only for received documents. |
receiving_toggle_cooldown | Receiving can be switched at most three times per hour. The next change is possible once an hour has passed since the oldest of the last three changes. |
registered_elsewhere | The participant is still registered with another provider; the code did not trigger the takeover. |
renderer_unavailable | The preview cannot be rendered right now (the rendering service is restarting). Try again in a moment; sending the invoice is not blocked. |
reply_already_cancelled | This reply was cancelled by a colleague in the meantime. |
reply_already_sent | This reply has already been sent; it cannot be cancelled or sent again. |
reply_not_found | The scheduled reply was not found. |
reply_to_invalid | The results address is not a valid e-mail address. |
representative_incomplete | Enter both the name and the position of the authorised person, or leave both fields empty. |
request_rejected | The request contains a character that is not allowed in a header or the URL. If you sent an API key, use the full client_secret shown when the key was created (the key list shows only its beginning with …) and plain quotes. |
reseller_not_found | The reseller was not found. |
reseller_owner | Your account owns a reseller registration. Arrange its hand-over with support; then you can delete the account. |
reseller_required | This part is only for the account of a registered intermediary. |
sbd_not_available | This document has no stored transport envelope (SBDH). Download the document XML instead. |
schedule_not_found | This scheduled export no longer exists. |
sender_email_invalid | The sender address is not a valid e-mail address. |
sender_email_system | Automated mailboxes (noreply, postmaster, mailer-daemon…) cannot be added; use a person's or the invoicing system's mailbox. |
sender_exists | This address is already on the allow-list. |
sender_limit | At most 20 allowed senders are possible. |
sending_not_verified | Sending is not allowed: the company does not have a verified provider selection on the Financial Administration portal yet. |
sending_paused | Sending is paused for this company by its managing account. Receiving invoices continues. |
service_unavailable | The service is temporarily unavailable. Please try again in a moment. |
session_full | This conversation is too long. Reload the page and start a new one. |
session_not_found | The device was not found. |
shoptet_feed_blocked | The address points to a private network. Paste the public address of your shop. |
shoptet_feed_http | Shoptet refused the address. Check the hash in the address and that the export is not limited to one IP address. |
shoptet_feed_invalid_xml | The export response is not valid XML. Check the address in Shoptet. |
shoptet_feed_not_configured | The Shoptet connection is not set up yet. |
shoptet_feed_not_orders_xml | This is not a Shoptet order export: the address must end with /export/orders.xml. |
shoptet_feed_not_system_xml | Paste the address of the “System Shoptet XML” export (its address contains patternId=-11). Custom templates, CSV and XLS are not supported. |
shoptet_feed_too_large | The export response is too large (over 10 MB). We ask for orders week by week, so such a response is usually a product export, not an order export. Check the export type. |
shoptet_feed_unreachable | The Shoptet export did not respond. Try again in a moment; Shoptet allows only one export at a time. |
sidlo_incomplete | The address must contain street, postal code and city, e.g. "Ulica 1, 010 01 Mesto". |
smp_cancel_failed | Cancelling the code at the central SMP failed. Try again in a moment or write to [email protected]. |
smp_deregister_failed | Deregistration from the Peppol network failed. Try again in a moment. |
smp_held_elsewhere | This company's registration is held by another provider. No migration code from us is needed. |
smp_not_test | The test environment is not connected to the test SMP correctly. Please write to us. |
smp_unavailable | The central SMP is not responding right now; try again in a moment. |
smtp_fromAddress_required | Enter the sender address. |
smtp_fromAddress_too_long | The sender address is too long. |
smtp_from_forbidden | The sender address cannot be on a Verteco domain. |
smtp_from_invalid | The sender address is not a valid e-mail. |
smtp_from_not_yours | The sender address must be on your company's domain (like the owner, member or company notification e-mail). |
smtp_from_taken | This sender address is already used by another company. |
smtp_host_blocked | The server cannot be found in DNS or is not publicly reachable (internal addresses are not allowed). |
smtp_host_invalid | Enter the server's domain name (e.g. smtp.company.com), not an IP address. |
smtp_host_required | Enter the SMTP server address. |
smtp_host_too_long | The SMTP server address is too long. |
smtp_incomplete | Fill in the server, port, username, password and sender address. |
smtp_password_invalid | The password must not contain spaces or line breaks. |
smtp_password_required | Enter the SMTP account password (it must be re-entered when the server changes). |
smtp_port_not_allowed | Allowed ports are 465, 587 and 2525. |
smtp_security_invalid | Security must be STARTTLS or SSL. |
smtp_test_cooldown | A test e-mail can be sent once per minute. Try again in a moment. |
smtp_test_failed | The test e-mail could not be sent. Check the SMTP server settings. |
smtp_username_invalid | The username must not contain spaces or line breaks. |
smtp_username_required | Enter the SMTP account username. |
smtp_username_too_long | The username is too long. |
state_not_for_direction | This state is not possible for the document direction (received or sent). |
stripe_error | The payment gateway could not be opened or the payment could not be verified. Please try again. |
support_not_on_sandbox | Support runs on the production portal peppol.verteco.digital, not in the test environment. |
tag_too_long | The tag is too long. |
target_blocked | The target account is blocked; the transfer is not possible. |
team_full | You reached the maximum number of team members. |
termination_confirmed | A confirmed termination notice cannot be cancelled from the app. Write to [email protected]. |
termination_pending | A termination request is already in progress. Confirm it via the e-mail link or cancel it. |
terms_not_accepted | You need to accept the terms and conditions to create an account. |
test_document_in_production | Test documents (number TEST-… or VITAJTE-…) are not sent through the live network from 1 January 2027. Use the test environment test.peppol.verteco.digital for trials. |
ticket_link_expired | The request has been closed for more than 90 days; the link is no longer valid. Write to us again at [email protected]. |
ticket_not_found | The request was not found or the link is not valid. |
token_companies_required | Select at least one company the key may access. |
token_company_not_allowed | This API key has no access to this company (it was created for selected companies only). |
token_company_not_member | A key can only cover companies your account is a member of. |
token_expired | The link has expired. Request a new one. |
token_invalid | The link or verification token is invalid or no longer valid. |
token_limit | You have too many active API keys (max 50). Revoke unused ones and try again. |
token_not_allowed | This setting can only be changed after signing in to the portal, not with an API key. |
token_not_found | The API key was not found. |
token_not_revoked | This API key is active; there is nothing to restore. |
token_restore_expired | The key was revoked more than 7 days ago and can no longer be restored. Create a new key. |
token_scope_invalid | The key scope must be "all" or "selected". |
too_many_ids | At most 500 documents can be processed at once. |
too_many_notification_emails | Enter at most 5 e-mail addresses. |
too_many_schedules | A company can have at most five scheduled exports. Delete one before adding another. |
too_many_tags | The document has too many tags. |
too_many_tickets | You have too many open requests. Please wait for a reply. |
transfer_canceled | The company transfer was cancelled. |
transfer_expired | The transfer link has expired. Request a new transfer. |
transfer_stale | The company ownership changed in the meantime; the transfer was cancelled. |
unauthorized | Please sign in again. |
unknown_document | Only the DPA, SLA and NDA documents can be signed. |
unsupported_document | The preview can render only an invoice or a credit note (UBL Invoice or CreditNote). |
unsupported_media_type | This content type is not accepted. Send the data as JSON. |
user_not_found | No account with this e-mail was found. The user needs to register first. |
validation_failed | Some fields are not filled in correctly. |
validator_unavailable | Validation is currently unavailable. Try again in a moment. |
verification_code_rejected | The SMP rejected the verification token from the FS selection; repeat the selection. |
verification_token_missing | The verification token from the FS selection is missing; repeat the selection and try again. |
verified_company_required | Only an account with at least one verified company can submit the application. |
version_stale | The document wording changed in the meantime. Reload the page and sign the current version. |
webhook_not_configured | Set and save the webhook URL first. |
webhook_url_https_required | In production the webhook URL must start with https:// (a delivery carries customer identifiers and a signature). |
webhook_url_unreachable | The webhook URL must be publicly reachable. Localhost and internal networks cannot be reached from our servers. |
white_label_owner_fixed | A company run through a partner is managed by the partner account. Another owner is added by the partner releasing the company, or by selecting Verteco on the Financial Administration portal. |
wrong_endpoint | This is not the token endpoint. Use POST /sapi/auth/token with client_id and client_secret. |
xml_not_available | The original XML is not stored for this document; its content is no longer available. |
xml_required | The document XML is missing, the preview cannot be rendered. |
xml_too_large | The document is too large for a preview (at most 8 MB including attachments). |
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." }- Discounts and rounding: rules R040 (allowance amount = base × % / 100) and R120 (line amount = quantity × price − line allowances) both tolerate 0.02 €; only R046 for a price-level discount (PriceAmount = BaseAmount − Amount) is exact. The unit price (BT-146) and the gross price and price discount (BT-147, BT-148) have no decimal limit; every other amount has 2. If your system applies the discount to the unit price and rounds it, write it at price level: PriceAmount = the rounded net unit price, BaseAmount = the list price, Amount = the exact difference, line amount = round(quantity × PriceAmount, 2); a percentage cannot be stated there (it belongs in the line note). The second option is an amount-only line allowance with reason code 95; state percentage and base in a line allowance only when base = round(quantity × exported PriceAmount, 2) and Amount = round(base × % / 100, 2). A negative quantity with a positive price (returned goods, a taxed advance deducted) is allowed in credit notes and invoices alike. Samples 11, 15 and 16 on /en/vzory.
- 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.
- Capacity: the volume is not limited by the price list, sending is asynchronous (the call hands the document over and delivery runs in the background) and a batch call accepts up to 100 documents at once; for a planned peak write us the expected volume in advance and we raise the key's limit.
- 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 (§ 76a(2)(d)): sentAt = hand-over to the delivery service (which fulfils the issuer's duty under § 85o(11)), deliveredAt = the receiver's Access Point confirmed by MLS, fsReportedAt = reported to FS; for received documents creationDateTime = receipt. All three are in every row of /sapi/document/sent, in /sapi/document/sent/{id} and in the webhooks (plus occurredAt).
- PDF: GET /sapi/document/receive/{id}/pdf and GET /sapi/document/sent/{id}/pdf (also linked in links.pdf) return the supplier's PDF when the document carries one, otherwise a PDF generated from the XML; printable HTML is at …/html (portal ?format=html).
- C#/.NET and other languages: generate the client from OpenAPI (NSwag, Kiota, openapi-generator); we do not ship our own NuGet package.
- Key scope: when creating an API key (API keys page) you choose whether it covers all companies of the account (including ones added later) or only selected ones. A call with the key for another company returns 403 token_company_not_allowed (SAPI-SK: SAPI-AUTH-003); rotating the key keeps its scope. A read-only key does not exist yet.
- Base path: every SAPI-SK call, including our extensions (GET /sapi/document/sent, GET /sapi/document/receive/{id}/pdf, GET /sapi/discovery), goes under
/sapiwith the same access token andX-Peppol-Participant-Idheader. A 401 “Authentication required” without a SAPI-… code means the path is outside/sapi(for example /api/v1/document/sent);/api/v1is the portal API and expects thevpt_key as Bearer, not a SAPI access token. - We do not check the issue date (BT-2) against the day of hand-over: the document enters the network the moment we receive it, so send it on its issue date and do not queue it past midnight. A document in a foreign currency is reported to the Financial Administration with the tax amount in EUR from BT-110 and BT-111 (TaxCurrencyCode EUR), as sample 03 shows.
- Delivery date: the XML carries it as BT-72 (
cac:Delivery/cbc:ActualDeliveryDate); a tax document for a received payment carries the payment date in BT-72 (sample 12). Our own service invoices and documents from the portal always state it, even when it equals the issue date (§ 74(1)(d) of the VAT Act requires it only when they differ). When reading, take BT-72, else BT-7 (cbc:TaxPointDate), else the issue date.
Rate-limit budgets (current values)
The window is 60 seconds. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy (limit;w=window seconds), a 429 also Retry-After. Values may be raised without notice; read the headers, not this table.
| Calls | Budget key | Limit |
|---|---|---|
/sapi/* and /api/mcp with a token | the client_id behind the token (access-token rotation does not reset it) | 300 / min |
/api/v1/* with an API key or session | key (hashed) or session | 300 / min |
POST /sapi/auth/token, renew, revoke | IP | 60 / min |
/api/v1/auth/* (login, reset) | IP | 20 / min |
Public tools (/api/v1/public/peppol-check, validator) | IP | 15 / min |
Bulk check (/public/peppol-check/batch) | IP | 6 / min |
Live network lookups (peppol-check, /sapi/discovery, MCP check_participant) | IP or key; plus the whole portal | 40 / h per caller and 150 / h in total; past the budget the nightly sweep answers (source: index), the bulk check is not limited |
Registration, forgotten password, contact form | IP | 15 / hour |
Protective ceiling for anything with an unverified bearer | IP | 600 / min |
HTTP/2 200
RateLimit-Limit: 300
RateLimit-Remaining: 287
RateLimit-Reset: 41
RateLimit-Policy: 300;w=60API 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). The response may carry an additive warnings[] array with the same non-blocking hints as /document/validate (a TEST- number, a recipient from the same account, an issue date that is not the sending day); a warning never blocks the document.
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 | one key per invoice, derived from something stable (e.g. your system + the invoice id or number); a repeated call with it returns the original result and never delivers twice, while a new key for the same invoice bypasses this protection. The key is scoped to the company (X-Peppol-Participant-Id). Exception: if the first attempt never entered the network (validation error, incomplete request, recipient does not publish the document type, the access point gave up retrying, transport error with status failed) or ended as undeliverable, the same key performs the send again, so "fix and resend" works under the same invoice number; see the note Correcting and resending below. The key never expires: it is stored on the document record, which we keep permanently (retention wipes only the content), so a repeat with the same key returns the original result even months later. |
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. documentTypeId: the full busdox-docid-qns identifier; a bare namespace or a CustomizationID alone is normalised to the type derived from the payload, a type that contradicts the payload is refused with SAPI-VAL-001; the participant scheme must come from the EAS list.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 lookup it would. The document is validated first: only a valid document is handed over and reported, an invalid one gets REJECTED with validation_failed even when the recipient is not in the network. 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, part II, 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 identifier is corrected or the recipient registers (see the note Correcting and resending below). 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.handling=strict) gets its own providerDocumentId, a rejected one too.- REJECTED by our pre-send check (validation error, incomplete request, recipient does not publish the document type, its SMP record has no endpoint for the type, the access point gave up retrying): nothing entered the network, nothing was delivered and nothing is reported to the Financial Administration. Correct the data and send the document again with the same
Idempotency-Key. The new attempt gets a newproviderDocumentId; the first record stays in the sent list asrejectedwith its reason, only as the audit trail of the failed attempt, and it cannot be deleted. Always bind the invoice in your system to the latestproviderDocumentId. The invoice number may stay the same, the invoice has not been issued in the sense of the law yet. Its issue date (BT-2) must however be the day it actually goes out: an e-invoice counts as issued on the day it is handed over to the delivery service, and it must be sent no later than 15 days after the supply or the payment (FS FAQ 9/DPH/2025/IM, part I, ex. 74). If the corrected invoice goes out on a later day, change the issue date to that day. - Handed over to the network (ACCEPTED without undeliverable: submitted, delivered or retrying in the background; this includes a document the recipient's MLS rejected after transmission, which a repeated call returns as REJECTED with the reason): the same key returns the result of that send and nothing is sent twice. Never send a delivered invoice again; it is corrected by a credit note (381) or a debit note (383).
- undeliverable (the recipient has no address in the Peppol network): the invoice was handed over, it is reported to the Financial Administration regardless of delivery and the issuer's obligation is fulfilled (FS FAQ 9/DPH/2025/IM, part I, ex. 42 and 75). The key is released. If the recipient identifier was wrong (for example an IČO or a DIČ with a typo instead of 0245:<10 digits of the DIČ>, or the recipient is in the network under another identifier), correct the identifier and send again with the same key; keep the invoice unchanged (same number and date), it has already been handed over. An 8-digit number is an IČO and is never found in the network. If the identifier is correct and the recipient simply is not registered, send the same invoice another way, best its XML by e-mail (PDF only as a preview). Under FS FAQ part I, ex. 75 this is re-sending the same invoice, not issuing a new one, so do not issue a new invoice with another number. Once the recipient registers, you can send the same invoice through Peppol again by repeating the call with the same
Idempotency-Key. - Wrong buyer identifier or an empty own one: an
X-Peppol-Participant-Idheader with the scheme only (for example0245:, when the company's DIČ is missing in the software settings) is refused on send with 403 SAPI-AUTH-003 and an explanation, and /document/validate with such a header adds a warning towarningsbefore you send. A buyer given as 0088 (GLN) or as 0245 with 8 digits (the IČO instead of the DIČ) is not found in the Slovak network: the document ends as undeliverable and itsstatusReason(the detail in the send response) carries the fix: 0245 plus the 10 digits of the DIČ. - Prevent errors: before sending call
POST /sapi/document/validate(the same rules as sending, stores and sends nothing) andGET /sapi/discovery?receiverId=…(is the recipient in the network and which document types it accepts). The final state is onGET /sapi/document/sent?invoiceNumber=…,GET /sapi/document/sent/{id},POST /sapi/document/sent/statusor the webhooksinvoice.delivered,invoice.rejectedandinvoice.undeliverable.
/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. The response also carries a warnings[] array with non-blocking hints (for example a TEST- number, a recipient that is another company of the same account, or an issue date BT-2 that is not the sending day); a warning never blocks the document.
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 (filter: received = only documents you have not acknowledged yet, acknowledged = only acknowledged ones, no parameter = all received documents; 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 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)
MLS receipt and delivery evidence
The MLS receipt (Peppol Message Level Status) is the receiving Access Point's answer to our transmission: AP = accepted and delivered, AB = delivered without business confirmation, RE = rejected with a reason. The portal stores it with the sent document and flips the status accordingly: AP and AB both mean delivered (with AB the statusDetail starts with "MLS AB: " and mls.responseCode is AB; under SPOG on MLS that is delivery without a business confirmation, not a reservation), RE means rejected. Under § 76a(2)(d) of Act 222/2004 the provider must be able to prove the date and time of sending, delivery and receipt; every row of GET /sapi/document/sent and every webhook carries those three times: sentAt (hand-over to the delivery service, which under § 85o(11) fulfils the issuer's duty), deliveredAt (the receiver's Access Point confirmed by MLS) and fsReportedAt (tax data reported to FS). A foreign Access Point may never send an MLS: the document then stays delivered with the detail as4_receipt_only and no receipt.
/sapi/document/sent/{documentId}Bearer (access)Detail of one sent document: the same row as in the list (incl. sentAt, deliveredAt, fsReportedAt, hasMls, links) plus mls.responseCode (AP/AB/RE), mls.receivedAt and mls.url. mls is null until a receipt arrives.
// 200 OK
{ "document": {
"documentId": "…", "peppolMessageId": "befc9112-…", "status": "delivered",
"sentAt": "2026-09-17T08:00:12Z", "deliveredAt": "2026-09-17T08:00:31Z", "fsReportedAt": null,
"hasMls": true,
"links": { "detail": "…/sapi/document/sent/…", "mls": "…/sapi/document/sent/…/mls" } },
"mls": { "responseCode": "AP", "receivedAt": "2026-09-17T08:00:31Z", "url": "…/sapi/document/sent/…/mls" } }Errors: SAPI-AUTH-002 (401) · SAPI-AUTH-003 (403) · SAPI-RES-001 (404: foreign or received document) · SAPI-RES-004 (404: no receipt yet)
/sapi/document/sent/{documentId}/mlsBearer (access)The receipt as a file: the MLS ApplicationResponse from the receiver's Access Point referencing the transmission's peppolMessageId. Proof of delivery or rejection; forced download. SAPI-RES-004 while no receipt has arrived.
/sapi/document/sent/{documentId}/xmlBearer (access)Content of a sent document (SAPI 1.8.15): the bare UBL XML we handed to the network (transport envelope stripped), forced download. The counterpart of the receive-side links: an external accountant's program can load the client's ISSUED invoices this way (MK-soft: „Načítavať aj vystavené faktúry“). Every row carries links.xml, links.html and links.pdf. SAPI-RES-002 once the company's retention wiped the content.
/sapi/document/sent/{documentId}/htmlBearer (access)Printable rendering of a sent document, built from the stored XML (nothing is stored).
/sapi/document/sent/{documentId}/pdfBearer (access)PDF of a sent document: the issuer's own PDF embedded in the XML (BT-125) when present, otherwise rendered from the XML; header X-Verteco-Pdf-Source: supplier | generated. 503 SAPI-PROC-001 while the renderer is unavailable (use /html).
FS report evidence, integrity and timeline (SAPI 1.8.14)
Since 1.8.14 every row of GET /sapi/document/sent, the sent detail and the received list/detail carry an fsReport object: state = reported (the report was handed to the C5 collection point, reportedAt = fsReportedAt), pending (the report is owed, the hand-over failed and is retried automatically, reason = c5_unavailable) or not_required (no report arises; reason says why: cross_border, self_send_test, issuer_without_vat_id, recipient_without_vat_id, not_billing_document, tdd_disabled, before_2026_09_01, not_recorded). After the hand-over tddMessageId (the report's SBDH InstanceIdentifier) appears and after C5 answers c5ResponseCode (AP/AB/RE), c5ResponseAt, c5Detail and the summary outcome: accepted (AP), accepted_with_reservations (AB), rejected (RE) or null until C5 answers. outcome = rejected means the report was handed over but the Financial Administration refused it (c5Detail carries the violated rule, e.g. BR-AE-08); state stays reported because the hand-over happened, and the portal re-sends the corrected report automatically (outcome becomes accepted once C5 accepts it). Sent rows also carry statusReason (a machine code behind statusDetail: network_retrying, network_retrying_sent, as4_receipt_only, recipient_not_registered, transport_failed, mls_rejected, validation_failed), xmlSha256 (SHA-256 of the handed-over XML, kept after the content wipe), retention and receiptRetention = permanent (the MLS receipt is never deleted). The receipt in the detail has mls.sha256 and mls.messageId. Received documents gained peppolMessageId, status (received/acknowledged), receivedAt, acknowledgedAt and xmlSha256 (SHA-256 of the business document bytes exactly as links.xml serves them, without the SBDH transport envelope; computed on receipt and kept after the content wipe). Both lists filter with ?fsReport=reported|pending|not_required|rejected (rejected = outcome rejected), sent also with ?status=undeliverable. The POST /sapi/document/send response carries peppolMessageId at acceptance. Everything is additive; existing fields are unchanged.
/sapi/document/sent/statusBearer (access)Status of up to 200 sent documents in one call: the same rows as the list plus mls when the receipt has arrived. Ids that are not this company's sent documents come back in missing; the call never fails because of them. Replaces N calls of GET /sapi/document/sent/{id} in an ERP sync.
// POST { "documentIds": ["7c1e…", "9af0…", "unknown"] } (max 200)
{ "documents": [ {
"documentId": "7c1e…", "status": "delivered", "statusReason": null, "peppolMessageId": "befc9112-…",
"sentAt": "2026-09-21T08:00:12Z", "deliveredAt": "2026-09-21T08:00:31Z", "fsReportedAt": "2026-09-21T08:05:40Z",
"fsReport": { "state": "reported", "reportedAt": "2026-09-21T08:05:40Z", "reason": null, "outcome": "accepted",
"tddMessageId": "3f2a…", "c5ResponseCode": "AP", "c5ResponseAt": "2026-09-21T08:06:02Z", "c5Detail": null },
"xmlSha256": "9b74…", "retention": { "mode": "storage", "contentAvailableUntil": null }, "receiptRetention": "permanent",
"hasMls": true, "links": { "detail": "…", "mls": "…" },
"mls": { "responseCode": "AP", "receivedAt": "2026-09-21T08:00:31Z", "url": "…", "sha256": "e1c0…", "messageId": "mls-…" } } ],
"missing": ["9af0…", "unknown"] }Errors: SAPI-AUTH-002 (401) · SAPI-AUTH-003 (403) · SAPI-VAL-001 (400: 0 / > 200 id)
/sapi/document/sent/{documentId}/eventsBearer (access)The document's timeline ordered by time: submitted, network_retrying, delivered / rejected / undeliverable / failed, mls_received, fs_reported, fs_c5_verdict, content_wiped, webhook_delivered and more (ignore unknown types). Phase 1 is derived from the stored facts, not a journal of every intermediate status change.
Errors: SAPI-AUTH-002 (401) · SAPI-AUTH-003 (403) · SAPI-RES-001 (404: foreign or received document)
/sapi/document/receive/{documentId}/eventsBearer (access)Timeline of a received document: received, acknowledged, fs_reported, fs_c5_verdict, content_wiped, webhook deliveries.
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. On a credentials error (SAPI-AUTH-001) the envelope also carries hint (a human-readable explanation of what belongs into Client Id and Client Secret) and docs (a link to the guide; MK-soft programs are sent straight to /navody/mksoft#prepojenie). Both fields are optional and absent elsewhere (1.8.20).
{ "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 12× with exponential backoff (30 s → max 8 h, about 16 hours in total), then dead-letter. HTTP 401/403 means your endpoint requires an authentication we do not send; verify the webhook-signature header instead and answer 2xx. 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, notificationEmails, 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 | one address or up to 5 separated by commas, max 256; each gets the received-invoice e-mails (400 invalid_notification_email / too_many_notification_emails) |
/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); invoice.undeliverable reports a recipient without a record in the Peppol network (accepted, reported to the FS, not delivered). 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, invoice.report_pending, invoice.report_rejected, 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 12 times, 30 s to 8 h apart (about 16 hours in total); 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 } |
Fields are loaded straight from the published openapi.json, so these tables are always identical to what the API returns.
Loading data models from openapi.json…
Glossary
- DIČ
- Slovak tax identification number (10 digits). The Slovak Peppol ID is 0245: + the DIČ digits, without SK.
- IČ DPH
- VAT identification number (SK + DIČ). Only VAT payers have one; a non-payer issues with tax category O.
- IČO
- Company registration number (8 digits). Field ico in the API, BT-30 in UBL.
- Verification token (VÚ)
- A signature of the Financial Administration created when the taxpayer selects a provider on the FS portal; it unlocks sending for the company.
- VPDS
- The FS application where a taxpayer selects its delivery-service provider (us).
- TDD
- Tax Data Document, the invoice report to the Financial Administration; we build and send it for you.
- C5 / C6
- The fifth and sixth corner of the Peppol model: the Financial Administration’s access point that receives TDDs (0242:001180).
- MLS
- Message Level Status, the receipt from the recipient’s access point (AP = accepted, RE = rejected, AB = acknowledged).
- SBDH
- Standard Business Document Header, the envelope a document travels in; its InstanceIdentifier is the peppolMessageId.
- SML / SMP
- The network directory: SML finds the recipient’s SMP, the SMP says which access point and document types the recipient accepts.
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). For received documents we process attachments up to 25 MB each; when sending, the whole XML including base64 attachments must stay under 10 MB via SAPI-SK and under 2 MB via the portal tab Odoslať súborom. 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 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 the UBL 2.1 schema, EN 16931 and Peppol BIS Billing 3.0, 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 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)validate_document: UBL validation against EN 16931 + Peppol BIS 3.0send_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
Sending by e-mail (company address)
In the portal a company switches on its own address <24 random characters>@peppol.send.verteco.app. An e-mail with a UBL XML attachment sent to that address from an allowed mailbox is sent through the Peppol network exactly as if the file had been uploaded on the Send by file tab; the sender gets one reply with the result per attachment and the document is in the portal and the API with its delivery status, MLS receipt and FS report. Set-up: Connections and integrations → Sending by e-mail (company owner or administrator, signed in). /dashboard/integrations/email
Processing rules
- Only mail from an address on the company's allow-list (up to 20) is accepted; everything else is discarded without a reply.
- By default the sender domain must pass SPF or DKIM at the delivering server (DMARC-style relaxed alignment). The delivering server additionally refuses mail that fails both SPF and DKIM and mail against the sender's DMARC policy. The check can be switched off in the settings; not recommended.
- Attachment: UBL 2.1 Invoice or CreditNote (Peppol BIS Billing 3.0), .xml extension, optionally in an SBDH envelope; at most 10 attachments per message, 15 MB per attachment, 200 messages per address per day. PDF and the message text are not processed.
- IFS Finvoice 2.0: an .xml attachment with the IFS SOAP envelope (
IFSFinvoice) or a bare Finvoice is converted to UBL and then processed the same way. Conversion rules:INV02or a negative total = credit note (381) with positive amounts and the preceding invoice reference (OriginalInvoiceNumber, with IFSOrderIdentifier); seller and buyer by their Slovak VAT id (SellerOrganisationTaxCode,BuyerOrganisationTaxCode→0245:DIČ), a buyer outside Slovakia is not converted; VAT breakdown fromVatSpecificationDetails, 0 % rows as category E with the reason fromVatFreeText, otherwise “Oslobodené od DPH”; IFS tax-rounding rows (net 0,00, VAT 0,01) are dropped and a unit price that does not match the row net is recomputed; units per UN/ECE (kg → KGM, pcs → H87, h → HUR, unknown → C62 with the original text in the line note); the embedded invoice image (InvoiceAttachments, PDF) is attached to the document up to 5 MB; EUR only. The conversion result is stated in the reply and in the company log. - Results address: when a system mailbox sends the invoices, choose another address for the reply with the results in the settings (
replyToinPUT /companies/{id}/email-send, empty = reply to the sender). - The seller in the XML (supplier EndpointID, BT-34) must be this company, otherwise the message is rejected (a forwarded invoice from your own supplier is never sent as yours). The buyer must have an EndpointID with a scheme (BT-49).
- The same message (Message-ID) is processed once. The document carries
origin = emailin the API and webhooks, actor = the sender address.
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.
What is done and what is coming
Already in production (often assumed missing)
- POST /companies/{id}/documents/send is a stable portal interface; for ERP integrations SAPI-SK with Idempotency-Key remains the recommendation.
- SMP registration happens automatically after the FS portal selection; moving from another provider via POST /companies/{id}/smp-migrate with a migration code.
- MLS receipts, delivery evidence and FS reports (fsReport) on every /sapi/document/sent row; webhook replay for a time window.
- Keys limited to selected companies; MCP server for AI assistants; UBL samples on /vzory; public validator.
In preparation (ordered by integrator demand)
- Asynchronous batch: POST /sapi/document/batch answers 202 with a job link and per-item status.
- Change feed GET /sapi/document/changes?since&cursor with a cursor instead of offset paging.
- Key expiry and rotation in the portal, read-only keys.
- Several webhook endpoints per company with an event filter.