nav-connect
Version:
Nav Online Invoice connection module
441 lines (334 loc) • 14.5 kB
Markdown
# NAV Connect
Node.js client library for the Hungarian Tax Authority's (NAV) Online Invoice System API v3.0.
## Installation
```bash
npm install nav-connect nav-osa-types
```
## Requirements
- Node.js >= 20.11.0 (required by `nav-osa-core`)
- NAV Online Invoice System credentials (technical user, exchangeKey, signatureKey)
## Usage
```typescript
import NavConnect from 'nav-connect';
const navClient = NavConnect.create({
testSystem: true,
taxNumber: '12345678',
technicalUser: {
user: 'yourUserName',
password: 'yourPassword',
signatureKey: 'yourSignatureKey',
exchangeKey: 'yourExchangeKey'
},
software: {
softwareId: 'YOUR-SOFTWARE-ID',
softwareName: 'YourSoftwareName',
softwareOperation: 'LOCAL_SOFTWARE',
softwareMainVersion: '1.0.0',
softwareDevName: 'Your Name',
softwareDevContact: 'your.com',
softwareDevCountryCode: 'HU',
softwareDevTaxNumber: '12345678'
},
httpTimeoutMs: 55_000, // optional, default 55_000
// minIntervalMs: 1000 // opt-in rate limiter (1 req/s) — not required by NAV v3
// validateResponse: true // optional, XSD-validate every response (default false)
});
```
## Dependencies
- [`nav-osa-core`](https://github.com/kzolti/nav-osa-core) — XML building, XSD validation, parsing
- [`nav-osa-types`](https://github.com/kzolti/nav-osa-types) — TypeScript type definitions (peer dependency)
Both packages must be installed separately. XSD loading and validation in `nav-osa-core` happens lazily on first use.
## Request validation
Every API call validates the request XML against the built-in XSD schemas **before** any network activity. If validation fails, an error is thrown immediately — no token exchange, no HTTP request.
Response XML is validated against the invoice API schema only when `validateResponse: true` is set (default is `false` for throughput). Violations are reported as `xmlValidationWarnings` on the response (never thrown).
## Rate limiting & timeout
### Per-instance request queue (opt-in)
The rate limiter can be enabled via `minIntervalMs`:
```typescript
const navClient = NavConnect.create({
// ... config
minIntervalMs: 1000, // 1 req/s serialization
});
```
When enabled, all API calls are serialized with the configured interval. Default is `0` — no rate limiting.
### HTTP timeout
The configured `httpTimeoutMs` (default `55_000` ms) is passed to the underlying axios client. If no response arrives within this window, a `NavApiTimeoutError` is thrown.
Per the NAV spec (§1.6.6):
- Typical response time is < 200 ms
- A timeout does **not** mean the submission failed — use `queryTransactionList` for recovery
- Hard NAV-side timeout is 60 seconds; the client default of 55 s is a soft limit below that
### Per-method throttle
`queryInvoiceDigestAll()` has its own `throttleMs` parameter (default `5000` ms) for pagination across large date ranges, independent of the per-instance rate limiter.
## API
All API methods return `NavApiResponse<T>`:
```typescript
import type { NavApiResponse } from 'nav-connect';
interface NavApiResponse<T> {
data: T; // The NAV API response object
xmlValidationWarnings?: string[]; // XSD validation warnings (if any)
}
```
### Instance getters
```typescript
console.log(navClient.taxNumber); // "12345678" — the configured tax number
console.log(navClient.testSystem); // true — whether test system is enabled
console.log(navClient.technicalUser); // "yourUserName" — the configured technical user
```
### queryInvoiceDigest
Query invoice digests for a date range (max 35 days). Throws `NavDateRangeError` if the range exceeds 35 days.
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { QueryInvoiceDigestResponse } from 'nav-osa-types';
const response: NavApiResponse<QueryInvoiceDigestResponse> = await navClient.queryInvoiceDigest({
page: 1,
invoiceDirectionType: "OUTBOUND",
insDate: {
dateTimeFrom: "2025-06-01T00:00:00Z",
dateTimeTo: "2025-06-30T23:59:59Z",
},
});
console.log(response.data.invoiceDigestResult[0].invoiceDigest);
```
### queryInvoiceDigestAll
Query invoice digests for arbitrary date ranges. Automatically splits into 35-day chunks, paginates, and throttles API calls. Throws `NavApiError` if `dateTimeFrom` is later than `dateTimeTo`.
```typescript
import type { InvoiceDigestType } from 'nav-osa-types';
const digests: InvoiceDigestType[] = await navClient.queryInvoiceDigestAll({
invoiceDirectionType: "INBOUND",
insDate: {
dateTimeFrom: "2025-01-01T00:00:00Z",
dateTimeTo: "2025-06-01T00:00:00Z",
},
throttleMs: 7000,
onProgress: (p: DigestAllProgress) => {
console.log(
`Chunk ${p.currentChunk}/${p.totalChunks}, ` +
`page ${p.currentPage}/${p.availablePages}, ` +
`collected: ${p.digestsCollected}`
);
},
});
```
### queryInvoiceData
Query specific invoice data by invoice number.
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { QueryInvoiceDataResponse } from 'nav-osa-types';
const response: NavApiResponse<QueryInvoiceDataResponse> = await navClient.queryInvoiceData({
invoiceNumber: "INV-2025-001",
invoiceDirection: "INBOUND",
supplierTaxNumber: "12345678",
});
console.log(response.data.invoiceDataResult?.invoiceData);
```
### queryInvoiceCheck
Check whether an invoice exists. Returns a boolean (`invoiceCheckResult`).
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { QueryInvoiceCheckResponse } from 'nav-osa-types';
const response: NavApiResponse<QueryInvoiceCheckResponse> = await navClient.queryInvoiceCheck({
invoiceNumber: "INV-2025-001",
invoiceDirection: "OUTBOUND",
supplierTaxNumber: "12345678",
});
console.log(response.data.invoiceCheckResult);
```
### manageInvoice
Submit invoices to NAV. Accepts an array of `InvoiceOperationType` items directly.
The caller is responsible for:
- setting the `index` (1-100, strictly sequential, no gaps)
- encoding the invoice XML as base64
- specifying the `invoiceOperation` (CREATE / MODIFY / STORNO) per item
- optional `electronicInvoiceHash` per item
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { ManageInvoiceResponse } from 'nav-osa-types';
const response: NavApiResponse<ManageInvoiceResponse> = await navClient.manageInvoice({
invoiceOperation: [
{
index: 1,
invoiceOperation: "CREATE",
invoiceData: Buffer.from('<?xml version="1.0"?><InvoiceData>...</InvoiceData>').toString("base64"),
},
{
index: 2,
invoiceOperation: "MODIFY",
invoiceData: Buffer.from('<?xml version="1.0"?><InvoiceData>...</InvoiceData>').toString("base64"),
electronicInvoiceHash: {
"@_cryptoType": "SHA3-512",
"#text": "ABC...",
},
},
],
compressedContent: false,
skipXmlValidation: false,
});
console.log(response.data.transactionId);
```
#### Correlation with queryTransactionStatus
When the caller provides the `index`, the corresponding `queryTransactionStatus` response returns the same index in `processingResults[].index` for correlation:
```typescript
const manageResponse = await navClient.manageInvoice({ ... });
const txId = manageResponse.data.transactionId;
const statusResponse = await navClient.queryTransactionStatus({ transactionId: txId });
for (const result of statusResponse.data.processingResults?.processingResult ?? []) {
console.log(`Index ${result.index}: ${result.invoiceStatus}`);
// result.index matches the index sent in manageInvoice
}
```
#### Required parameters
| Parameter | Type | Description |
|---|---|---|
| `invoiceOperation` | `InvoiceOperationType[]` | Array of invoice operations. Each item must have `index`, `invoiceOperation`, `invoiceData` (base64). |
| `compressedContent` | `boolean` | Whether the invoice data is compressed. **Required** — no default. |
| `skipXmlValidation` | `boolean` | If `false`, each item's XML is decoded and validated against the XSD schema. If `true`, validation is skipped. **Required** — no default. |
| `exchangeToken` | `string` | Optional. If omitted, a token is obtained automatically via `tokenExchange()`. |
#### Validations
- `index` must be between 1-100, strictly increasing without gaps
- `compressedContent: true` + `skipXmlValidation: false` → error: compressed XML cannot be validated; decompress before submission or set `skipXmlValidation: true`
- `skipXmlValidation: false` → each item's `invoiceData` is base64-decoded and validated against `data.xsd`
### manageAnnulment
Submit technical annulments (storno) for invoices.
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { ManageAnnulmentResponse } from 'nav-osa-types';
const response: NavApiResponse<ManageAnnulmentResponse> = await navClient.manageAnnulment({
annulmentOperations: [
{
index: 1,
annulmentOperation: "ANNUL",
invoiceAnnulment: base64EncodedAnnulmentXml,
},
],
});
console.log(response.data.transactionId);
```
### tokenExchange
Request an exchange token for invoice submission. Called automatically by `manageInvoice()` and `manageAnnulment()` if not provided. Returns the decoded token together with its validity window.
```typescript
const { token, tokenValidityFrom, tokenValidityTo } = await navClient.tokenExchange();
import type { NavApiResponse } from 'nav-connect';
import type { ManageInvoiceResponse } from 'nav-osa-types';
const response: NavApiResponse<ManageInvoiceResponse> = await navClient.manageInvoice({
invoiceOperation: [
{
index: 1,
invoiceOperation: "CREATE",
invoiceData: Buffer.from(xml).toString("base64"),
},
],
compressedContent: false,
skipXmlValidation: false,
exchangeToken: token,
});
```
### queryTransactionStatus
Query the processing status of a submitted transaction.
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { QueryTransactionStatusResponse } from 'nav-osa-types';
const response: NavApiResponse<QueryTransactionStatusResponse> = await navClient.queryTransactionStatus({
transactionId: "ABC123...",
returnOriginalRequest: false,
});
console.log(response.data.processingResults?.processingResult);
```
### queryTransactionList
List all submitted transactions within a date range.
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { QueryTransactionListResponse } from 'nav-osa-types';
const response: NavApiResponse<QueryTransactionListResponse> = await navClient.queryTransactionList({
page: 1,
insDate: {
dateTimeFrom: "2025-01-01T00:00:00Z",
dateTimeTo: "2025-01-31T23:59:59Z",
},
requestStatus: "FINISHED",
});
console.log(response.data.transactionListResult.transaction);
```
### queryTaxpayer
Query taxpayer information by tax number.
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { QueryTaxpayerResponse } from 'nav-osa-types';
const response: NavApiResponse<QueryTaxpayerResponse> = await navClient.queryTaxpayer({
taxNumber: "12345678",
});
console.log(response.data.taxpayerData?.taxpayerName);
```
### queryInvoiceChainDigest
Query the invoice chain (base invoice, modifications, stornos).
```typescript
import type { NavApiResponse } from 'nav-connect';
import type { QueryInvoiceChainDigestResponse } from 'nav-osa-types';
const response: NavApiResponse<QueryInvoiceChainDigestResponse> = await navClient.queryInvoiceChainDigest({
page: 1,
invoiceChainQuery: {
invoiceNumber: "INV-2025-001",
invoiceDirection: "OUTBOUND",
taxNumber: "12345678",
},
});
console.log(response.data.invoiceChainDigestResult.invoiceChainElement);
```
## Error Handling
All errors extend `NavApiError`. Specific error classes:
- `NavConfigError` — invalid configuration
- `NavDateRangeError` — date range exceeds 35 days
- `NavXmlValidationError` — request XML fails XSD validation (before the HTTP call)
- `NavResponseXmlValidationError` — NAV API response XML fails XSD validation (soft warning path)
- `NavApiResponseError` — NAV API returned an error (structured funcCode/errorCode/message)
- `NavApiHttpError` — HTTP error with unparseable body
- `NavApiTimeoutError` — HTTP request exceeded `httpTimeoutMs` (configurable, default 55 s)
Request validation errors are thrown as `NavXmlValidationError` **before** any network calls.
```typescript
import { NavApiError, NavXmlValidationError, NavResponseXmlValidationError, NavApiResponseError, NavApiHttpError, NavApiTimeoutError } from 'nav-connect';
try {
const response = await navClient.manageInvoice({
invoiceOperation: [
{
index: 1,
invoiceOperation: "CREATE",
invoiceData: Buffer.from(xml).toString("base64"),
},
],
compressedContent: false,
skipXmlValidation: false,
});
console.log("transactionId:", response.data.transactionId);
} catch (error) {
if (error instanceof NavXmlValidationError) {
// 1. Invoice XML validation failed — no network call made
console.error("Invoice XML errors (index", error.requestType, "):", error.validationErrors);
} else if (error instanceof NavResponseXmlValidationError) {
// 2. NAV response XML validation error
console.error("Response XML error:", error.validationErrors);
} else if (error instanceof NavApiResponseError) {
// 3. NAV API rejected the request (e.g. invalid signature, expired token)
console.error("NAV error:", error.funcCode, error.errorCode, error.message);
} else if (error instanceof NavApiTimeoutError) {
// 4. Request timeout (httpTimeoutMs)
console.error("Timeout:", error.timeoutMs, "ms");
} else if (error instanceof NavApiHttpError) {
// 5. HTTP error (e.g. 401, 500)
console.error("HTTP error:", error.httpStatus, error.statusText);
} else if (error instanceof NavApiError) {
// 6. Other NAV errors (e.g. validation, config)
console.error("NAV error:", error.message);
} else {
// 7. Other error (e.g. network error)
console.error("Unknown error:", error);
}
}
```
## Support
If you find this package useful, consider supporting the development:
- [Buy me a coffee (GitHub Sponsors)](https://github.com/sponsors/kzolti)
- [Buy me a coffee (Revolut)](https://revolut.me/zoltnifdgo?note=nav-connect)
## License
Apache License 2.0 - see [LICENSE](LICENSE).
## Author
Zoltan Istvan KADA (kAdatSoft)
https://github.com/kzolti/nav-connect