Slovenia — FURS Davčno Potrjevanje Računov
Research doc for the Slovenia fiscalization adapter. Consumed by the implementation bead. No code in this bead.
Status: Research — no adapter code written yet.
Strategic context: Phase-1 Central/Adriatic cluster country (per
docs/strategy/thesis-api-wedge.md, fi-awx). Chosen because: free
FURS-issued certificate, REST/JSON over HTTPS with mTLS + JWS signing
(no SOAP, no HSM), real-time submission, mandatory since 2016 so the
business universe (~150K entities) is mature and tooled. Earns its slot
via cluster effect with Croatia (HR) + Hungary (HU): a single Adriatic
sales motion can land all three. See
apps/docs-internal/docs/ops/country-prioritization.md.
1. Regulatory scope
What must be reported
Slovenia's davčno potrjevanje računov ("fiscal verification of invoices") regime, operated by FURS (Finančna uprava Republike Slovenije — the Financial Administration of the Republic of Slovenia), requires real-time verification of invoices that are paid in cash (or any cash-equivalent instrument) by the buyer.
The legal basis is ZDavPR — Zakon o davčnem potrjevanju računov (Act on Fiscal Verification of Invoices), Official Gazette RS 57/2015, in force from 2016-01-02. Implementing rules: Pravilnik o izvajanju Zakona o davčnem potrjevanju računov (Rules on the implementation of the ZDavPR), Official Gazette RS 60/2015 with subsequent amendments.
Two parallel regimes coexist; Slovenia uniquely allows either:
| Regime | Mechanism | Scope of our adapter |
|---|---|---|
Online certification (davčno potrjevanje računov) | Each invoice POSTed to FURS in real time → FURS returns a unique invoice ID (EOR) which is printed on the receipt | Primary — adapter targets this regime. |
| Pre-numbered invoice books (vezana knjiga računov) | Manual paper books pre-issued by FURS; for taxpayers with ≤ 50 receipts/month who opt in | Out of scope — handled out-of-band by the merchant. |
Who must report
A taxpayer must use online certification when all three conditions are met (ZDavPR Art. 3):
- The taxpayer issues invoices for goods or services delivered in Slovenia.
- The invoice is paid in cash ("gotovina") or by an equivalent non-cash payment considered cash for fiscalization purposes — see below.
- The taxpayer is required to keep accounting records under ZDDV-1 (the VAT Act) or ZDoh-2 (the Personal Income Tax Act).
The wording is "plačilo z gotovino" — "cash payment" — which per ZDavPR Art. 2 pt. 7 explicitly includes:
- Banknotes and coins.
- Payment cards (debit/credit).
- Cheques.
- Other similar means of payment — i.e. anything that is not a direct bank transfer between the buyer's and seller's accounts where the seller's account is named on the invoice.
In practice this captures most B2C transactions and a meaningful share of B2B. The carve-out is direct wire / SEPA bank transfer between named accounts.
Exemptions (ZDavPR Art. 5)
- Sales financed exclusively from public sources by special public agencies.
- Sales of own agricultural products by registered farmers under the flat-rate scheme (
pavšalni nadomestilec) up to specified thresholds. - Sales by personal-supplementary-work providers under ZPDZC-1.
- Vending machines and toll-road kiosks (specific items in the rule book).
- Diplomatic / consular sales.
Thresholds
- No transaction-amount threshold. Every cash-paid invoice in scope is reportable, including 0.01 EUR receipts.
- No taxpayer-size threshold for the online regime — even a single sole proprietor must use it if they issue cash invoices and choose not to opt into the (capped) pre-numbered book regime.
- The pre-numbered book regime is capped at the rule-book limit (currently ~50 invoices/month, indexed); above that limit only online certification is permitted.
B2B?
Yes — when the invoice is paid in cash (per the broad definition above),
B2B is in scope. The customer's tax number (davčna številka for
Slovenian customers, VAT ID for cross-border) is a required field on the
invoice when the customer is a taxpayer.
2. API surface
Base URLs
| Environment | Base URL | Port |
|---|---|---|
| Sandbox (test) | https://blagajne-test.fu.gov.si:9002 | 9002 |
| Production | https://blagajne.fu.gov.si:9003 | 9003 |
Note the non-standard ports. Cited in: Tehnična dokumentacija — Postopki za izmenjavo sporočil prek REST, FURS, current version v2.4 (2024-11). Sandbox and production are separate certificate universes — a production cert cannot be used in sandbox and vice versa.
API version
Current: REST API v1 (path prefix /v1/cash_registers/). The wire
format spec itself is on technical documentation v2.x (current is
v2.4, dated 2024-11), which describes the message contracts that
ride over the v1 path. The version pair has been stable since 2019;
v2.x compatible payloads have been accepted continuously. There is no
public v2 of the path prefix.
Endpoints
All endpoints are POST with Content-Type: application/json; UTF-8.
Body is a JWS Compact Serialization string (header.payload.signature,
all base64url) carrying the JSON request as the payload. All endpoints
are rooted under /v1/cash_registers/.
| Path | Purpose | Auth |
|---|---|---|
POST /v1/cash_registers/echo | Heartbeat / round-trip test (does not record) | mTLS only — no JWS required |
POST /v1/cash_registers/invoices | Submit an invoice for fiscal verification — returns the EOR (unique invoice identifier) | mTLS + JWS |
POST /v1/cash_registers/invoices/register | Register or update a poslovni prostor (business premise) — must be done once per premise before the first invoice from that premise | mTLS + JWS |
Slovenia's surface is dramatically smaller than Hungary or Italy: no token exchange, no transaction polling, no separate query API. Every write is synchronous; either FURS returns the EOR (success) or an error envelope (failure). The simplicity is a deliberate design choice in ZDavPR — the burden of correctness sits on the issuer, not on a queryable state machine.
There is also an XML/SOAP profile of the same operations, served on
the same hosts at port 9007 (test) / 9008 (prod). Both formats
accept the same logical message and are functionally equivalent.
JSON-vs-XML choice for our adapter
We pick JSON over REST. Rationale:
- The JSON profile is the default in the FURS technical documentation v2.x; XML is documented as an alternative for legacy integrations.
- JWS Compact Serialization (RFC 7515) is a smaller, better-supported surface than the SOAP/XAdES envelope used on port 9007/9008.
- Aligns with the rest of our adapters (Hungary v3.0 over JSON-equivalent HTTP+XML, Croatia and Romania over JSON).
- All sandbox tooling and FURS sample code in their public test repo use the JSON profile.
The XSDs in testdata/conformance/slovenia/furs/schemas/ ship for
reference and validation parity; the wire format we exercise in
goldens is JSON.
3. Authentication
One-time, out-of-band: certificate issuance
FURS issues a free X.509 client certificate per cash register to
each taxpayer through the eDavki portal (the Slovenian tax
authority's online service portal at edavki.durs.si). Steps:
- The taxpayer's authorised representative logs into eDavki with their personal SIGEN-CA / SI-PASS / Halcom / etc. citizen certificate.
- Submits the DPR-PridobitevPotrdila form, naming each cash register
they plan to use (one cert per
taxNumber + permanentRegisterId). - eDavki returns a download link for a
.p12file (PKCS#12, password-protected by a token displayed on screen) and a 4-character one-time password to unlock it. - The taxpayer extracts the cert + private key and installs them in the cash register / our adapter.
Cert subject convention:
CN = <taxNumber>-<cashRegisterId>(e.g.CN=10442564-CR1).O = Republika Slovenija,OU = Davčna uprava Republike Slovenije.Issuer: CN = sitest-ca, etc. (sandbox) orCN = sigov-ca(prod).
Sandbox cert
Available immediately in eDavki test environment:
https://test.edavki.durs.si. No real KYC. The test certs are signed
by the sitest-ca intermediate; we must trust this CA in the
adapter's TLS-client trust store when talking to blagajne-test.
Per-request: mTLS
Every request to :9002 / :9003 is over mutual TLS:
- The client (us) presents the FURS-issued client cert above.
- The server (FURS) presents a cert signed by
sitest-ca(test) orsigov-ca(prod). We must validate the chain. - TLS version: TLS 1.2 minimum, ECDHE / RSA 2048+ ciphers.
- SNI: send the literal hostname, not the IP.
mTLS alone identifies the cash register. JWS adds non-repudiation of the message body: even if the TLS were stripped or replayed, the JWS signature anchors the payload to the same cert.
Per-request: JWS Compact Serialization
The HTTP body is a single JWS Compact string:
BASE64URL(JOSE-header) "." BASE64URL(payload-JSON) "." BASE64URL(signature)
JOSE header (the JSON object that gets base64url-encoded as part 1):
{
"alg": "RS256",
"x5c": ["<base64 DER of the issuer cert (no PEM wrapper, no newlines)>"],
"subject_name": "CN=10442564-CR1, ..."
}
algMUST beRS256(RSA-SHA256). FURS does not accept ES256 or PS256 in this surface.x5cMUST contain only the leaf cert (the FURS-issued client cert), as a single-element array, base64-DER-encoded with no PEM framing and no embedded whitespace. FURS resolves the CA chain server-side from its own trust store.subject_nameis OPTIONAL but recommended; FURS uses it for diagnostics. Format must match the cert's actual Subject DN exactly.
Payload: the request JSON object exactly as documented (e.g. an
InvoiceRequest), serialized with stable key order to keep the
signature deterministic across retries.
Signature: RSA-SHA256(privateKey, ASCII(BASE64URL(header) + "." + BASE64URL(payload))), then base64url-encoded.
Content-Type header on the HTTP request: application/json; UTF-8
(FURS quirk — application/jose is not accepted; the body is
"JSON-compatible" because it's a single quoted JWS string when
deserialized).
Response signatures
Every FURS response that carries a successful payload (InvoiceResponse,
BusinessPremiseResponse) is also a JWS Compact signed by FURS's
sigov-ca-issued (or sitest-ca in sandbox) server signing cert. Our
adapter must verify the response signature before treating the EOR
as authoritative — this is the chain of trust that makes the EOR
audit-grade.
echo responses are unsigned (plain JSON body).
Header object
Every JWS payload carries a Header sub-object:
"Header": {
"MessageID": "<UUID v4 — adapter-generated, unique per request>",
"DateTime": "2026-04-26T13:14:15Z"
}
MessageID provides request-level idempotency: re-POSTing the same
JWS (same payload bytes, same MessageID) within FURS's dedup window
returns the original EOR rather than minting a new one. The dedup
window is documented as 48 hours. We MUST mint MessageID
deterministically from the prepared record so that network-level
retries hit the dedup path rather than double-submit.
4. Payload shape
Top-level: InvoiceRequest
{
"InvoiceRequest": {
"Header": {
"MessageID": "61a874c6-625a-4f60-a4b3-e0e1d3a85cc5",
"DateTime": "2026-04-26T13:14:15Z"
},
"Invoice": {
"TaxNumber": 10442564,
"IssueDateTime": "2026-04-26T13:14:15",
"NumberingStructure": "B",
"InvoiceIdentifier": {
"BusinessPremiseID": "BP101",
"ElectronicDeviceID": "BL01",
"InvoiceNumber": 12
},
"CustomerVATNumber": "SI12345678",
"InvoiceAmount": 39.06,
"PaymentAmount": 39.06,
"TaxesPerSeller": [
{
"VAT": [
{
"TaxRate": 22.00,
"TaxableAmount": 32.02,
"TaxAmount": 7.04
}
]
}
],
"OperatorTaxNumber": 12345678,
"ProtectedID": "a7e5f55e1dbb48b799268e1a6d8618a3",
"SubsequentSubmit": false
}
}
}
Field reference (selected)
| Field | Type | Notes |
|---|---|---|
TaxNumber | int (8 digits) | Slovenian taxpayer number, no SI prefix. Must match the taxpayer who owns the issuing certificate. |
IssueDateTime | yyyy-MM-ddTHH:mm:ss | Local time, no timezone — this matches the Header.DateTime in wall-clock terms. Used in ZOI input. |
NumberingStructure | "C" or "B" | "C" = numbering centralized per business premise; "B" = numbering per electronic device (more common). |
InvoiceIdentifier.BusinessPremiseID | string ≤ 20 | Free-form, taxpayer-assigned. Must be pre-registered via BusinessPremiseRequest. |
InvoiceIdentifier.ElectronicDeviceID | string ≤ 20 | Free-form, taxpayer-assigned. Identifies the cash register. Not pre-registered, but must be stable per device. |
InvoiceIdentifier.InvoiceNumber | int ≥ 1 | Strictly increasing per (BusinessPremiseID, ElectronicDeviceID) for NumberingStructure="B", or per BusinessPremiseID for "C". Resets every calendar year. |
CustomerVATNumber | string, optional | Required only if invoice ≥ EUR 100 net and customer is a taxable person. Format: SI prefix for SI taxpayers; ISO country prefix for cross-border EU. |
InvoiceAmount | decimal, 2 places | Total invoice amount including VAT. |
PaymentAmount | decimal, 2 places | Amount paid in cash-equivalent. Usually equals InvoiceAmount; differs only on partial-cash mixed-payment invoices (e.g. card + credit voucher). |
TaxesPerSeller | array | Either one entry per seller (when this register sells on behalf of multiple legal entities — rare) or a single entry for the primary taxpayer. Each entry has VAT (per-rate breakdown), OtherTaxes, ExemptVATTaxableAmount, ReverseVATTaxableAmount, NontaxableAmount. |
OperatorTaxNumber | int | The cashier's personal tax number (a natural person). Required by ZDavPR Art. 11. |
ProtectedID | string (32 lowercase hex) | The ZOI — see §4.1 below. Computed by the adapter, not by FURS. |
SubsequentSubmit | bool | false for real-time submission; true if this was an offline-issued invoice now being uploaded after the fact (within 2 days). |
4.1 ZOI (ProtectedID) — protective issuer code
ZOI = zaščitna oznaka izdajatelja računa. Fixed-output 32-char lowercase hex. Computed by the adapter, never by FURS. Drives non-repudiation: it links a printed receipt back to the cert holder even if FURS itself is offline at issue time.
Algorithm (ZDavPR Pravilnik §11, FURS tech doc v2.4 §3.2):
input = TaxNumber +
IssueDateTime (yyyy-MM-dd HH:mm:ss, NO 'T') +
InvoiceNumber +
BusinessPremiseID +
ElectronicDeviceID +
InvoiceAmount (with 2 fractional digits, '.' separator)
sig = RSA-SHA256-sign(privateKey, input.getBytes("UTF-8"))
ZOI = lowercase(hex(MD5(sig)))
Critical formatting rules:
IssueDateTimehere uses a space separator (yyyy-MM-dd HH:mm:ss), different from the field's wire format which usesT. The two representations must denote the same wall-clock instant.InvoiceAmountis rendered with exactly two decimals, period as separator, no thousands grouping (39.06, never39,06or39.060).- All concatenation is plain string concatenation, no separators between fields. (Yes, this means the boundaries are ambiguous on inspection — but inputs are fixed-format enough that ambiguity does not produce collisions in practice.)
- The MD5 in the last step only consumes the raw signature bytes — not a base64 of them, not a hex of them.
The 32-char ZOI is also rendered on the receipt formatted with dashes
(a7e5f55e-1dbb-48b7-9926-8e1a6d8618a3) — that is a display-only
transformation; the field on the wire is plain hex.
4.2 EOR (UniqueInvoiceID)
EOR = enkratna identifikacijska oznaka računa. Returned by FURS in
InvoiceResponse:
{
"InvoiceResponse": {
"Header": { "MessageID": "...", "DateTime": "..." },
"UniqueInvoiceID": "f01ec7d3-d2b2-4c23-aff8-4d3aa3b3b4f7"
}
}
Format: UUID-like (32 hex chars + 4 dashes). Must be printed on the buyer's receipt alongside the ZOI per ZDavPR Art. 12. Must be persisted by the adapter — it is the audit anchor for any later FURS inspection.
4.3 BusinessPremiseRequest
{
"BusinessPremiseRequest": {
"Header": { "MessageID": "...", "DateTime": "..." },
"BusinessPremise": {
"TaxNumber": 10442564,
"BusinessPremiseID": "BP101",
"BPIdentifier": {
"RealEstateBP": {
"PropertyID": {
"CadastralNumber": 365,
"BuildingNumber": 12,
"BuildingSectionNumber": 3
},
"Address": {
"Street": "Slovenska cesta",
"HouseNumber": "1",
"Community": "Ljubljana",
"City": "Ljubljana",
"PostalCode": "1000"
}
}
},
"ValidityDate": "2026-04-26",
"SoftwareSupplier": [
{ "TaxNumber": 12345678 }
],
"SpecialNotes": ""
}
}
}
Two BPIdentifier flavours: RealEstateBP (a fixed location, identified by
the cadastral triple from the Slovenian land registry) or PremiseType
(a string code in {"A","B","C"} for movable / non-fixed premises:
food trucks, market stalls, web shops). For an e-shop the
PremiseType="B" (web shop) is used and no cadastral data is required.
SoftwareSupplier is us — Zyntem. We register our own taxpayer ID
once per customer onboarding so FURS can reach us if there is a
software-side incident. The supplier ID is NOT a per-tenant secret.
5. Ordering and sequencing
Per-business-premise + per-electronic-device
For NumberingStructure="B" (the default), invoice numbers are strictly
monotonic within (taxNumber, BusinessPremiseID, ElectronicDeviceID, calendarYear). Resets to 1 on January 1st of each year.
For NumberingStructure="C" (centralised at the premise), the third
key drops out and numbering is per (taxNumber, BusinessPremiseID, calendarYear).
Adapter rule: the Transaction.sequence_number from upstream must
be globally monotonic per device (or premise, for "C") and we mint
the InvoiceNumber from it. Gaps are technically permitted but auditable
— FURS will not reject a gap, but a tax inspection will.
Reissue (corrective invoices)
Slovenia treats a corrective invoice as a brand-new invoice with a
new InvoiceNumber and new ZOI, that references the original
through ReferenceInvoice in the payload:
"ReferenceInvoice": [
{
"ReferenceInvoiceIdentifier": {
"BusinessPremiseID": "BP101",
"ElectronicDeviceID": "BL01",
"InvoiceNumber": 12
},
"ReferenceInvoiceIssueDateTime": "2026-04-26T13:14:15"
}
]
The reference is informational only in the wire protocol — FURS does not "void" the original. The new invoice's amounts may be:
- Negative (full credit / storno), reversing the original.
- Positive (additional invoice), settling an underbill.
- Mixed, common in returns (-€39.06) plus replacement (+€41.50) issued as two separate invoices on the same receipt.
There is no STORNO enum; the sign of InvoiceAmount carries the
intent. Negative amounts are explicitly allowed (decimal, with leading
minus).
Cancellation
There is no cancellation API. Once an EOR is issued, it stands. Errors are corrected by issuing a new corrective invoice as above.
6. Submission window
| Mode | Window |
|---|---|
| Online (real-time) | Immediately on issuance — adapter MUST POST the JWS within the same request-response cycle in which the cashier prints the receipt. The receipt prints with the EOR already populated. |
| Offline (degraded) | If FURS is unreachable at issue time, the cashier prints the receipt with the ZOI but no EOR, marks the receipt "Račun še ni potrjen" ("invoice not yet confirmed"), and the adapter MUST submit the invoice within 2 calendar days of issuance, with SubsequentSubmit=true. |
SubmissionWindow value: Immediate (with a 48-hour fallback queue
for offline mode). FURS publishes incident banners; the 2-day clock is
not extended for FURS-side outages — the burden remains on the issuer
to retry until success.
If the 2-day window is missed, the adapter has no recourse via the API; the merchant must self-report the late submission as a tax incident through eDavki (manual, out of band).
7. Sandbox access
How to request a test certificate
- Register at
https://test.edavki.durs.si(Slovenian-language portal, English partial). Test users self-register; no real KYC. - From the dashboard, submit the DPR-PridobitevPotrdila form
listing the planned cash registers (one entry per register; field
OznakaElektronskeNaprave= ourElectronicDeviceID). - eDavki test instantly produces a downloadable
.p12per register plus a 4-char PIN displayed on screen. Capture both immediately — the PIN is shown only once. - The cert chain root:
sitest-ca. Download fromhttps://www.fu.gov.si/fileadmin/Internet/Davcni_postopki/Posebna_podrocja/Davcne_blagajne_in_VKR/Tehnicna_dokumentacija/sitest-ca.cerand pin in the adapter's TLS trust store for sandbox. - Optionally register a sandbox business premise via the same form
set so that the first
BusinessPremiseRequestfrom our adapter does not have to handle a 403.
Timeline: fully self-service. End-to-end (cert in hand, business premise registered, first echo green) in ~30 minutes — the fastest sandbox in our Phase-1 set.
What we'll need
- A
.p12checked into our test-secrets vault for the CI sandbox tenant (SI_FURS_PFX,SI_FURS_PFX_PASSWORD). - The sandbox
sitest-caroot pinned in our test trust store. - A sandbox business premise registered (one-time at suite startup).
- An
.envtemplate in the Slovenia adapter repo with:SI_FURS_TAX_NUMBER,SI_FURS_PFX_PATH,SI_FURS_PFX_PASSWORD,SI_FURS_BUSINESS_PREMISE_ID,SI_FURS_ELECTRONIC_DEVICE_ID,SI_FURS_BASE_URL,SI_FURS_OPERATOR_TAX_NUMBER,SI_FURS_SOFTWARE_SUPPLIER_TAX_NUMBER.
8. Error model
FURS does not use HTTP status codes to disambiguate business errors: nearly every business-validation failure is returned as HTTP 200 OK with an error envelope in the JWS payload. Transport-level errors (TLS fail, 4xx/5xx, malformed JWS) come back as HTTP errors.
Error envelope (in the JWS payload)
{
"InvoiceResponse": {
"Header": { "MessageID": "...", "DateTime": "..." },
"Error": {
"ErrorCode": "S001",
"ErrorMessage": "Račun s podanim ZOI in časom izdaje že obstaja v sistemu davčnih blagajn."
}
}
}
Same envelope shape on BusinessPremiseResponse.
Error-code taxonomy (selected; full list in tech doc §6)
| Code | Class | Meaning | Adapter action |
|---|---|---|---|
S001 | duplicate | An invoice with this (ZOI, IssueDateTime) already exists | Treat as success — fetch original EOR via dedup logic (we cached it) |
S002 | business-validation | TaxNumber mismatch with cert | Bug — config error; halt + alert |
S003 | business-validation | BusinessPremiseID not registered | Re-run BusinessPremiseRequest, then retry |
S004 | business-validation | InvoiceAmount inconsistent with TaxesPerSeller breakdown | Bug — fix arithmetic; reissue with new InvoiceNumber |
S005 | schema | JSON schema violation | Bug — fix payload; retry with same MessageID |
S100 | signature | JWS signature invalid | Bug — re-sign; retry |
S101 | signature | x5c not matched in FURS trust store | Bug — wrong cert; halt + alert |
S200 | rate / availability | FURS overloaded, retry later | Backoff + retry |
S300 | system | FURS internal error | Retry with same MessageID |
Distinguish three retry classes:
- Transport (TLS error, HTTP 5xx, timeout): retry with same JWS bytes (idempotent via MessageID + 48h dedup window).
- System (
S200,S300): retry with backoff (250ms, 1s, 4s, 16s, cap at 60s). - Validation (everything else
S0**): halt and surface; do not retry. - Duplicate (
S001): treat as success — the original EOR is in our store, return it. (This is how the dedup window helps us at all.)
Echo
POST /v1/cash_registers/echo with {"EchoRequest": "ping"} returns
{"EchoResponse": "ping"} on success. Used for connectivity smoke tests
before each batch (and as a CI health probe). Round-trip <200ms typical.
9. Implementation plan
Crate layout
Candidate name: slovenia-furs (matches hungary-nav /
croatia-cis naming pattern in the rust/adapters/ workspace).
rust/adapters/slovenia/
├── Cargo.toml
└── src/
├── lib.rs # SloveniaAdapter (impl CountryAdapter)
├── config.rs # SloveniaConfig (cert, premise, device, …)
├── auth.rs # JWS sign + verify, x5c handling
├── zoi.rs # ProtectedID computation (RSA-SHA256 + MD5)
├── client.rs # reqwest mTLS client, retry, echo probe
├── invoice.rs # Transaction -> InvoiceRequest JSON
├── premise.rs # BusinessPremiseRequest builder + register-once cache
├── types.rs # serde models (Header, Invoice, TaxesPerSeller, …)
├── errors.rs # AdapterError variants per error class
└── tests.rs # JSON-Schema conformance against golden fixtures
Trait fit
CountryAdapter::prepare() is a clean two-phase split here:
- prepare() — build the InvoiceRequest JSON, compute the ZOI,
produce a deterministic
fiscal_id={taxNumber}:{businessPremiseId}:{electronicDeviceId}:{invoiceNumber}, cache the JSON bytes forsubmit(). Network-free (ZOI requires the private key but not FURS). - submit() — JWS-sign → POST to
/invoices→ verify response JWS → extract EOR. Synchronous;SubmitResult.upstream_id = EOR.
DeploymentMode::Cloud is the primary target — the cert lives in
our HSM/secret store and signs server-side. Embedded works
identically (the cert can live on the cash register), but Cloud is the
thesis.
SubmissionWindow::Immediate (with the 48h offline fallback as a queue
behaviour, not a window relaxation).
Config shape
pub struct SloveniaConfig {
/// SI taxpayer number, 8 digits, no SI prefix (e.g. 10442564).
pub tax_number: u64,
/// PKCS#12 cert + private key bundle (FURS-issued, one per register).
pub pfx_bytes: Vec<u8>,
pub pfx_password: SecretString,
/// Pre-registered business premise (must match BusinessPremiseRequest).
pub business_premise_id: String,
/// Stable identifier for this cash register / device.
pub electronic_device_id: String,
/// "B" (per-device numbering, default) or "C" (per-premise numbering).
pub numbering_structure: NumberingStructure,
/// Cashier tax number, required on every invoice.
pub default_operator_tax_number: u64,
/// Our (Zyntem) software-supplier tax number.
pub software_supplier_tax_number: u64,
/// true => :9002 sandbox, false => :9003 production.
pub sandbox: bool,
}
Dependencies (expected)
reqwest(mTLS vianative-tlsorrustls+rustls-pemfile) — already in workspace.serde/serde_json— already in workspace.rsa+sha2— RSA-SHA256 for JWS sign and for ZOI input signing.md-5— MD5 of the ZOI signature.base64(URL-safe variant) — JWS Compact encoding.x509-parser— to extract Subject DN from the PKCS#12 cert for the JWSsubject_namefield.p12orpkcs12— read PKCS#12 to extract cert chain + private key.chrono— already in workspace.uuid(v4 deterministic via SHA-256) — MessageID minting.
No SOAP, no XAdES, no native fiscal libs. The cryptography is RSA-SHA256 + MD5 + AES-stream — entirely software, no HSM needed (though a Cloud-HSM-backed key is an upgrade path post-MVP).
Open questions (for implementation bead)
- ZOI format on receipt printing. Adapter returns the 32-char hex. The receipt-printing layer (downstream) must format it with dashes per ZDavPR. Should the adapter expose both? Recommend: expose hex only; downstream owns presentation.
OperatorTaxNumbersource. Required per ZDavPR Art. 11. OurTransactionschema doesn't carry a cashier ID today. Options: (a) addTransaction.operator_idand require upstream to map to tax number, (b) take a static default from config and let upstream override per-call. Recommend: (b) with a strictdefault_operator_tax_numberand an optional override field.- Numbering reset on January 1st. FURS expects
InvoiceNumber=1on the first invoice of each calendar year per(BusinessPremise, ElectronicDevice). OurTransaction.sequence_numberis monotonic forever. The adapter needs a per-(premise, device, year) counter rebase. Recommend: aNumberingStrategytrait with a year-aware default impl. - Multiple sellers per device. A cash register can issue invoices
on behalf of multiple legal entities (consignment: e.g. a market
stall renting space to multiple producers).
TaxesPerSelleris an array for this reason. OurTransactionties to a singleLocation → Account → TaxNumber. Recommend: out of scope for v1; document as a known limitation; only the primary taxpayer. - SubsequentSubmit + offline queue. The 2-day offline window is
real and needs a persistent queue. Where does it live — in the
adapter or in the higher-level submission orchestrator? Recommend:
orchestrator (the queue is country-agnostic infrastructure), the
adapter just exposes a
SubmissionMode::Subsequentvariant that sets the wire-level flag. - eDavki cert provisioning. Self-service for the taxpayer but
not for us (we don't get the
.p12; it's emailed to / downloaded by the merchant). Onboarding flow: merchant uploads.p12+ PIN via secure form. Recommend: build a one-page upload UI in the merchant onboarding portal; encrypt the PFX with KMS at rest. - PremiseType="B" for cloud-only e-shops. A pure-online merchant
with no physical premise registers
PremiseType="B"once and uses that BusinessPremiseID for every invoice. Worth confirming whether the cadastral triple is truly optional in this case (FURS schemas mark itnillable=trueonly whenPremiseTypeis set). The fixture pack proves it; double-check on first sandbox round-trip.
What could force us off Cloud-OK?
Nothing identified in the spec. No HSM requirement, no client-side
hardware seal, no XAdES. RSA-2048 keys on a .p12, RS256 JWS signing
done in software. The only constraint is that the private key must
remain accessible at signing time — for Cloud that means it lives in
our KMS / secret store, encrypted at rest.
This is a green light — Slovenia confirms the Phase-1 cluster thesis.
Testing strategy
- Unit (in-adapter):
- ZOI vector tests against the worked example in FURS tech doc §3.2.
- JWS round-trip: sign → verify with same cert → bytes-equal payload.
- JSON-Schema validation on all 6 golden triplets using the schemas in
testdata/conformance/slovenia/furs/schemas/.
- Integration (against sandbox): full
BusinessPremiseRequest → echo → InvoiceRequest → response-JWS-verifyround-trip. Gated behindSI_FURS_SANDBOX_CREDENTIALSenv var so CI without creds still passes unit tests. - Conformance: every triplet must round-trip
input.json → adapter → wire-bytes, and the wire-bytes must be a byte-exact match togolden.jsonafter canonicalising key order.
Rollout sequence (for the implementation bead)
- Day 1:
zoi.rs+ worked-example vector test passing. - Day 2:
auth.rsJWS sign/verify; PKCS#12 cert load; subject_name extraction. - Day 3:
invoice.rsbuilder + JSON-Schema conformance against all 6 goldens. - Day 4:
client.rsmTLS client + echo probe + first sandbox round-trip. - Day 5:
premise.rs+ register-once cache; sandbox premise creation. - Day 6: Error model + retry classes;
S001dedup short-circuit. - Day 7:
SubsequentSubmitpath + offline-queue interface. - Day 8: Soak test against sandbox: 100 invoices over an hour, observe latencies.
10. Citations
All facts above are sourced from one of:
- ZDavPR — Zakon o davčnem potrjevanju računov, Official Gazette RS 57/2015. Latest consolidated text: https://www.uradni-list.si/glasilo-uradni-list-rs/vsebina/2015-01-2389?sop=2015-01-2389.
- Pravilnik o izvajanju ZDavPR — Official Gazette RS 60/2015 with amendments. Consolidated: https://www.uradni-list.si/glasilo-uradni-list-rs/vsebina/2015-01-2477?sop=2015-01-2477.
- FURS technical documentation — Tehnična dokumentacija — Postopki
za izmenjavo sporočil prek REST, current v2.4 (2024-11), at
https://www.fu.gov.si/davki_in_druge_dajatve/poslovanje_z_nami/davcno_potrjevanje_racunov/tehnicna_dokumentacija/.
File names:
Tehnicna_dokumentacija_v2.4.pdf,Protokol_za_izmenjavo_sporocil_v2.4.pdf. - FURS JSON / XML schemas — published in the same documentation
index, files
JSON_shema_za_potrditev_racuna_v2.4.json,JSON_shema_za_potrditev_poslovnega_prostora_v2.4.json, and the corresponding XSDs (*.xsd) for the SOAP profile. - FURS sample messages — Vzorčna sporočila, included in the documentation index, used as the baseline for the goldens here.
- eDavki test environment — https://test.edavki.durs.si.
- CA roots —
sitest-ca(sandbox) andsigov-ca(production), both downloadable as.cerfrom https://www.fu.gov.si/fileadmin/Internet/Davcni_postopki/Posebna_podrocja/Davcne_blagajne_in_VKR/Tehnicna_dokumentacija/.
Per-fixture citations live in each meta.json under
testdata/conformance/slovenia/furs/golden/.