Skip to content

Developer notes

Technical reference for developers who customize Lexware Office Connector. Every call to Lexware Office is taken from the official Public API documentation (https://developers.lexware.io/docs/, read 2026-11-07). Items the documentation does not state are marked not documented and are not claimed anywhere in the product.

Item Value
Add-on id kv_lexware_office
Function prefix fn_kv_lexware_office_
Language variables kv_lexware_office.*
CSS scope .kv-lexware-office
PHP namespace Tygh\Addons\KvLexwareOffice
Preset accounting (generated by kv new --kind integration), then rewritten for the Lexware Office API (API key, no OAuth)
Class Role
Client\BaseClient cURL with TLS verification and timeouts, JSON, error mapping (regular and legacy error bodies), masked logging
Client\Api All Lexware Office calls (table below); rate limit: a database lock per API key keeps 520 ms between calls, a 429 is retried with back-off
Client Client per company, “Test connection” (stores organizationId, profile and the countries list)
Accounts, Secret API key of test and live mode sealed with AES-256-GCM; one account per store owner or per vendor
Sync Pure builders (invoice, contact, credit note, tax types) and the queue jobs syncInvoice, syncCredit, syncReturn, syncPayment
Docs Table kv_lexware_office_docs (invoices and credit notes), file download, permalink
Webhook RSA-SHA512 signature check with the published public key; event ids
Config, Access Settings, owner mode, who may see and who may act
Queue, Jobs, JobException Job queue: back-off 5 min / 15 min / 1 h / 3 h / 12 h, per-job isolation, overlap lock
Idempotency lock() per order (GET_LOCK), claim(), key()
Log Event log with masking, filters, CSV, retention

Lexware Office API: endpoints, fields and where the documentation says so

Section titled “Lexware Office API: endpoints, fields and where the documentation says so”
What the add-on does Call Documentation section
Base URL, version https://api.lexware.io/v1/... (the old gateway URL was replaced; change log 27.05.2025: lexoffice became Lexware Office) Getting started, change log
Authentication Authorization: Bearer {API key}; the key is created by the merchant at app.lexware.de/addons/public-api Authentication
Rate limit 2 requests per second per client, over all endpoints; HTTP 429 and the call is not performed; token bucket and exponential back-off recommended Rate Limits
Test connection GET /v1/profile (organizationId, companyName, taxType, distanceSalesPrinciple, smallBusiness, businessFeatures) Profile Endpoint
Country classification GET /v1/countries (taxClassification de, intraCommunity, thirdPartyCountry) Countries Endpoint
Find a contact GET /v1/contacts?customer=true&email=... (pattern match: _, %, backslash escape; at least 3 characters; &, <, > HTML-encoded for names) Filtering Contacts, Search String Encoding
Create a contact POST /v1/contacts with version: 0, roles.customer: {}, company (name, vatRegistrationId, one contactPersons entry with lastName) or person (lastName), addresses.billing[0] (countryCode required), emailAddresses, phoneNumbers, note Create a Contact
Update a contact GET for version, then PUT /v1/contacts/{id}; a contact with more than one address / e-mail cannot be updated through the API Update a Contact, Optimistic Locking
Create an invoice POST /v1/invoices (draft) or POST /v1/invoices?finalize=true (status open, number assigned). Required: voucherDate, address (contactId, or name + countryCode), lineItems, totalPrice.currency, taxConditions.taxType, shippingConditions.shippingType (shippingDate for delivery). The status cannot be changed through the API afterwards Create an Invoice
Line items type: custom (no Lexware article needed): name, quantity (4 decimals), unitName, unitPrice {currency: EUR, netAmount or grossAmount (by taxType, 4 decimals), taxRatePercentage}, discountPercentage; text up to 255 / 2000 characters Line Items Details, Texts in Sales Vouchers
Tax types net, gross, vatfree, intraCommunitySupply, constructionService13b, externalService13b, thirdPartyCountryService, thirdPartyCountryDelivery, photovoltaicEquipment; vat-free types other than vatfree need a referenced contact and rate 0; taxSubType distanceSales / electronicServices for EU consumers Tax Conditions Details
Tax rates Domestic: 7 and 19 (0 for vat-free). EU destination rates only for distance sales when distanceSalesPrinciple is DESTINATION Valid Tax Rates
Discount totalPrice.totalDiscountAbsolute Total Price Details
Read an invoice GET /v1/invoices/{id}: voucherStatus (draft, open, paid, voided), voucherNumber, electronicDocumentProfile (NONE, EN16931 = ZUGFeRD, XRechnung), totalPrice.totalGrossAmount Invoices Properties
Credit note POST /v1/credit-notes?precedingSalesVoucherId={invoice}&finalize=true. “When referenced to an invoice, a finalized credit note is immediately paidoff and the open amount of the invoice is reduced by the amount of the credit note.” An invoice in draft is refused with 406 Pursue to a Credit Note
Files GET /v1/invoices/{id}/file and /v1/credit-notes/{id}/file with Accept: application/pdf (every profile) or application/xml (XRechnung only; 404 for ZUGFeRD and plain PDF); a draft has no file (409). The PDF of an XRechnung is only a preview Download an Invoice File
Payment state GET /v1/payments/{voucherId} (read only): openAmount, paymentStatus (balanced, openRevenue), voucherStatus, paidDate, paymentItems. There is no call to book a payment through the API Payments Endpoint
Look up a lost document `GET /v1/voucherlist?voucherType=invoice creditnote&voucherStatus=any&createdDateFrom=…&size=250&page=N (voucherTypeandvoucherStatusare mandatory; up to 250 per page), thenGET /v1/invoices
Event subscriptions POST /v1/event-subscriptions {eventType, callbackUrl} (409 when it exists; HTTPS with a valid certificate, Lexware sends a HEAD request first), GET, DELETE /v1/event-subscriptions/{id}. Events used: invoice.status.changed, payment.changed, credit-note.status.changed Event Subscriptions Endpoint
Webhook signature RSA-SHA512 signature of the JSON body (without whitespace and line breaks), base64, header X-Lxo-Signature; verified with the public key published in the documentation (embedded in Webhook::PUBLIC_KEY). The sample payload and signature of the documentation are a unit test Verify Authenticity
Webhook delivery Answer 200, 201, 202 or 204 within 5 seconds; 401, 403, 429 and 5xx are retried (5 retries at 10, 20, 40, 80, 160 s, then 20 retries every 2 h); 404 removes the subscription; payload organizationId, eventType, resourceId, eventDate (no event id: the add-on hashes type, resource and date) Delivery Failure Behavior, Webhook Callback Properties
Permalink https://app.lexware.de/permalink/invoices/view/{id} Deeplink to an Invoice
Errors Regular: message, details[{violation, field, message}]; legacy (contacts, files, vouchers): IssueList[{i18nKey, source, type}]; 401 {"message": "Unauthorized"}; 402 contract issue; 406 validation; 409 conflict (outdated version, draft file); 500 “Internal server error or rate limit exceeded”; 504 after 30 s Error Codes, HTTP Status Codes
Date format yyyy-MM-ddTHH:mm:ss.SSSXXX (milliseconds exactly 3 digits, offset with a colon) Datetime Format
  • A sandbox: Lexware Office has none; “test mode” of the add-on is a second (trial) account with its own API key. The documentation says a free trial account with all features can be created and that you can create as many as you need.
  • That vatRegistrationId accepts non-German VAT ids: the documentation says the id “has to follow the german rules”. The add-on sends the buyer’s VAT id and, when Lexware Office rejects it (406 naming vatRegistrationId), creates the contact without it and logs a warning.
  • An idempotency key: there is none. The add-on stores a pending document row before the POST and looks the document up in the voucher list after a timeout (see below).
  • That electronicDocumentProfile can be chosen through the API: the XRechnung format is produced by Lexware Office for a contact with a Leitweg-ID (xRechnung.buyerReference and vendorNumberAtCustomer on the contact, set in Lexware Office). The add-on reads the profile back and offers the XML download; it does not create Leitweg-ID contacts.
  • ZUGFeRD / XRechnung “compliance”: the add-on makes no claim; Lexware Office produces the files.

Idempotency and races (recurring-defect checklist)

Section titled “Idempotency and races (recurring-defect checklist)”
  1. Sync::syncInvoice() and Sync::syncCredit() take Idempotency::lock('lxo-order-<id>') (MySQL GET_LOCK) around “look up, POST, store”.
  2. The document row is written in state P (pending) with attempt_at and the expected gross before the POST; the remote id is stored in the same request as the answer.
  3. After a timeout or 5xx (ApiException::retryable and status != 429) the row stays P; the next attempt calls Sync::reconcile() (GET /v1/voucherlist, all pages of 250, type and created since the attempt day; each document not already ours is read with GET and compared by the reference Ref KV-<order>-<random> that every document carries in its closing text (remark); Lexware assigns the voucher number itself and has no external reference field). A match is adopted; none within 20 minutes waits; none after that sets the row to failed with a “check Lexware Office, then press Issue now” message: the add-on never POSTs again by itself, a finalized document cannot be deleted through the API. A scan cut by the page or read cap stops the job the same way.
  4. A 429 means “not performed”: Api::send() repeats the call (1, 2, 4 s or Retry-After) up to three times.
  5. One credit note per invoice (Credit Notes Endpoint > Purpose: “There can only be one credit note related to an invoice.”): when a row C in state O or P exists for the order, a further credit is set to X with a warning in the log and never sent (the API would answer 406). The first credit is capped to the invoice; a surplus of at most 5 cents (rounding) is removed with totalDiscountAbsolute, more is refused.
  6. Vendor scope: Access::canActOn() (root admin, or the vendor of the record when owner mode is off) guards every POST that creates or retries a document; run_now, backfill and cron are admin only in vendor_multivendor.post.php; lists are filtered by company.
  7. Dates are the shop calendar day (date(), CS-Cart sets the PHP zone from Appearance > Time zone), never gmdate.
  8. Text is cut with mb_substr(..., 'UTF-8'); request bodies are encoded with JSON_UNESCAPED_UNICODE.
Table Purpose
?:kv_lexware_office_accounts API key per mode (sealed), organizationId, profile and countries cache (extra), last connection check
?:kv_lexware_office_docs Invoices (kind I) and credit notes (kind C, ref_key full or ret<return id>): remote id, number, state P/O/E/X, Lexware status, profile, tax type, gross, open amount, paid date, stored line detail
?:kv_lexware_office_map Contact per customer (guests: one per order)
?:kv_lexware_office_log Log
?:kv_lexware_office_jobs Queue
Hook Purpose
PHP change_order_status_post Queues the invoice at the configured status and the credit note at the refund status
PHP rma_update_details_post Queues a credit note for a return completed as a refund (only when the Returns add-on is active)
Template orders:tabs_extra (admin) The “Lexware Office” tab of the order
Template orders:details_bullets (storefront) The invoice download link on the customer’s order page

The storefront output is limited to that link on orders.details (a session page of a signed-in customer, never cached by full page cache) and the endpoints kv_lexware_office.pdf, .cron, .webhook (no template, not cacheable).

Test the HTTP layer without Lexware Office

Section titled “Test the HTTP layer without Lexware Office”

Development only, in config.local.php:

$config['kv_lexware_office_base_url'] = 'http://127.0.0.1:11830/v1'; // mock server
$config['kv_lexware_office_webhook_key'] = "-----BEGIN PUBLIC KEY-----\n..."; // public key of the mock (GET /__ctl/pubkey)
$config['kv_lexware_office_currency'] = 'EUR'; // dev stores whose primary currency is not EUR

node tests/mock-server/server.js 11830 starts a stand-in that follows the documentation (Bearer key, 2 requests per second with 429, required fields, vat-free rules, totals computed server-side, drafts, files by Accept, credit notes against the invoice, voucher list, subscriptions, RSA-signed webhooks). node tests/e2e/e2e.mjs --store cs417 starts its own mock and edits config.local.php for the run; php tests/unit.php runs the unit tests against the official samples in tests/fixtures.

Admin templates live in design/backend/templates/addons/kv_lexware_office/. Do not edit them in a store; they are replaced on update.