xingen-sdk
v0.2.0
Published
TypeScript client SDK for the Xingen e-invoice validation API
Maintainers
Readme
xingen-sdk
TypeScript client SDK for the Xingen e-invoice validation API — submit UBL, CII, ZUGFeRD, and SAP IDoc/OData invoices for validation against EN16931, XRechnung, and Peppol.
Requires Node 22+. Built on native fetch/FormData — the only runtime dependency is
lossless-json, used internally to preserve exact
monetary/quantity precision (see Design notes).
Status: v1, covering invoice submission/validation and API key management. Contacts and dashboard/user endpoints are not exposed (they're Firebase-auth-only on the backend).
Install
npm install xingen-sdkShips both ESM and CommonJS builds, so both import and require() work.
Authentication
Every request needs an API key (xgn_live_... for production, xgn_test_... for sandbox — sandbox
requests never count toward quota). Create one from the Xingen dashboard or via client.apiKeys.
import { XingenClient } from "xingen-sdk";
const client = new XingenClient({ apiKey: process.env.XINGEN_API_KEY! });baseUrl overrides the default https://app.xingen.de/api, useful for self-hosted or local
(./gradlew bootRun, port 10001) testing. timeoutMs (default 30000) covers the whole request.
Validate a file
Every validate/submit endpoint is asynchronous — the backend queues the invoice and returns
immediately. Use a *AndWait helper to submit and poll for the result in one call:
import { InvoiceStatus, ValidationProfile } from "xingen-sdk";
const result = await client.invoices.validateFileAndWait("invoice.xml", ValidationProfile.XRECHNUNG);
if (result.status === InvoiceStatus.VALIDATED && result.validationResult?.valid) {
console.log("Valid!");
} else {
for (const error of result.validationResult?.errors ?? []) {
console.log(`${error.severity}: ${error.message} (${error.field})`);
}
}PollOptions controls the backoff (initialIntervalMs, maxIntervalMs, backoffMultiplier), the
overall timeoutMs, and an optional cancellationCheck. A failed validation is not an error —
it's a completed API call that found the invoice invalid, so *AndWait resolves normally with
validationResult.valid === false. Only a transport failure, cancellation, or timeout rejects.
const result = await client.invoices.validateFileAndWait(
"invoice.xml",
ValidationProfile.XRECHNUNG,
{ initialIntervalMs: 300, maxIntervalMs: 3000, timeoutMs: 30000 },
);If you'd rather manage polling yourself, use the low-level pair:
const submitted = await client.invoices.validateFile("invoice.xml", ValidationProfile.EN16931);
// ... later ...
const record = await client.invoices.get(submitted.id);validateIdoc/validateIdocAndWait work the same way for SAP IDoc XML files. Both, and
validateFile/validateFileAndWait, also accept { filename, content } (a Uint8Array) if you
already hold the file bytes in memory instead of a path — the bare-path form only works in Node
(read via node:fs/promises), so browser/edge callers should always use the in-memory form.
Submit a structured invoice (JSON)
Monetary and quantity fields (price, quantity, taxRate, etc.) are typed as string, not
number — see Design notes for why.
const result = await client.invoices.submitAndWait({
invoiceNumber: "INV-2024-0042",
issueDate: "2024-03-15",
currency: "EUR",
buyerReference: "991-12345-06",
validationProfile: ValidationProfile.XRECHNUNG,
supplier: {
name: "Acme GmbH",
vatId: "DE123456789",
address: { city: "Berlin", countryCode: "DE" },
},
buyer: {
name: "Buyer Co",
leitwegId: "991-12345-06",
address: { countryCode: "DE" },
},
lines: [
{ description: "Software License Q1", quantity: "5", unit: "C62", price: "199.00", taxRate: "19" },
],
paymentMeans: [{ typeCode: "58", creditTransferAccountId: "DE89370400440532013000" }],
});InvoiceSubmission has full parity with the backend's domain model — every invoice type it can
validate, it can also submit, including non-standard VAT categories (exempt/reverse-charge/export
via LineInput.taxCategoryCode + exemptionReason/exemptionReasonCode), payee/
taxRepresentative, delivery, invoicePeriod, precedingInvoiceReferences (for credit notes),
and the full BT-11..BT-19 document reference fields. See the types exported alongside
InvoiceSubmission for the complete set.
SAP S/4HANA OData supplier-invoice payloads are supported as a thin passthrough — pass raw JSON or a plain object rather than a fully typed model:
await client.invoices.submitOData(rawODataJson, ValidationProfile.EN16931);Extract an invoice from a PDF (AI)
Upload a plain invoice PDF — including scanned/image-based PDFs — and let the backend extract
structured fields with Claude. Works exactly like the other submit endpoints: async, so use
extractInvoiceAndWait or the low-level extractInvoice/get pair.
import { ExtractionModelTier, ValidationProfile } from "xingen-sdk";
const result = await client.invoices.extractInvoiceAndWait(
"scanned-invoice.pdf",
ValidationProfile.EN16931,
ExtractionModelTier.FAST, // or ACCURATE -- higher accuracy, Pro subscription required
);If the extraction missed a field or validation flagged something, correct it with a JSON
merge-patch (RFC 7386) and re-validate synchronously — only invoices that finished processing
(VALIDATED or FAILED_VALIDATION) can be corrected. Array fields (lines, paymentMeans,
allowanceCharges, taxBreakdowns) are replaced wholesale when present in the patch:
const corrected = await client.invoices.patchInvoice(result.id, {
currency: "EUR",
buyerReference: "991-12345-06",
});To find out which fields the backend fills in automatically per profile (so you know what not to prompt the user for):
const autoFilled = await client.invoices.getAutoFilledFields();List and retrieve invoices
const page = await client.invoices.list(0, 20, "createdAt,desc");
// or, to walk every invoice without managing page indices yourself:
for await (const record of client.invoices.listAll(50)) {
console.log(record.id, "->", record.status);
}
const one = await client.invoices.get("inv_01HXYZ");Download results
const pdf = await client.invoices.downloadPdf(id); // ZUGFeRD PDF with embedded XML, Uint8Array
const idocXml = await client.invoices.downloadIdocXml(id); // SAP IDoc XML, Uint8ArrayAPI keys
const created = await client.apiKeys.create({ name: "Production CI", sandbox: false });
console.log("Store this now, it's shown only once:", created.rawKey);
const keys = await client.apiKeys.list();
await client.apiKeys.revoke(created.id);Error handling
All SDK errors extend XingenError. HTTP errors map to typed subclasses of ApiError:
| Error | Status | Notes |
|---|---|---|
| AuthenticationError | 401 | Missing or invalid API key |
| PermissionError | 403 | Resource exists but isn't owned by the caller |
| NotFoundError | 404 | |
| ValidationRequestError | 400 | .fieldErrors has details for request-body validation failures |
| QuotaExceededError | 429 | Monthly request quota exhausted |
| ApiError | other 4xx/5xx | Fallback; .statusCode / .rawBody always available |
import { QuotaExceededError, ValidationRequestError, XingenError } from "xingen-sdk";
try {
await client.invoices.submit(submission);
} catch (error) {
if (error instanceof ValidationRequestError) {
for (const [field, message] of Object.entries(error.fieldErrors)) {
console.log(`${field}: ${message}`);
}
} else if (error instanceof QuotaExceededError) {
console.log("Quota exceeded — upgrade or wait for the next billing period");
} else if (error instanceof XingenError) {
console.log(`Request failed: ${error.message}`);
}
}Design notes
- No automatic retries. Retrying a
submit()after a client-side timeout is unsafe without idempotency keys, which the API doesn't support yet — a retried submit could create a duplicate invoice. Handle retries at the call site if you need them. - Monetary and quantity fields are
string, nevernumber. JavaScript numbers are IEEE-754 doubles with no arbitrary-precision decimal type. Worse, nativeJSON.parse's reviver runs after numeric literals are already converted tonumber— it cannot recover lost precision. This SDK parses every response throughlossless-jsoninstead, which intercepts numeric tokens before conversion, so every number becomes its exact source-text string; each model then narrows only the handful of known-safe small integer counters (page numbers, quota counts, etc.) back tonumber. Monetary/quantity fields are left untouched — parse them with your own decimal library if you need arithmetic. - Dates are
string(ISO 8601), notDate.Dateis mutable, has no timezone-safe date-only representation, andJSON.parsenever reconstructsDateobjects automatically anyway — keeping dates as strings avoids a whole class of timezone-shift bugs for date-only fields likeissueDate.
Contributing
npm install
npm run lint
npm run typecheck
npm test
npm run buildTests run against a real (loopback) node:http server, not a mocking library — no network calls
leave the machine, and no external test-server dependency is required.
License
MIT — see LICENSE.
