@cloudcreators-gmbh/lexware-ts-sdk
v0.1.3
Published
TypeScript SDK for the Lexware Office Public API
Readme
Lexware TypeScript SDK
Note: this is not an official SDK by Haufe/Lexware, it was developed by Cloud Creators GmbH from Freiburg.
A strongly-typed, ergonomic SDK for the Lexware Office Public API. It wraps common REST patterns (paging, filtering, file download/upload, webhooks) and exposes composable helpers for querying invoices, contacts, voucherlist, vouchers, and more.
Base URL:
https://api.lexware.io(May 26, 2025 rebrand; former gateway continues only until Dec 2025)
Auth: API Key (Bearer) created in your Lexware account under Add-ons → Public API.
Note: parts of this code including the Readme is AI-generated, so make sure to test your results and open bug reports or PRs.
✨ Features
- First-class support for voucherlist, invoices, contacts, vouchers, files, event-subscriptions (webhooks).
- Typed filters and query builders (date ranges, status, voucher types, name search, etc.).
- Pagination helpers with sensible defaults and auto iteration.
- File APIs: download invoice PDFs / e-invoice XML via
/v1/invoices/{id}/file; upload voucher files via/v1/files. - Rate limiting guard (2 req/s) with exponential backoff on
429(configurable). - Optimistic locking helpers for PUTs with
version. - Node & TypeScript first. Fully typed responses.
📦 Installation
npm install @cloudcreators-gmbh/lexware-ts-sdk
# or
yarn add @cloudcreators-gmbh/lexware-ts-sdk
pnpm add @cloudcreators-gmbh/lexware-ts-sdk⚙️ Setup
import { LexwareClient } from "@cloudcreators-gmbh/lexware-ts-sdk";
const client = new LexwareClient({ apiKey: process.env.LEXWARE_API_KEY! });Tip: Store the API key in your secrets manager. The SDK only sends it via the
Authorization: Bearer <token>header toapi.lexware.io.
🧭 Quick starts & Recipes
▶️ Running the examples
Set your API key and optional filters via environment variables (all examples use LEXWARE_-prefixed vars):
# Common
export LEXWARE_API_KEY=your_token
export LEXWARE_PAGE=0
export LEXWARE_SIZE=25
# Contacts filters
export LEXWARE_NAME="Muster & Partner" # optional
export LEXWARE_EMAIL="[email protected]" # optional
export LEXWARE_CUSTOMER=true # optional (true/false)
export LEXWARE_VENDOR=false # optional (true/false)
# Invoices filters
export LEXWARE_STATUS="any" # e.g. "open,overdue" or "any"
export LEXWARE_CONTACT_ID="..." # optional
export LEXWARE_NUMBER="RE-2025-0001" # optional
export LEXWARE_SORT="updatedDate,DESC" # optional
# Voucherlist filters
export LEXWARE_VOUCHER_TYPE="invoice,purchaseinvoice" # optional
export LEXWARE_VOUCHER_STATUS="any" # optional
export LEXWARE_FROM="2025-01-01" # optional (yyyy-MM-dd)
export LEXWARE_TO="2025-01-31" # optional (yyyy-MM-dd)
export LEXWARE_SORT="updatedDate,DESC" # optional
# Run
npm run example-contacts
npm run example-invoices
npm run example-voucherlist
# Download a PDF for a specific invoice
export LEXWARE_INVOICE_ID="<invoice-id>"
npm run example-download-invoice-pdfNotes:
- All examples require
LEXWARE_API_KEYto be set. - Booleans accept:
true/false,1/0,yes/no,y/n,on/off. - Status lists accept comma-separated values or
any.
1) List invoices and download the PDF (or XRechnung XML)
// Page through latest invoices (draft/open), newest first:
const page1 = await client.invoices.list({ page: 0 }); // default size 25
for (const inv of page1.content) {
console.log(inv.id, inv.voucherStatus, inv.voucherNumber);
}
// Download file ("pdf" or "xml" for XRechnung when available):
const bytes = await client.invoices.downloadFile(invId, 'pdf');
await fs.promises.writeFile(`invoice-${invId}.pdf`, Buffer.from(bytes));Notes:
- Use
client.invoices.downloadFile(id, {accept})which hits/v1/invoices/{id}/fileand returns raw bytes. - Invoices in draft have no file; server returns
409in that case—SDK raises a typed error.
2) Find contacts (with paging and name/email filters)
// Fetch first page
const contacts = await client.contacts.list({ page: 0, size: 50 });
// Filter by name and/or email
const result = await client.contacts.list({
name: "johnson & partner",
email: "[email protected]",
page: 0,
size: 25,
});
console.log(result.totalElements, result.content[0]?.person ?? result.content[0]?.company);Some endpoints (incl. contacts, voucherlist, vouchers) require the search string to be HTML-encoded and URL-encoded for reserved characters like
&,<,>. The SDK does this for you automatically.
3) Use the voucherlist for fast cross-entity searches
// List all purchase invoices and sales invoices that are open in March 2025:
const voucherPage = await client.voucherlist.list({
voucherType: ["purchaseinvoice", "invoice"],
voucherStatus: ["open"],
voucherDateFrom: "2025-03-01",
voucherDateTo: "2025-03-31",
size: 100, // up to 250
page: 0,
});
// Pick one and follow relations
for (const v of voucherPage.content) {
const contact = v.contactId ? await client.contacts.get(v.contactId) : null;
console.log(v.id, v.voucherType, v.voucherStatus, contact?.person?.firstName ?? contact?.company?.name);
}Prefer voucherlist for filtering across sales vouchers (invoices, credit notes, quotations, order confirmations, delivery notes) and bookkeeping vouchers. The older
GET /v1/vouchers?voucherNumber=…filter is deprecated.
4) Get a voucher by id (bookkeeping, e.g., purchaseinvoice) and follow to related contact
const voucher = await client.vouchers.get(voucherId); // /v1/vouchers/{id}
const contact = voucher.contactId ? await client.contacts.get(voucher.contactId) : null;5) Upload a voucher file (PDF/JPG/PNG/XML) to create bookkeeping vouchers
// Creates/returns a bookkeeping voucher and file id (async OCR → status 'unchecked' later)
const data = await fs.promises.readFile("/path/to/receipt.pdf");
const { id: fileId, voucherId } = await client.files.upload(data, "receipt.pdf", "voucher");- Uses
POST /v1/fileswithmultipart/form-dataandtype=voucher. - Max file size 5 MB for vouchers. If file already exists (checksum), server returns existing ids.
- After upload, voucher is available via
/v1/vouchers/{id}. Initial status may beblankuntil OCR completes, thenunchecked.
🔎 Pagination
All paged endpoints return a Spring-like page envelope:
type Page<T> = {
content: T[];
first: boolean;
last: boolean;
totalPages: number;
totalElements: number;
numberOfElements: number;
size: number; // requested size
number: number; // page index (0-based)
sort?: Array<{ direction: "ASC"|"DESC"; property: string }>;
};- Defaults:
size=25,page=0. Maximumsize=250for contacts, voucherlist, vouchers, etc. - The SDK exposes
forEachPage(...)helpers to iterate safely and respects the 10,000-entry search window. Narrow your date ranges if you hitMaximum search window size exceeded.
⏱️ Rate limits
- The public API allows 2 requests/second (token-bucket).
- SDK retries on
429with exponential backoff and jitter. You can configurerateLimitor plug your own limiter.
🔐 Optimistic locking
PUTupdates require a matchingversion(a revision number).- The SDK flow:
get → apply changes → putand throws a specificVersionConflictError (409)when versions mismatch. On firstPOST, setversion: 0(handled by the SDK).
🧱 Errors
The SDK throws typed errors with status, code, i18nKey (if present) and raw response snapshot. Handle known cases:
401/403: missing/invalid token.404: resource not found.406: unacceptable media type or validation issue (e.g., trying to render a PDF for a draft invoice).409: version conflict or draft invoice file download attempt.429: rate-limited (SDK retries).
try {
await client.invoices.downloadFile(id);
} catch (e) {
if (e.name === "HttpError" && e.status === 409) {
// invoice is draft → finalize first or poll until open
}
}🧪 End-to-end examples
List invoices updated this week and download their files
import { endOfToday, subDays, formatISO } from "date-fns";
const updatedFrom = formatISO(subDays(endOfToday(), 7), { representation: "date" });
for await (const v of client.voucherlist.iterateAll({
voucherType: ["invoice"],
updatedDateFrom: updatedFrom,
size: 250,
})) {
try {
const bin = await client.invoices.downloadFile(v.id, 'pdf');
await fs.promises.writeFile(`out/${v.voucherNumber ?? v.id}.pdf`, Buffer.from(bin));
} catch (e) {
console.warn(`Skip ${v.id}: ${e}`);
}
}Search contacts by name and map to invoices
const c = await client.contacts.search({ name: "Muster & Partner", size: 50 });
for (const contact of c.content) {
const related = await client.voucherlist.list({
contactId: contact.id,
voucherType: ["invoice"],
size: 50,
});
console.log(contact.id, related.numberOfElements);
}🔁 Filtering cheat-sheet
- voucherlist:
voucherType[],voucherStatus[],voucherDateFrom/To,createdDateFrom/To,updatedDateFrom/To,dueDateFrom/To,voucherNumber,contactId,archived,page,size. - contacts:
name,email, role filters (customer/vendor), paging. - vouchers (bookkeeping):
GET /v1/vouchers/{id}; filtering byvoucherNumberis deprecated—usevoucherlistinstead. - invoices:
GET /v1/invoices/{id}, file via/v1/invoices/{id}/file(PDF or XML for XRechnung).
The SDK auto-encodes search strings for endpoints that require HTML+URL encoding for
&,<,>.
🗂 File handling
- Download invoice file:
GET /v1/invoices/{id}/file(useacceptto pickapplication/pdforapplication/xmlfor XRechnung). - Download bookkeeping voucher file:
GET /v1/files/{id}. - Upload voucher file:
POST /v1/files(multipart/form-datawithtype=voucher; max 5 MB).
🧰 SDK surface (high-level)
class LexwareClient {
contacts: {
list(q?: { page?: number; size?: number; name?: string; email?: string; customer?: boolean; vendor?: boolean }): Promise<Page<Contact>>;
iterateAll(q?: Omit<{ page?: number; size?: number; name?: string; email?: string; customer?: boolean; vendor?: boolean }, 'page'>): AsyncGenerator<Contact>;
get(id: string): Promise<Contact>;
create(patch: Partial<Contact>): Promise<{ id: string; resourceUri: string; createdDate: string; updatedDate: string; version: number }>;
update(id: string, patch: Partial<Contact>): Promise<void>;
};
voucherlist: {
list(q: VoucherlistListParams): Promise<Page<VoucherListItem>>;
iterateAll(q: Omit<VoucherlistListParams, 'page'|'size'> & { size?: number }): AsyncGenerator<VoucherListItem>;
};
invoices: {
get(id: string): Promise<Invoice>;
list(q: { status: VoucherStatus|VoucherStatus[]|'any'; from?: string; to?: string; page?: number; size?: number; sort?: string; hydrate?: boolean; contactId?: string; number?: string }): Promise<Page<Invoice | VoucherListItem>>;
downloadFile(id: string, format?: 'pdf'|'xml'|'auto'): Promise<Uint8Array>;
};
vouchers: {
create(body: Omit<Voucher, 'id'|'organizationId'|'version'> & Partial<Pick<Voucher,'version'>>): Promise<{ id: string; resourceUri: string; createdDate: string; updatedDate: string; version: number }>;
get(id: string): Promise<Voucher>;
update(id: string, body: Partial<Voucher>): Promise<void>;
uploadFile(id: string, file: Blob | Uint8Array | ArrayBuffer | Buffer, filename: string): Promise<{ id: string }>;
};
files: {
upload(file: Blob | Uint8Array | ArrayBuffer | Buffer, filename: string, type?: 'voucher'): Promise<{ id: string; voucherId?: string }>;
download(id: string): Promise<Uint8Array>;
};
getRelatedContact(input: VoucherListItem | Voucher | Invoice | string, typeHint?: 'voucher'|'invoice'|'voucherlist'): Promise<Contact | null>;
}✅ Best practices
- Prefer voucherlist for cross-entity searches & filtering.
- Handle rate limits and 429 with retries (SDK does this by default).
- Respect optimistic locking (
versionon PUT/POST). - Use the new
/v1/invoices/{id}/fileinstead of legacy render+files flow. - Narrow filters if you hit the 10,000-entry window limit.
- When searching in contacts/voucherlist, let the SDK do the required HTML+URL encoding of search strings.
📚 Types
All endpoints return fully typed models derived from the official API reference. You can import them directly:
import type { Invoice, VoucherlistItem, Contact, Voucher, Page } from "@cloudcreators-gmbh/lexware-ts-sdk/types";🔗 Links
- Dashboard/Vouchers:
https://app.lexware.de/vouchers - Create API Key:
https://app.lexware.de/addons/public-api - Lexware API docs:
https://developers.lexware.io/docs/#lexware-api-documentation
