Skip to main content

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 dateScope change
2018-07-01B2B invoices with VAT ≥ 100,000 HUF (domestic only) — initial launch, v1.0
2020-07-01All domestic B2B invoices regardless of VAT amount (threshold removed), v2.0
2021-01-04Added: cross-border invoices (EU + non-EU supplies where seller is HU VAT subject). Grace period. v3.0
2021-07-01Added: 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-01eVAT (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 SIMPLIFIED in 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 customerTaxNumber when the input provides it; otherwise the customerVatStatus field (DOMESTIC / OTHER / PRIVATE_PERSON) controls what customer identification is required.

2. API surface

Base URLs

EnvironmentAPI baseWeb portal
Sandbox (test)https://api-test.onlineszamla.nav.gov.hu/https://onlineszamla-test.nav.gov.hu/
Productionhttps://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/.

PathPurposeAuth
POST /invoiceService/v3/tokenExchangeExchange login + signatureKey for a 5-minute one-shot exchange tokenbasic (login + passwordHash + requestSignature)
POST /invoiceService/v3/manageInvoiceSubmit CREATE / MODIFY / STORNO — up to 100 invoices per callbasic + exchangeToken
POST /invoiceService/v3/manageAnnulmentTechnical annulment (cancel a submitted transaction that was erroneous at the API level — not a STORNO)basic + exchangeToken
POST /invoiceService/v3/queryTransactionStatusPoll processing status + per-index results for a submissionbasic
POST /invoiceService/v3/queryTransactionListPaginated list of prior submissionsbasic
POST /invoiceService/v3/queryInvoiceDataRetrieve a submitted invoice by invoice number / directionbasic
POST /invoiceService/v3/queryInvoiceCheckYes/no existence check for an invoice numberbasic
POST /invoiceService/v3/queryInvoiceDigestPaginated search over one's submitted invoices (date, partner, etc.)basic
POST /invoiceService/v3/queryInvoiceChainDigestOrdered chain of CREATE + MODIFY + STORNO for a single invoice numberbasic
POST /invoiceService/v3/queryTaxpayerPublic lookup: is a HU tax number valid + active + what is its name/addressbasic

Adapter hot path: tokenExchangemanageInvoice → (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:

FieldPurposeFormat
loginTechnical user ID15-char alphanumeric
passwordTechnical user passwordUser-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
signatureKeyHMAC-ish key used in requestSignatureXX-XXXXXXXX-XXXXXXXXXXXXXXXXXXX — 19 chars with 2 dashes
exchangeKeyAES-128 key used to decrypt the exchange token from tokenExchange responses16 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:

  • requestId is the 30-char alnum ID the client sent in common:header/requestId (must be globally unique for that taxpayer within a 12-month window).
  • timestamp_compact is the common:timestamp value, reformatted to YYYYMMDDhhmmss with no separators (drop the T, -, :, ., Z, and the fractional seconds).
  • signatureKey is 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

  1. Client POSTs a TokenExchangeRequest (no token yet, just basic user fields
    • software block).
  2. NAV returns <encodedExchangeToken> (base-64 of AES-128/ECB-encrypted raw token bytes) and <tokenValidityFrom> / <tokenValidityTo> (valid 5 minutes).
  3. Client decrypts with exchangeKey (AES-128/ECB, PKCS5 padding) → plaintext token string (ASCII).
  4. Client includes that plaintext token as <exchangeToken> in the next manageInvoiceRequest.

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:

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

ValueMeaning
CREATEOriginal invoice report.
MODIFYModification (= Hungarian helyesbítő / módosító) document. Carries originalInvoiceNumber reference and modifyWithoutMaster=true/false. Can also be a supplementing invoice (pót-számla).
STORNOCancelling 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 manageInvoice call returns a transactionId (20-char alnum).
  • Up to 100 invoices per call; NAV processes them in-order by index but each succeeds/fails independently.
  • The transactionId + (taxNumber + requestId) together give idempotency.

Idempotency via requestId

  • requestId must be unique per taxpayer for 12 months.
  • If the client sends a duplicate requestId, NAV responds with a general error (INVALID_REQUEST, validationErrorCode REQUEST_ID_NOT_UNIQUE) — and the prior submission remains canonical.
  • Adapter rule: we mint requestId deterministically 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 receiving REQUEST_ID_NOT_UNIQUE, we call queryTransactionList to find the prior transactionId and treat the submission as successful.

Per-invoice idempotency

  • supplierTaxNumber + invoiceNumber is a uniqueness key within a year.
  • Submitting a CREATE with an already-reported invoiceNumber returns business-validation ERROR INVOICE_NUMBER_NOT_UNIQUE and the invoice is rejected (status ABORTED per-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 queryTransactionList to reconcile.
  • queryTransactionStatus is 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:

IssuerWindow
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

  1. Go to https://onlineszamla-test.nav.gov.hu/regisztracio/start ("Regisztráció").
  2. Choose "Tesztpéldány - kizárólag fejlesztői célra" (test instance for development use only).
  3. 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.
  4. Log in to the test portal, go to Felhasználók → Új technikai felhasználó.
  5. NAV generates the four credentials: login, initial password, signatureKey, exchangeKey. The signatureKey and exchangeKey are displayed once — capture immediately.
  6. 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 + exchangeKey checked into our password vault for the CI sandbox tenant.
  • A softwareId we've committed to (we'll pick e.g. ZYNTEM-FI-001000000, 18 chars). Once chosen, treat as immutable.
  • An .env template 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

LevelCodesWhat it means for us
TransportHTTP 5xx, timeout, DNSRetry with same requestId.
General / funcCodeOK, ERRORERROR = envelope-level failure (bad XML, bad auth, malformed header). Fix + resubmit with new requestId.
General error codesINVALID_REQUEST, INVALID_SECURITY_USER, INVALID_REQUEST_SIGNATURE, OPERATION_FAILED, INVALID_EXCHANGE_TOKEN, TOKEN_EXPIRED, REQUEST_ID_NOT_UNIQUEOur bug (except TOKEN_EXPIRED → exchange a fresh token and retry same requestId).
InvoiceStatus (per-invoice, async)RECEIVEDPROCESSINGSAVEDDONE (happy path) or → ABORTED (rejected)SAVED/DONE = success (SAVED = saved but async-validation pending, DONE = fully validated). ABORTED = NAV rejected that invoice specifically.
businessValidationMessage/validationResultCodeERROR, WARN, INFOERROR → 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-invoice invoiceStatus) — 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

ErrorRetry 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_UNIQUEReconcile via queryTransactionList; treat original as canonical
Per-invoice ABORTEDOnly after fixing the data (new invoice number)✅ surface to merchant
Per-invoice WARN + DONEN/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 InvoiceData XML, base64, gzip (if large), produce a deterministic fiscal_id = {supplierTaxNumber}:{invoiceNumber} and record the invoiceOperation (CREATE by default). Fast, no network. Stores the prepared bytes in PreparedRecord.content for later submit().
  • submit() — call tokenExchange → build ManageInvoiceRequest envelope → POST to /manageInvoice → parse response to SubmitResult with transactionId as the upstream reference. Does not wait for the async processing result; we add a separate reconciliation job that polls queryTransactionStatus on 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 for requestSignature.
  • sha2 — SHA-512 for passwordHash.
  • aes + cipher — AES-128/ECB for exchangeToken decrypt.
  • base64 — already in workspace.
  • flate2 — gzip for compressedContent.
  • 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)

  1. Currency precision. HUF has no decimals; EUR/USD/etc. have two. Our Transaction.amount carries Decimal — do we silently truncate or reject on mis-scale? Recommend: reject with AdapterError::InvalidCurrencyScale at prepare().
  2. B2C customer address. For customerVatStatus=PRIVATE_PERSON, NAV explicitly allows omitting the name and full address. Our current Transaction schema doesn't distinguish B2C — do we need a new Transaction.customer_type field or infer from "no tax number"?
  3. 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 in Location.country_config.
  4. Invoice number continuity. Hungarian tax law requires a strict, unbroken sequence per "billing block" — gaps are auditable. Our Transaction.sequence_number is monotonic but not guaranteed gapless. Recommend: the Hungary adapter takes invoice number from Transaction.document_number verbatim (source of truth = POS) and does not mint its own.
  5. The softwareId 18-char space. Does it need to be unique per deployment tenant? Strictly no — it identifies our product, not the tenant. But NAV cross-references softwareId + taxNumber in incident triage, so using a different softwareId per tenant would hurt debuggability. Recommend: one softwareId for cloud, one for embedded. Store in global config, not per-tenant.
  6. 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): requestSignature vector tests using the NAV sample requests' signatureKey placeholders. Golden XSD validation on all 6 fixture triplets using xmlschema (Python) or an in-workspace Rust validator.
  • Integration (against sandbox): full CREATE → queryTransactionStatus round-trip. Gated behind HU_NAV_SANDBOX_CREDENTIALS env var so CI without creds still passes unit tests.
  • Conformance: the testdata/conformance/hungary/nav-online/golden/* triplets validated as part of cargo test -p hungary-adapters.

Rollout sequence (for the implementation bead)

  1. Day 1–2: invoice_data.rs builder + xsd::validate against invoiceData.xsd. Pass all 6 goldens.
  2. Day 3: auth.rsrequestSignature + AES exchangeToken decrypt, covered by golden vectors.
  3. Day 4: client.rs + envelope — dry POST against sandbox, confirm DONE status.
  4. Day 5: Error model + retry, queryTransactionStatus polling, reconciliation job.
  5. Day 6–7: Cross-border + credit-note + multi-VAT edge cases.
  6. Day 8: Soak test against sandbox with 100 invoices/batch, observe rate limits.

10. Citations

All facts above are sourced from one of:

Per-fixture citations live in each meta.json under testdata/conformance/hungary/nav-online/golden/.