API and webhooks
Connect vporiadku to your own system
A REST API for invoices and every other document, payments, clients, the item catalogue, expenses and received e-invoices, and webhooks that tell you when something happens. Keys with precise scopes, OAuth 2.0 for apps that serve many customers, requests that are safe to retry, and an OpenAPI 3.1 reference.
What the API does
Every request goes through the same rules as the app: document numbering, the VAT rate checks, the plan's limits and the audit trail. The API is another door into the product, not a second copy of it.
Invoices and documents
List with filters by type, status, client, dates, tags and change, detail, PDF, ISDOC and UBL, create, copy, update, issue, e-mail, payment reminder, credit note and cancellation.
Payments
Payments of a document: list, record, settle the rest in one call, delete. The document's status follows by itself.
Clients
The client directory: search by name, IČO, DIČ or e-mail, changes since a moment, detail, create with a duplicate check, update and delete.
Catalogue
Items and services of the catalogue: filter, sort, create, update and delete. Part of the Profi plan.
Expenses
Received documents: list filtered by status, category, period and change, book, update and delete. Part of the Profi plan.
Received e-invoices
The e-invoice inbox: list, detail with lines and VAT breakdown, and the archived original UBL. Reading through the API marks nothing as read.
Recurring invoices
Recurring invoice schedules and their latest runs (read-only).
Company profiles
The companies the workspace issues documents under, with the default one marked (read-only).
Bank accounts
The accounts documents are paid into, with the primary one marked (read-only).
Webhooks
Events vporiadku sends to your URL: a document issued, paid, a new expense, a received e-invoice and more. Signed with HMAC-SHA256, managed in the app or through the API.
Partner apps
OAuth 2.0 with PKCE: customers connect your app with one click, and you hold a token per workspace with exactly the scopes they approved.
Quick start
The base URL of the API is https://api.vporiadku.sk/api/v1. Responses are JSON, money is a decimal number in the document currency, dates are YYYY-MM-DD.
Step 1
Turn on the Firma module
The public API is part of the Firma add-on module for the Profi plan. Without it the API answers 403.
Step 2
Create a key
In Settings → API the owner or an admin creates a key and chooses its scopes. The full key is shown once.
Step 3
Send the first request
Ping checks the key and returns its scopes. Keep the key in an environment variable, never in code.
Step 4
Continue in the reference
Every field of every request and response is in the OpenAPI 3.1 reference, which can also generate a client for you.
curl https://api.vporiadku.sk/api/v1/ping \
-H "Authorization: Bearer $VPORIADKU_API_KEY"Authentication
Send an API key in one of two headers, or, from a partner app, an OAuth access token. Keys start with fak_, access tokens with vpa_.
Authorization: Bearer fak_…
X-API-Key: fak_…
Authorization: Bearer vpa_…- The full key is shown only when it is created; we keep only its SHA-256 fingerprint. A lost key is not recovered. Revoke it and create a new one.
- A key or an access token acts as the owner of its workspace and never reaches another one.
- The API is meant for server-to-server calls. A key does not belong in a browser or a mobile app: whoever sees it can use it.
- Revoke a key with one click in Settings → API; the next request with it gets 401.
- An app that serves many vporiadku customers does not collect their keys: it connects over OAuth 2.0, described in the next section.
Partner apps (OAuth 2.0)
Building an integration for many vporiadku customers: an e-shop platform, accounting software, a CRM? Register it once as an app. Each customer connects it with one click on the consent screen, and your server holds an access token per workspace with exactly the scopes that customer approved. The standard authorization code flow with PKCE, the same scopes as API keys.
- 1
Register the app
In Settings → API → Apps: the name and website customers will see, your redirect URIs and the most the app may ask for. You get a client_id and a client_secret; the secret is shown once. A workspace may register up to 10 apps, and no paid plan is needed for it.
- 2
Send the customer to the consent screen
With response_type=code, your client_id, a registered redirect_uri, the scopes you need, a state and a PKCE code_challenge (S256). The customer signs in, chooses the workspace and approves, or cancels, and you get error=access_denied. The workspace needs the Firma module, and only its owner or an admin can approve.
- 3
Trade the code for tokens
The code comes back to your redirect URI with your state. Within 10 minutes, exchange it at the token endpoint with your client secret and the code_verifier. You get an access token, a refresh token, the granted scope and the workspace_id.
- 4
Call the API
Send the access token as Authorization: Bearer on every /api/v1 request. It is valid 60 minutes; ping tells you the workspace and the scopes it carries.
- 5
Refresh before it expires
grant_type=refresh_token returns a new pair and spends the old refresh token. A refresh token lasts 365 days from its issue; one presented again more than 30 seconds after it was spent revokes the whole chain.
- 6
Disconnect
Revoke a token at the revocation endpoint (RFC 7009); revoking the last refresh token disconnects the app from the workspace. The customer can disconnect your app at any time too. Its tokens then answer 401.
Register an app in Settings → API → Apps →
Endpoints
- Consent screen
https://vporiadku.sk/oauth/authorize- Token endpoint
https://api.vporiadku.sk/api/oauth/token- Revocation endpoint
https://api.vporiadku.sk/api/oauth/revoke
- PKCE with S256 is required, and so is redirect_uri, matched exactly against what you registered: https, or http on localhost only.
- Every app is a confidential client: the token endpoint wants your client secret, by HTTP Basic or as client_id and client_secret in the form, never both. Keep the secret on your server.
- The scopes are the API key scopes. A request beyond what the customer approved answers 403 API_KEY_SCOPE_MISSING with WWW-Authenticate: Bearer error="insufficient_scope".
- The rate limit counts per app and workspace: 120 requests per 60 seconds for each workspace your app is connected to, refreshed tokens included.
- Your app can register up to 5 webhook endpoints in each connected workspace through /webhooks; they carry only events your scopes can read and are deleted when the customer disconnects the app.
- Token endpoint errors follow RFC 6749: error and error_description; a client that calls it too often gets 429 temporarily_unavailable with Retry-After. An expired access token answers 401 with WWW-Authenticate: Bearer error="invalid_token". Refresh it and retry.
import { createHash, randomBytes } from 'node:crypto'
const codeVerifier = randomBytes(32).toString('base64url')
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url')
const state = randomBytes(16).toString('base64url')
// Keep codeVerifier and state in the user's session until the callback;
// send codeChallenge and state with the user to the consent screen.https://vporiadku.sk/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=https%3A%2F%2Fyour-app.example%2Foauth%2Fcallback&scope=invoices%3Awrite%20clients%3Aread%20webhooks%3Awrite&state=$STATE&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256curl -X POST https://api.vporiadku.sk/api/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://your-app.example/oauth/callback \
-d code_verifier="$CODE_VERIFIER"curl -X POST https://api.vporiadku.sk/api/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN"curl -X POST https://api.vporiadku.sk/api/v1/webhooks \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.example/hooks/vporiadku", "events": ["invoice.paid", "client.created"]}'Key scopes
A key may do only what its owner allowed when it was created, and a partner app only what the customer approved. An e-shop integration needs to write invoices; a reporting tool needs to write nothing.
| Scope | What it allows |
|---|---|
invoices:read | Read documents: list, detail, PDF, ISDOC and UBL. |
invoices:write | Create, copy, update and delete drafts, issue, e-mail, send reminders, credit and cancel. |
payments:read | Read the payments recorded on documents. |
payments:write | Record payments, settle the rest of a document and delete payments. |
clients:read | Read the client directory. |
clients:write | Create, update and delete clients. |
items:read | Read the catalogue. |
items:write | Create, update and delete catalogue items. |
expenses:read | Read expenses. |
expenses:write | Book, update and delete expenses. |
received_invoices:read | Read received e-invoices and their originals. |
recurring_invoices:read | Read recurring invoice schedules. |
company:read | Read the workspace's company profiles. |
bank_accounts:read | Read the workspace's bank accounts. |
webhooks:read | Read the webhook endpoints the key or the app manages. |
webhooks:write | Register, change, test and delete webhook endpoints for events the other scopes can read. |
- Writing includes reading the same resource: whoever creates an invoice gets it back in the response.
- An operation outside the scopes ends in 403 API_KEY_SCOPE_MISSING, and details.required names the missing scope. An existing key's scopes do not change: create a new key; an app asks the customer to approve it again.
- Keys created before scopes existed have full access (stored as *), so existing integrations keep working.
Limits and paging
Each key (and each partner app in each workspace it is connected to) may send 120 requests per 60 seconds, across all endpoints together. The window is fixed and refills completely when it ends.
HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 60- Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (the seconds until the window refills).
- Past the limit comes 429 RATE_LIMIT_EXCEEDED with a Retry-After header; wait that many seconds. Every key has its own window, and so does every app in each workspace it is connected to.
- After 30 failed authentications from one address within 60 seconds, the API stops checking keys from that address until the window ends.
- Lists take page (from 1) and limit (default 20, at most 100) and answer { data, total, page, limit }.
Idempotency-Key
Send the Idempotency-Key header on POST and PATCH, and repeating a request after a dropped connection will not create a second invoice. A key lasts 24 hours and belongs to the API key (or the partner app in that workspace) that sent it.
- A repeat with the same method, path and body gets the stored response with Idempotent-Replayed: true, and nothing runs again: no second invoice, e-mail or webhook.
- The same key with a different request ends in 409 IDEMPOTENCY_KEY_REUSED.
- A repeat while the first request is still running ends in 409 IDEMPOTENCY_KEY_IN_USE. Try again in a moment.
- Only a successful response is stored: a failed request can be repeated with the same key.
curl -X POST https://api.vporiadku.sk/api/v1/clients \
-H "Authorization: Bearer $VPORIADKU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a9e-4b7d-4e0a-9c55-0f3e8a1b2c4d" \
-d '{"name": "Novák s.r.o.", "ico": "12345678", "email": "faktury@novak.sk"}'Errors
Every error has the same shape. Decide on error.code; the message is English and may change. An unknown field in the request body is a 400, never a silently ignored value.
{
"data": null,
"meta": null,
"error": {
"code": "API_KEY_SCOPE_MISSING",
"message": "This API key lacks the \"invoices:write\" scope",
"details": {
"operation": "createInvoice",
"required": "invoices:write",
"granted": [
"invoices:read"
]
}
}
}| Code | Meaning |
|---|---|
UNAUTHORIZED | 401: the key or the access token is missing, unknown, revoked or expired. |
FORBIDDEN | 403: the workspace does not have the Firma module. |
API_KEY_SCOPE_MISSING | 403: the key or the token lacks a scope; details.required names it. |
FEATURE_NOT_AVAILABLE | 403: the feature is not in the workspace's plan. |
PLAN_LIMIT_REACHED | 403: a limit of the plan is used up; details give the limit and the current count. |
VALIDATION_FAILED | 400: the request body did not pass validation; details.errors lists the problems. |
NOT_FOUND | 404: there is no such thing in this workspace. |
IDEMPOTENCY_KEY_REUSED | 409: the Idempotency-Key already belongs to another request. |
IDEMPOTENCY_KEY_IN_USE | 409: a request with this Idempotency-Key is still running. |
WEBHOOK_EVENT_NOT_ALLOWED | 403: a webhook event needs a scope the key or the token lacks, or stays in the app. |
RATE_LIMIT_EXCEEDED | 429: the limit is used up; wait Retry-After seconds. |
INTERNAL_ERROR | 500: an error on our side; nothing internal is exposed. Before repeating a write, check whether it took effect. |
Endpoints
49 operations under https://api.vporiadku.sk/api/v1. The table is generated from the same catalogue the server runs on.
Every field, parameter and response in the API reference →
Key
| Request | What it does | Scope |
|---|---|---|
GET/ping | Checks the key and returns the plan, the key prefix and its scopes. | none |
Invoices and documents
| Request | What it does | Scope |
|---|---|---|
GET/invoices | Documents of every type with their lines and client: filters by type, status, payment state, client, dates, number, tags, currency, text and updatedSince; sortable. | invoices:read |
GET/invoices/{id} | One document with its lines and client. | invoices:read |
GET/invoices/{id}/pdf | The document's PDF, exactly as the customer receives it. | invoices:read |
GET/invoices/{id}/isdoc | The document as ISDOC 6.0.1 XML for accounting software. | invoices:read |
GET/invoices/{id}/ubl | An issued invoice as Peppol BIS 3.0 UBL XML; needs the e-Faktúra module, which is being prepared. | invoices:read |
POST/invoices | Creates a document; the server always recomputes the totals. | invoices:write |
PATCH/invoices/{id} | Updates a document while it is still editable. | invoices:write |
DELETE/invoices/{id} | Deletes a draft without payments (to the recycle bin). | invoices:write |
POST/invoices/{id}/issue | Issues a draft: fixes its number and locks its content. | invoices:write |
POST/invoices/{id}/duplicate | Copies a document into a new draft dated today. | invoices:write |
POST/invoices/{id}/send | E-mails an issued document to the client with the PDF. | invoices:write |
POST/invoices/{id}/reminder | Sends a payment reminder for an unpaid document. | invoices:write |
POST/invoices/{id}/credit-note | Issues a credit note against an invoice. | invoices:write |
POST/invoices/{id}/cancel | Voids an issued document with a cancellation document. | invoices:write |
Payments
| Request | What it does | Scope |
|---|---|---|
GET/invoices/{id}/payments | The payments recorded on a document. | payments:read |
POST/invoices/{id}/payments | Records a payment; a covered document is marked paid. | payments:write |
POST/invoices/{id}/mark-paid | Settles what is left of a document in one payment. | payments:write |
DELETE/payments/{id} | Deletes a payment; the document's status is recomputed. | payments:write |
Clients
| Request | What it does | Scope |
|---|---|---|
GET/clients | Clients, searched by name, IČO, DIČ or e-mail, with updatedSince and sorting. | clients:read |
GET/clients/{id} | One client. | clients:read |
POST/clients | Creates a client, with a duplicate check. | clients:write |
PATCH/clients/{id} | Updates a client. | clients:write |
DELETE/clients/{id} | Deletes a client no document names (to the recycle bin). | clients:write |
Catalogue
| Request | What it does | Scope |
|---|---|---|
GET/items | Catalogue items, with filters and sorting. | items:read |
GET/items/{id} | One catalogue item. | items:read |
POST/items | Creates a catalogue item. | items:write |
PATCH/items/{id} | Updates a catalogue item. | items:write |
DELETE/items/{id} | Deletes a catalogue item (to the recycle bin). | items:write |
Expenses
| Request | What it does | Scope |
|---|---|---|
GET/expenses | Expenses, filtered by status, category, period and updatedSince. | expenses:read |
GET/expenses/{id} | One expense. | expenses:read |
POST/expenses | Books an expense. | expenses:write |
PATCH/expenses/{id} | Updates an expense. | expenses:write |
DELETE/expenses/{id} | Deletes an expense (to the recycle bin). | expenses:write |
Received e-invoices
| Request | What it does | Scope |
|---|---|---|
GET/received-invoices | Received e-invoices. | received_invoices:read |
GET/received-invoices/{id} | One received e-invoice with its lines and VAT breakdown. | received_invoices:read |
GET/received-invoices/{id}/xml | The archived original UBL of a received e-invoice, with its SHA-256. | received_invoices:read |
Recurring invoices
| Request | What it does | Scope |
|---|---|---|
GET/recurring-invoices | Recurring invoice schedules. | recurring_invoices:read |
GET/recurring-invoices/{id} | One schedule with its latest runs. | recurring_invoices:read |
Company profiles
| Request | What it does | Scope |
|---|---|---|
GET/companies | The workspace's company profiles. | company:read |
Bank accounts
| Request | What it does | Scope |
|---|---|---|
GET/bank-accounts | The workspace's bank accounts. | bank_accounts:read |
GET/bank-accounts/{id} | One bank account. | bank_accounts:read |
Webhooks
| Request | What it does | Scope |
|---|---|---|
GET/webhooks | The webhook endpoints the key or the app manages. | webhooks:read |
GET/webhooks/{id} | One webhook endpoint. | webhooks:read |
POST/webhooks | Registers an endpoint for events the scopes can read; answers with its secret once. | webhooks:write |
PATCH/webhooks/{id} | Changes the URL, events, label or state of an endpoint. | webhooks:write |
DELETE/webhooks/{id} | Deletes an endpoint. | webhooks:write |
POST/webhooks/{id}/rotate-secret | Replaces the signing secret of an endpoint. | webhooks:write |
POST/webhooks/{id}/test | Sends a signed ping event and reports the answer. | webhooks:write |
Examples
They read the key from the VPORIADKU_API_KEY environment variable. Replace the values in angle brackets with ids from earlier responses.
curl "https://api.vporiadku.sk/api/v1/invoices?status=overdue&page=1&limit=50" \
-H "Authorization: Bearer $VPORIADKU_API_KEY"# 1. A draft; the server computes every total itself
curl -X POST https://api.vporiadku.sk/api/v1/invoices \
-H "Authorization: Bearer $VPORIADKU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-2026-1001" \
-d '{"clientId": "<client id>", "issueDate": "2026-10-12", "dueDate": "2026-10-26", "items": [{"name": "Konzultácia", "quantity": 2, "unit": "hod", "unitPrice": 60, "vatRate": 23}]}'
# 2. Issue it: the number is fixed and the content locked
curl -X POST https://api.vporiadku.sk/api/v1/invoices/<invoice id>/issue \
-H "Authorization: Bearer $VPORIADKU_API_KEY"
# 3. E-mail it to the client with the PDF attached
curl -X POST https://api.vporiadku.sk/api/v1/invoices/<invoice id>/send \
-H "Authorization: Bearer $VPORIADKU_API_KEY"curl -X POST https://api.vporiadku.sk/api/v1/invoices/<invoice id>/payments \
-H "Authorization: Bearer $VPORIADKU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 147.60, "paymentDate": "2026-10-20", "paymentMethod": "bank_transfer"}'Webhooks
35 events in 13 groups. vporiadku POSTs them to the URL you register and signs them with that URL's secret.
- A URL receives only the events you choose; every URL has its own whsec_ secret.
- The signature is in the x-vporiadku-signature header: v1= and the HMAC-SHA256 of "timestamp.body". Verify it over the raw body, compare in constant time and refuse a delivery older than 300 seconds.
- Answer 2xx within 10 seconds. Other attempts are retried with a growing delay, 8 times in total; 400, 401, 403, 404, 405, 410 and 422 are not retried.
- The same delivery can arrive twice. Drop repeats by the envelope's idempotencyKey.
- Endpoints, event choice, secrets, a test event and the delivery log are in Settings → API → Webhooks. Through /webhooks, with the webhooks:write scope, you can register, change, test and delete endpoints and replace their secrets. A partner app sees and manages only the endpoints it registered.
- Through the API an endpoint may carry only events whose data the key or the token can read: invoice and document events need invoices:read, payment events payments:read, expense events expenses:read, and so on. Bank feed, VAT report, subscription and team events stay in the app.
Events
- Invoices
invoice.createdinvoice.updatedinvoice.sentinvoice.viewedinvoice.paidinvoice.overdueinvoice.cancelled- Other documents
credit_note.createdquote.createdquote.acceptedquote.rejectedquote.expireddelivery_note.createdorder.created- Payments
payment.recordedpayment.matched- Expenses
expense.createdexpense.updatedexpense.paid- Bank
bank_statement.importedbank_feed.syncedbank_feed.consent_expiring- e-Faktúra
einvoice.senteinvoice.deliveredeinvoice.failedeinvoice.received- Contacts
client.createdsupplier.created- Catalogue
item.created- Recurring invoices
recurring_invoice.generatedrecurring_invoice.failed- VAT
vat_report.generated- Subscription
subscription.updated- Team
team.member_joined- Test
ping
import { createHmac, timingSafeEqual } from 'node:crypto'
export const isGenuine = (rawBody, signature, timestamp, secret) => {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const expected = 'v1=' + createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(String(signature))
return a.length === b.length && timingSafeEqual(a, b)
}
// rawBody = the request body exactly as received; signature = 'x-vporiadku-signature',
// timestamp = 'x-vporiadku-timestamp'. Answer 2xx within 10 seconds, work afterwards,
// and drop repeats by the envelope's idempotencyKey.The envelope, the headers and every event in the API reference →
Access and price
The public API and webhooks are part of the Firma add-on module for the Profi plan, at €3.90 a month excl. VAT. The module also adds a team with roles, several companies in one account and an accountant seat.
The owner manages the keys
Keys are created and revoked by the owner or an admin of the workspace in Settings → API. Give every integration its own key with the fewest scopes it needs.
An integration for many customers
Connecting an e-shop, accounting software or another system for several vporiadku customers? Register it as an app in Settings → API → Apps: each customer connects it with one click and approves the scopes your app asks for. Nobody has to send you a key. Write to us at podpora@vporiadku.sk and we will go through the integration with you.
Frequently asked questions
- How much does API access cost?
- The API and webhooks are part of the Firma add-on module for the Profi plan. Calls cost nothing extra; the limit is 120 requests per minute per key.
- Can I send e-invoices to the Peppol network through the API?
- No. Received e-invoices can be read through the API; the public API does not offer sending over Peppol.
- Is there a test environment?
- There is no separate sandbox: the API always works with your workspace's live data. Start with a read-only key, and try writes knowing that even a draft takes a number from its series.
- How does the API change?
- The version is in the path: /api/v1. Adding a field to a response is not a breaking change. Ignore fields you do not know. A change that would break an existing integration would come in a new version.
- Can webhooks be managed through the API?
- Yes. With the webhooks:write scope, /webhooks registers, changes, tests and deletes endpoints, only for events whose data the key or the token can read. A partner app manages only its own endpoints in each connected workspace.
- How does an app for many customers connect to vporiadku?
- With OAuth 2.0: register the app in Settings → API → Apps, send each customer to the consent screen, and store the tokens you get per workspace. The customer chooses the workspace, approves the scopes and can disconnect the app at any time. Each connected workspace has its own rate limit of 120 requests per minute.