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.

  1. 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.

  2. 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.

  3. Step 3

    Send the first request

    Ping checks the key and returns its scopes. Keep the key in an environment variable, never in code.

  4. 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.

Checking the key
curl
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_.

HTTP
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. 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. 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. 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. 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. 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. 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.
Generating the PKCE verifier, the challenge and the state
Node.js
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.
The consent screen link for a customer
URL
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=S256
Trading the code for tokens
curl
curl -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"
Refreshing the tokens
curl
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"
Registering a webhook endpoint in the customer's workspace
curl
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.

ScopeWhat it allows
invoices:readRead documents: list, detail, PDF, ISDOC and UBL.
invoices:writeCreate, copy, update and delete drafts, issue, e-mail, send reminders, credit and cancel.
payments:readRead the payments recorded on documents.
payments:writeRecord payments, settle the rest of a document and delete payments.
clients:readRead the client directory.
clients:writeCreate, update and delete clients.
items:readRead the catalogue.
items:writeCreate, update and delete catalogue items.
expenses:readRead expenses.
expenses:writeBook, update and delete expenses.
received_invoices:readRead received e-invoices and their originals.
recurring_invoices:readRead recurring invoice schedules.
company:readRead the workspace's company profiles.
bank_accounts:readRead the workspace's bank accounts.
webhooks:readRead the webhook endpoints the key or the app manages.
webhooks:writeRegister, 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
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.
A new client, safe to retry thanks to Idempotency-Key
curl
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.

The answer to a write with a key that may only read
JSON
{
  "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"
      ]
    }
  }
}
CodeMeaning
UNAUTHORIZED401: the key or the access token is missing, unknown, revoked or expired.
FORBIDDEN403: the workspace does not have the Firma module.
API_KEY_SCOPE_MISSING403: the key or the token lacks a scope; details.required names it.
FEATURE_NOT_AVAILABLE403: the feature is not in the workspace's plan.
PLAN_LIMIT_REACHED403: a limit of the plan is used up; details give the limit and the current count.
VALIDATION_FAILED400: the request body did not pass validation; details.errors lists the problems.
NOT_FOUND404: there is no such thing in this workspace.
IDEMPOTENCY_KEY_REUSED409: the Idempotency-Key already belongs to another request.
IDEMPOTENCY_KEY_IN_USE409: a request with this Idempotency-Key is still running.
WEBHOOK_EVENT_NOT_ALLOWED403: a webhook event needs a scope the key or the token lacks, or stays in the app.
RATE_LIMIT_EXCEEDED429: the limit is used up; wait Retry-After seconds.
INTERNAL_ERROR500: 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

RequestWhat it doesScope
GET/pingChecks the key and returns the plan, the key prefix and its scopes.none

Invoices and documents

RequestWhat it doesScope
GET/invoicesDocuments 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}/pdfThe document's PDF, exactly as the customer receives it.invoices:read
GET/invoices/{id}/isdocThe document as ISDOC 6.0.1 XML for accounting software.invoices:read
GET/invoices/{id}/ublAn issued invoice as Peppol BIS 3.0 UBL XML; needs the e-Faktúra module, which is being prepared.invoices:read
POST/invoicesCreates 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}/issueIssues a draft: fixes its number and locks its content.invoices:write
POST/invoices/{id}/duplicateCopies a document into a new draft dated today.invoices:write
POST/invoices/{id}/sendE-mails an issued document to the client with the PDF.invoices:write
POST/invoices/{id}/reminderSends a payment reminder for an unpaid document.invoices:write
POST/invoices/{id}/credit-noteIssues a credit note against an invoice.invoices:write
POST/invoices/{id}/cancelVoids an issued document with a cancellation document.invoices:write

Payments

RequestWhat it doesScope
GET/invoices/{id}/paymentsThe payments recorded on a document.payments:read
POST/invoices/{id}/paymentsRecords a payment; a covered document is marked paid.payments:write
POST/invoices/{id}/mark-paidSettles 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

RequestWhat it doesScope
GET/clientsClients, searched by name, IČO, DIČ or e-mail, with updatedSince and sorting.clients:read
GET/clients/{id}One client.clients:read
POST/clientsCreates 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

RequestWhat it doesScope
GET/itemsCatalogue items, with filters and sorting.items:read
GET/items/{id}One catalogue item.items:read
POST/itemsCreates 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

RequestWhat it doesScope
GET/expensesExpenses, filtered by status, category, period and updatedSince.expenses:read
GET/expenses/{id}One expense.expenses:read
POST/expensesBooks 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

RequestWhat it doesScope
GET/received-invoicesReceived 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}/xmlThe archived original UBL of a received e-invoice, with its SHA-256.received_invoices:read

Recurring invoices

RequestWhat it doesScope
GET/recurring-invoicesRecurring invoice schedules.recurring_invoices:read
GET/recurring-invoices/{id}One schedule with its latest runs.recurring_invoices:read

Company profiles

RequestWhat it doesScope
GET/companiesThe workspace's company profiles.company:read

Bank accounts

RequestWhat it doesScope
GET/bank-accountsThe workspace's bank accounts.bank_accounts:read
GET/bank-accounts/{id}One bank account.bank_accounts:read

Webhooks

RequestWhat it doesScope
GET/webhooksThe webhook endpoints the key or the app manages.webhooks:read
GET/webhooks/{id}One webhook endpoint.webhooks:read
POST/webhooksRegisters 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-secretReplaces the signing secret of an endpoint.webhooks:write
POST/webhooks/{id}/testSends 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.

Overdue invoices, 50 per page
curl
curl "https://api.vporiadku.sk/api/v1/invoices?status=overdue&page=1&limit=50" \
  -H "Authorization: Bearer $VPORIADKU_API_KEY"
An invoice from draft to e-mail
curl
# 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"
Recording a payment
curl
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
Verifying the signature in Node.js
Node.js
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.