Hungary — NAV Online Számla
Research doc for the Hungary fiscalization adapter. Consumed by the implementation bead (fi-g40). No code in this bead.
Status: Research — no adapter code written yet.
Strategic context: Phase-1 wedge, country #3 after Spain/Italy. Chosen because
€0 cert cost, 2–4 week sandbox onboarding, Cloud-OK (REST + token, no local
signing), 800K businesses, cleanest REST surface in the EU. See
docs/strategy/thesis-api-wedge.md (fi-awx) and
apps/docs-internal/docs/ops/country-prioritization.md.
1. Regulatory scope
What must be reported
Hungary's Online Számla (Online Invoice) system, operated by NAV (Nemzeti Adó- és Vámhivatal — the National Tax and Customs Administration), requires real-time invoice data reporting for VAT-registered entities issuing invoices under Hungarian VAT law (Act CXXVII of 2007 on VAT, §10). Three successive expansions define current scope:
| Effective date | Scope change |
|---|---|
| 2018-07-01 | B2B invoices with VAT ≥ 100,000 HUF (domestic only) — initial launch, v1.0 |
| 2020-07-01 | All domestic B2B invoices regardless of VAT amount (threshold removed), v2.0 |
| 2021-01-04 | Added: cross-border invoices (EU + non-EU supplies where seller is HU VAT subject). Grace period. v3.0 |
| 2021-07-01 | Added: B2C invoices (issued to non-taxable natural persons). Core data only — name/address of the customer are optional. Current obligation: all invoices issued by HU VAT subjects, without threshold. |
| 2024-01-01 | eVAT (eÁfa) M2M interface launched (separate system, out of scope for this adapter). |
Note — no VAT threshold. Unlike Spain SII (which keys off tax residency and transaction type) Hungary requires reporting every invoice issued under HU VAT, including 0 HUF domestic tax-exempt invoices.
Exemptions
- Invoices received (import side) — recipient does not report.
- Invoices issued by non-HU-VAT-subject entities — not in scope.
- Receipts (nyugta, cash-register slips) — handled by a separate online cash register ("pénztárgép") regime, not Online Számla. Our adapter does not target the pénztárgép regime; a POS which issues a proper számla (invoice) for every transaction reports through Online Számla.
- Simplified invoices (egyszerűsített számla, under 900,000 HUF gross where
the seller chooses the simplified form) — are in scope; category
SIMPLIFIEDin the schema. - Aggregate invoices (gyűjtőszámla) — in scope; category
AGGREGATE.
Thresholds (current)
- None for the reporting obligation itself.
- For data content, the 100,000 HUF VAT threshold controls mandatory customer
tax-number inclusion on the invoice itself (a separate VAT Act rule). Our
adapter must include
customerTaxNumberwhen the input provides it; otherwise thecustomerVatStatusfield (DOMESTIC/OTHER/PRIVATE_PERSON) controls what customer identification is required.
2. API surface
Base URLs
| Environment | API base | Web portal |
|---|---|---|
| Sandbox (test) | https://api-test.onlineszamla.nav.gov.hu/ | https://onlineszamla-test.nav.gov.hu/ |
| Production | https://api.onlineszamla.nav.gov.hu/ | https://onlineszamla.nav.gov.hu/ |
Cited in the NAV GitHub README (nav-gov-hu/Online-Invoice) and the interface
specification PDF v3.0 (HU). Sandbox and production are separate taxpayer
universes — a production tax number cannot be used in sandbox.
API version
Current: 3.0 (since 2020-11). common:requestVersion = "3.0";
common:headerVersion = "1.0". Confirmed from the XSD headers
(invoiceApi.xsd v3.0 2020/11/09, invoiceData.xsd v3.0 2020/11/23) and from
NAV's public developer log (fejlesztoi_naplo).
There is no public v4.0 as of the 2026-04 freeze. Historical v1.0/v1.1/v2.0 are retired on production; v3.0 has been the only live version since 2021-04-01.
Endpoints
All endpoints are POST with Content-Type: application/xml, request body
is XML in the http://schemas.nav.gov.hu/OSA/3.0/api namespace. All paths are
relative to the base URL and rooted under /invoiceService/v3/.
| Path | Purpose | Auth |
|---|---|---|
POST /invoiceService/v3/tokenExchange | Exchange login + signatureKey for a 5-minute one-shot exchange token | basic (login + passwordHash + requestSignature) |
POST /invoiceService/v3/manageInvoice | Submit CREATE / MODIFY / STORNO — up to 100 invoices per call | basic + exchangeToken |
POST /invoiceService/v3/manageAnnulment | Technical annulment (cancel a submitted transaction that was erroneous at the API level — not a STORNO) | basic + exchangeToken |
POST /invoiceService/v3/queryTransactionStatus | Poll processing status + per-index results for a submission | basic |
POST /invoiceService/v3/queryTransactionList | Paginated list of prior submissions | basic |
POST /invoiceService/v3/queryInvoiceData | Retrieve a submitted invoice by invoice number / direction | basic |
POST /invoiceService/v3/queryInvoiceCheck | Yes/no existence check for an invoice number | basic |
POST /invoiceService/v3/queryInvoiceDigest | Paginated search over one's submitted invoices (date, partner, etc.) | basic |
POST /invoiceService/v3/queryInvoiceChainDigest | Ordered chain of CREATE + MODIFY + STORNO for a single invoice number | basic |
POST /invoiceService/v3/queryTaxpayer | Public lookup: is a HU tax number valid + active + what is its name/address | basic |
Adapter hot path: tokenExchange → manageInvoice → (later) queryTransactionStatus.
3. Authentication
Credentials (one-time, out-of-band)
Each taxpayer registers a technical user (technikai felhasználó) in the
Online Számla portal (/felhasznalok/uj/technikai). Registration yields
four secret strings that the adapter stores per-tenant:
| Field | Purpose | Format |
|---|---|---|
login | Technical user ID | 15-char alphanumeric |
password | Technical user password | User-chosen; adapter stores plaintext (used to compute passwordHash per request) — ideally stored as SHA-512 digest only, and we transmit the digest; we never need the plaintext |
signatureKey | HMAC-ish key used in requestSignature | XX-XXXXXXXX-XXXXXXXXXXXXXXXXXXX — 19 chars with 2 dashes |
exchangeKey | AES-128 key used to decrypt the exchange token from tokenExchange responses | 16 bytes, delivered base-64 |
Plus the taxpayer ID (taxNumber, the 8-digit HU tax number root —
without the VAT code -N- and county suffixes).
Per-request header (common:user)
Every request includes:
<common:user>
<common:login>lwilsmn0uqdxe6u</common:login>
<common:passwordHash cryptoType="SHA-512">
SHA-512(password) hex, uppercase, 128 chars
</common:passwordHash>
<common:taxNumber>11111111</common:taxNumber>
<common:requestSignature cryptoType="SHA3-512">
see below
</common:requestSignature>
</common:user>
requestSignature algorithm (critical)
Per the v3.0 interface spec, §1.5.1 "requestSignature számítása" and
CHANGELOG_2.0.md:
For query operations (every endpoint except manageInvoice / manageAnnulment):
requestSignature = uppercase(hex(
SHA3-512(
requestId +
timestamp_compact + // YYYYMMDDhhmmss (UTC, NO dashes/colons/T/Z)
signatureKey
)
))
Where:
requestIdis the 30-char alnum ID the client sent incommon:header/requestId(must be globally unique for that taxpayer within a 12-month window).timestamp_compactis thecommon:timestampvalue, reformatted toYYYYMMDDhhmmsswith no separators (drop theT,-,:,.,Z, and the fractional seconds).signatureKeyis the raw 19-char string (including dashes), exactly as issued.
For /manageInvoice and /manageAnnulment (the write-path exception):
base = requestId + timestamp_compact + signatureKey
per_index[i] = uppercase(hex(SHA3-512(
invoiceOperation_i + invoiceData_i_base64 // e.g. "CREATE" + "PD94bWw…"
)))
requestSignature = uppercase(hex(SHA3-512(
base + per_index[1] + per_index[2] + … + per_index[N]
)))
The invoiceData_i_base64 is exactly the string that appears in
<invoiceData>…</invoiceData> in the request (i.e. already base64-encoded,
already gzipped if compressedContent=true).
In v1.x the per-index values used CRC32; v2.0+ they are SHA3-512. The
cryptoType attribute on <requestSignature> is SHA3-512 for v3.0.
tokenExchange flow
- Client POSTs a
TokenExchangeRequest(no token yet, just basic user fields- software block).
- NAV returns
<encodedExchangeToken>(base-64 of AES-128/ECB-encrypted raw token bytes) and<tokenValidityFrom>/<tokenValidityTo>(valid 5 minutes). - Client decrypts with
exchangeKey(AES-128/ECB, PKCS5 padding) → plaintext token string (ASCII). - Client includes that plaintext token as
<exchangeToken>in the nextmanageInvoiceRequest.
The token is one-shot: it's valid for exactly one manageInvoice call. The
adapter must exchange a fresh token before each submission. Our cache strategy
is therefore "cache for <4 minutes, on use consume and discard".
Software block
Every request (inside the main <software> element) advertises the reporting
software:
<software>
<softwareId>123456789123456789</softwareId> <!-- 18-char tech ID we pick and keep stable -->
<softwareName>Zyntem Fiscalization</softwareName>
<softwareOperation>LOCAL_SOFTWARE | ONLINE_SERVICE</softwareOperation>
<softwareMainVersion>1.0.0</softwareMainVersion>
<softwareDevName>Zyntem Kft.</softwareDevName>
<softwareDevContact>support@zyntem.eu</softwareDevContact>
<softwareDevCountryCode>HU</softwareDevCountryCode> <!-- optional -->
<softwareDevTaxNumber>…</softwareDevTaxNumber> <!-- optional -->
</software>
For Cloud deployment, softwareOperation = "ONLINE_SERVICE". We own this
value; NAV does not certify it. We do need to register our softwareId
prefix — not via an upstream process, we simply pick an 18-char ID and keep
it stable across releases.
4. Payload shape
Top-level request: ManageInvoiceRequest
ManageInvoiceRequest (namespace: …/OSA/3.0/api)
├── common:header # requestId, timestamp, requestVersion=3.0, headerVersion=1.0
├── common:user # login, passwordHash, taxNumber, requestSignature
├── software # as above
├── exchangeToken # plaintext from tokenExchange step
└── invoiceOperations
├── compressedContent (bool) # if true, each invoiceData is gzip+base64
└── invoiceOperation × {1..100}
├── index (1..100) # must be sequential from 1, no gaps
├── invoiceOperation (CREATE|MODIFY|STORNO)
├── invoiceData (base64, optionally gzipped InvoiceData XML)
└── electronicInvoiceHash (optional; for e-invoices stored by issuer)
InvoiceData (the fiscal payload)
This is the actual invoice, base64-encoded inside <invoiceData>. The raw
XML validates against invoiceData.xsd v3.0:
- XSD: https://github.com/nav-gov-hu/Online-Invoice/blob/master/src/schemas/nav/gov/hu/OSA/invoiceData.xsd
- Namespace:
http://schemas.nav.gov.hu/OSA/3.0/data - Imports:
…/OSA/3.0/base(invoiceBase.xsd),…/NTCA/1.0/common(common.xsd)
Top-level shape (simplified):
InvoiceData
├── invoiceNumber (1..50 chars, unique per issuer per year — also the legal invoice number)
├── invoiceIssueDate (YYYY-MM-DD)
├── completenessIndicator (bool; true = this XML contains the entire invoice content, no external paper doc)
├── electronicInvoiceHash (optional)
└── invoiceMain
└── invoice
├── invoiceHead
│ ├── supplierInfo
│ │ ├── supplierTaxNumber (taxpayerId + vatCode + countyCode)
│ │ ├── groupMemberTaxNumber (optional, for VAT groups)
│ │ ├── communityVatNumber (optional)
│ │ ├── supplierName
│ │ ├── supplierAddress (simple or detailed)
│ │ ├── supplierBankAccountNumber (optional)
│ │ ├── individualExemption (bool; alanyi adómentes)
│ │ └── …
│ ├── customerInfo
│ │ ├── customerVatStatus (DOMESTIC | OTHER | PRIVATE_PERSON)
│ │ ├── customerVatData (customerTaxNumber xor communityVatNumber xor thirdStateTaxId)
│ │ ├── customerName (required for DOMESTIC/OTHER; optional for PRIVATE_PERSON)
│ │ ├── customerAddress
│ │ └── customerBankAccountNumber (optional)
│ ├── fiscalRepresentativeInfo (optional)
│ └── invoiceDetail
│ ├── invoiceCategory (NORMAL | SIMPLIFIED | AGGREGATE)
│ ├── invoiceDeliveryDate
│ ├── invoiceDeliveryPeriodStart / End (for period invoices)
│ ├── invoiceAccountingDeliveryDate
│ ├── periodicalSettlement (bool)
│ ├── smallBusinessIndicator (bool)
│ ├── currencyCode (ISO 4217)
│ ├── exchangeRate (decimal; 1 if HUF)
│ ├── utilitySettlementIndicator (bool)
│ ├── selfBillingIndicator (bool)
│ ├── paymentMethod (TRANSFER | CASH | CARD | VOUCHER | OTHER)
│ ├── paymentDate
│ ├── cashAccountingIndicator (bool)
│ ├── invoiceAppearance (PAPER | ELECTRONIC | EDI | UNKNOWN)
│ └── …
├── invoiceLines (lines[], each with lineNumber, lineDescription,
│ quantity, unitOfMeasure, unitPrice, lineVatRate, …)
└── invoiceSummary
├── summaryNormal (for NORMAL/AGGREGATE; per-VAT-rate breakdown)
├── summarySimplified (for SIMPLIFIED; gross-only per VAT rate)
└── summaryGrossData (overall totals in invoice currency + HUF)
See invoiceData.xsd for the full list — ~200 named types. Our golden fixtures
cover the common subset.
Encoding & compression
<invoiceData>content is always base64-encoded.- If
<compressedContent>true</compressedContent>, the raw XML is gzipped first, then base64'd. Saves ~60% on the wire for large invoices; NAV enforces a 3 MB cap on the XML body (not the base64 length), so large multi-line invoices need compression. - UTF-8 always.
<?xml version="1.0" encoding="UTF-8"?>header required on the inner XML. - HUF monetary values are integers. Other currencies are decimals with up to 2 fractional digits.
invoiceOperation values
| Value | Meaning |
|---|---|
CREATE | Original invoice report. |
MODIFY | Modification (= Hungarian helyesbítő / módosító) document. Carries originalInvoiceNumber reference and modifyWithoutMaster=true/false. Can also be a supplementing invoice (pót-számla). |
STORNO | Cancelling invoice (sztornó számla). References the original. Represents a single legal document that wholly reverses the original. |
NAV does not let you "delete" a previously submitted invoice via
manageInvoice. Mistakes are corrected by issuing and submitting a new
document. The only true delete is manageAnnulment, which is reserved for
"this submission was technically wrong and should not count" — restricted to
24 hours after the original submission, and flagged for manual NAV review
(AnnulmentVerificationStatus).
5. Ordering and sequencing
Transaction-level semantics
- Each
manageInvoicecall returns atransactionId(20-char alnum). - Up to 100 invoices per call; NAV processes them in-order by
indexbut each succeeds/fails independently. - The transactionId + (taxNumber + requestId) together give idempotency.
Idempotency via requestId
requestIdmust be unique per taxpayer for 12 months.- If the client sends a duplicate
requestId, NAV responds with a general error (INVALID_REQUEST, validationErrorCodeREQUEST_ID_NOT_UNIQUE) — and the prior submission remains canonical. - Adapter rule: we mint
requestIddeterministically from the prepared record ID (RID+ 20 chars), so network-level retries (timeout without response) resend the identical request and NAV returns the original transactionId instead of double-submitting. On receivingREQUEST_ID_NOT_UNIQUE, we callqueryTransactionListto find the prior transactionId and treat the submission as successful.
Per-invoice idempotency
supplierTaxNumber + invoiceNumberis a uniqueness key within a year.- Submitting a
CREATEwith an already-reportedinvoiceNumberreturns business-validationERRORINVOICE_NUMBER_NOT_UNIQUEand the invoice is rejected (statusABORTEDper-index); the other invoices in the same batch still process normally. - Adapter rule: we compute the invoice number at prepare-time and never change it on retry.
Retries
- Safe to retry the entire manageInvoice call on network error, 5xx, or
tokenExchange-expired errors. Dedup via requestId. - Never synthesise a new requestId to "get past" a duplicate error — that
will either double-report or deadlock. Use
queryTransactionListto reconcile. queryTransactionStatusis safe to poll; NAV publishes per-second rate limits (documented as 10 req/s per taxpayer, not strictly enforced but respected by adapter).
6. Submission window
Two distinct regimes coexist in Hungarian law:
| Issuer | Window |
|---|---|
| Invoicing software (our customers) | Immediately, "without delay". Operationalised by NAV as: the software must POST the invoice to Online Számla at the moment of issuance. The 4-second guidance circulating in press is a soft target from NAV's 2020 FAQ; it is not in the statute but is the effective SLA. |
| Manually-issued paper invoices (non-software) | Within 5 calendar days of issuance, via the web form. Extended to 4 days when VAT ≥ 500,000 HUF — but if they use software, that override is moot. |
Our adapter targets the "invoicing software, immediately" regime.
SubmissionWindow value: Immediate (same as Spain Verifactu). Practical
target: submit within the request-response cycle of the POS transaction,
with an async fallback queue if NAV is slow (>2 seconds) or unreachable.
If NAV is down for an extended period, the statute's position is that the issuance is valid and the reporting obligation must be fulfilled "as soon as the impediment ceases" — NAV publishes incident banners; a ≤72h retry window is the operational norm.
7. Sandbox access
Registering a sandbox taxpayer
- Go to
https://onlineszamla-test.nav.gov.hu/regisztracio/start("Regisztráció"). - Choose "Tesztpéldány - kizárólag fejlesztői célra" (test instance for development use only).
- Register a fictitious taxpayer. For sandbox you pick a made-up 8-digit tax number (any value starting with 1). No real KYC, no real tax authority interaction. Free.
- Log in to the test portal, go to Felhasználók → Új technikai felhasználó.
- NAV generates the four credentials:
login, initialpassword,signatureKey,exchangeKey. ThesignatureKeyandexchangeKeyare displayed once — capture immediately. - Optionally create a secondary technical user for the same taxpayer (we use two — one for CI, one for manual testing — to avoid requestId collisions).
Timeline: fully self-service. Expect 2–4 weeks to validate end-to-end because NAV sandbox sometimes lags (queries return empty for ~hours after a submission), and because the first real-mode cert flow with a customer in production does require NAV review of a test-mode dry run.
What we'll need
- A
signatureKey+exchangeKeychecked into our password vault for the CI sandbox tenant. - A
softwareIdwe've committed to (we'll pick e.g.ZYNTEM-FI-001000000, 18 chars). Once chosen, treat as immutable. - An
.envtemplate in the Hungary adapter repo with:HU_NAV_LOGIN,HU_NAV_PASSWORD,HU_NAV_TAX_NUMBER,HU_NAV_SIG_KEY,HU_NAV_EXCH_KEY,HU_NAV_SOFTWARE_ID,HU_NAV_BASE_URL.
8. Error model
Every NAV response uses the GeneralErrorResponse envelope when a request
fails schema or authentication:
<GeneralErrorResponse>
<result>
<funcCode>ERROR</funcCode>
<errorCode>INVALID_REQUEST</errorCode>
<message>…human-readable…</message>
</result>
<technicalValidationMessages>…</technicalValidationMessages>
</GeneralErrorResponse>
For async processing (manageInvoice is accepted synchronously, then each
invoice processes asynchronously), the per-invoice outcome arrives via
queryTransactionStatus:
<processingResults>
<processingResult>
<index>1</index>
<batchIndex>1</batchIndex>
<invoiceStatus>DONE</invoiceStatus> <!-- or PROCESSING, SAVED, ABORTED, RECEIVED -->
<technicalValidationMessages>…</technicalValidationMessages>
<businessValidationMessages>
<validationResultCode>WARN</validationResultCode>
<validationErrorCode>DIRECTION_MISMATCH</validationErrorCode>
<message>…</message>
</businessValidationMessages>
<compressedContentIndicator>false</compressedContentIndicator>
<originalRequest>…</originalRequest>
</processingResult>
</processingResults>
Result-code taxonomy
| Level | Codes | What it means for us |
|---|---|---|
| Transport | HTTP 5xx, timeout, DNS | Retry with same requestId. |
General / funcCode | OK, ERROR | ERROR = envelope-level failure (bad XML, bad auth, malformed header). Fix + resubmit with new requestId. |
| General error codes | INVALID_REQUEST, INVALID_SECURITY_USER, INVALID_REQUEST_SIGNATURE, OPERATION_FAILED, INVALID_EXCHANGE_TOKEN, TOKEN_EXPIRED, REQUEST_ID_NOT_UNIQUE | Our bug (except TOKEN_EXPIRED → exchange a fresh token and retry same requestId). |
| InvoiceStatus (per-invoice, async) | RECEIVED → PROCESSING → SAVED → DONE (happy path) or → ABORTED (rejected) | SAVED/DONE = success (SAVED = saved but async-validation pending, DONE = fully validated). ABORTED = NAV rejected that invoice specifically. |
| businessValidationMessage/validationResultCode | ERROR, WARN, INFO | ERROR → the invoice is ABORTED. WARN → invoice still DONE, but NAV is flagging a data-quality issue (we should surface it to the merchant and log). INFO → informational, ignore. |
Key distinction: ABORTED vs WARN vs INVALID_REQUEST
INVALID_REQUEST(general) — request never entered processing. Our bug: bad signature, bad XML, bad auth. Fix + retry.ABORTED(per-invoiceinvoiceStatus) — request accepted, but the invoice failed business validation and was not reported. Client-side bug 95% of the time (bad tax number, duplicate invoice number, bad arithmetic). Not retryable as-is; requires data correction + a new CREATE with a new invoice number (because the rejected one is still "used up").WARN(business) — invoice was reported (invoiceStatus: DONE), but NAV thinks something is off. Bubble up to the merchant; don't retry.
Retry policy matrix
| Error | Retry same requestId? | Retry with new requestId? | Human ticket? |
|---|---|---|---|
| 5xx / timeout | ✅ (dedup by requestId) | ❌ | ❌ |
TOKEN_EXPIRED | ✅ after token exchange | ❌ | ❌ |
INVALID_REQUEST_SIGNATURE | ❌ | ❌ | ✅ config |
INVALID_SECURITY_USER | ❌ | ❌ | ✅ config |
REQUEST_ID_NOT_UNIQUE | ❌ | Reconcile via queryTransactionList; treat original as canonical | ❌ |
Per-invoice ABORTED | ❌ | Only after fixing the data (new invoice number) | ✅ surface to merchant |
Per-invoice WARN + DONE | N/A (success) | ❌ | Notify merchant |
9. Implementation plan
Crate layout
rust/adapters/hungary/
├── Cargo.toml
└── src/
├── lib.rs # HungaryAdapter (impl CountryAdapter)
├── config.rs # HungaryConfig (login, keys, softwareId, sandbox, …)
├── auth.rs # passwordHash, requestSignature, tokenExchange decrypt
├── client.rs # reqwest client, retry, rate-limit, queryTransactionStatus polling
├── invoice_data.rs # Transaction -> InvoiceData XML (quick-xml writer)
├── envelope.rs # InvoiceData -> ManageInvoiceRequest envelope
├── types.rs # enums mapping to InvoiceCategory / InvoiceOperation / CustomerVatStatus
├── errors.rs # AdapterError variants for general + per-invoice failures
└── tests.rs # XSD-schema conformance against golden fixtures
Trait fit
CountryAdapter::prepare() is the two-phase split we want:
- prepare() — build
InvoiceDataXML, base64, gzip (if large), produce a deterministicfiscal_id={supplierTaxNumber}:{invoiceNumber}and record theinvoiceOperation(CREATE by default). Fast, no network. Stores the prepared bytes inPreparedRecord.contentfor latersubmit(). - submit() — call
tokenExchange→ buildManageInvoiceRequestenvelope → POST to/manageInvoice→ parse response toSubmitResultwithtransactionIdas the upstream reference. Does not wait for the async processing result; we add a separate reconciliation job that pollsqueryTransactionStatuson a schedule.
DeploymentMode::Cloud is the primary target. Embedded works too (no local
signing required) but there's little reason to deploy it that way.
SubmissionWindow::Immediate.
Config shape
pub struct HungaryConfig {
/// HU tax number (8-digit root, e.g. "11111111").
pub tax_number: String,
/// Technical user login (15-char).
pub login: String,
/// SHA-512 hex of password (pre-computed; we never store the plaintext).
pub password_hash: String,
/// 19-char signature key.
pub signature_key: String,
/// 16-byte AES-128 exchange key (base64).
pub exchange_key_b64: String,
/// Stable 18-char software identifier.
pub software_id: String,
/// true => api-test.onlineszamla.nav.gov.hu, false => api.onlineszamla.nav.gov.hu.
pub sandbox: bool,
/// Optional: VAT group member tax number (for group taxpayers).
pub group_member_tax_number: Option<String>,
/// Optional: fiscal representative (rarely used).
pub fiscal_representative: Option<FiscalRep>,
}
Dependencies (expected)
quick-xml— XML build + parse (already in workspace via Spain adapters).sha3— SHA3-512 forrequestSignature.sha2— SHA-512 forpasswordHash.aes+cipher— AES-128/ECB for exchangeToken decrypt.base64— already in workspace.flate2— gzip forcompressedContent.reqwest— already in workspace.chrono— already in workspace.
No native signing libs, no XAdES, no SOAP — the interface is plain XML over HTTPS.
Open questions (for implementation bead)
- Currency precision. HUF has no decimals; EUR/USD/etc. have two. Our
Transaction.amountcarriesDecimal— do we silently truncate or reject on mis-scale? Recommend: reject withAdapterError::InvalidCurrencyScaleatprepare(). - B2C customer address. For
customerVatStatus=PRIVATE_PERSON, NAV explicitly allows omitting the name and full address. Our currentTransactionschema doesn't distinguish B2C — do we need a newTransaction.customer_typefield or infer from "no tax number"? - VAT group members. A VAT group (
csoportos adóalanyiság) reports as the group, but must disclose the invoicing member's tax number. Needs per-location config — can fit inLocation.country_config. - Invoice number continuity. Hungarian tax law requires a strict,
unbroken sequence per "billing block" — gaps are auditable. Our
Transaction.sequence_numberis monotonic but not guaranteed gapless. Recommend: the Hungary adapter takes invoice number fromTransaction.document_numberverbatim (source of truth = POS) and does not mint its own. - The
softwareId18-char space. Does it need to be unique per deployment tenant? Strictly no — it identifies our product, not the tenant. But NAV cross-referencessoftwareId + taxNumberin incident triage, so using a differentsoftwareIdper tenant would hurt debuggability. Recommend: onesoftwareIdfor cloud, one for embedded. Store in global config, not per-tenant. - Historical submissions. If a merchant switches to us mid-year, do we need to backfill? Per NAV, no — prior submissions remain valid under the prior software. Document this clearly in onboarding.
What could force us off Cloud-OK?
Nothing identified in the spec. No HSM, no certificate, no local signing.
The only cryptography is hash-and-AES on known-length inputs, which runs
anywhere. The signatureKey and exchangeKey are plain shared secrets we
hold server-side (analogous to an API key). No hardware dependency.
This is a green light — the whole point of picking Hungary for Phase 1 stands.
Testing strategy
- Unit (in-adapter):
requestSignaturevector tests using the NAV sample requests'signatureKeyplaceholders. Golden XSD validation on all 6 fixture triplets usingxmlschema(Python) or an in-workspace Rust validator. - Integration (against sandbox): full CREATE → queryTransactionStatus
round-trip. Gated behind
HU_NAV_SANDBOX_CREDENTIALSenv var so CI without creds still passes unit tests. - Conformance: the
testdata/conformance/hungary/nav-online/golden/*triplets validated as part ofcargo test -p hungary-adapters.
Rollout sequence (for the implementation bead)
- Day 1–2:
invoice_data.rsbuilder +xsd::validateagainstinvoiceData.xsd. Pass all 6 goldens. - Day 3:
auth.rs—requestSignature+ AES exchangeToken decrypt, covered by golden vectors. - Day 4:
client.rs+ envelope — dry POST against sandbox, confirmDONEstatus. - Day 5: Error model + retry,
queryTransactionStatuspolling, reconciliation job. - Day 6–7: Cross-border + credit-note + multi-VAT edge cases.
- Day 8: Soak test against sandbox with 100 invoices/batch, observe rate limits.
10. Citations
All facts above are sourced from one of:
- NAV official spec —
Online_Szamla_interfesz_specifikacio_HU_v3.0.pdf, linked from https://onlineszamla.nav.gov.hu/dokumentaciok. - NAV XSD schemas (v3.0) — https://github.com/nav-gov-hu/Online-Invoice,
src/schemas/nav/gov/hu/OSA/{invoiceApi,invoiceData,invoiceBase,invoiceAnnulment,serviceMetrics}.xsdandhttps://github.com/nav-gov-hu/CommonforNTCA/1.0/common.xsd. - NAV changelogs —
CHANGELOG_2.0.md,CHANGELOG_3.0.mdin the same repo. - NAV developer log — https://onlineszamla.nav.gov.hu/fejlesztoi_naplo.
- NAV sample requests —
sample/API sample/in thenav-gov-hu/Online-Invoicerepo.
Per-fixture citations live in each meta.json under
testdata/conformance/hungary/nav-online/golden/.