npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

beliq-sevdesk

v0.2.0

Published

Self-hostable worker that polls sevDesk for e-invoices, converts each to the formats a counterparty needs, and validates them against beliq's authority-pinned, drift-checked rules. The sevDesk token never leaves your environment.

Readme

beliq-sevdesk

A small, self-hostable worker that polls sevDesk for your e-invoices, converts each one to the format a counterparty actually needs, and validates it against beliq's authority-pinned, drift-checked rules.

sevDesk generates one configured e-invoice format and trusts its own output. This worker adds the two things it cannot:

  • Conversion. sevDesk emits its single configured format. When a counterparty needs a different one (ZUGFeRD to Peppol BIS UBL, or a French Factur-X profile), the worker retargets each invoice with beliq convert.
  • An independent verdict. beliq validate gives an authority-pinned second opinion sevDesk does not provide, catching profile-specific gaps such as BR-DE rules or a missing buyer reference / Leitweg-ID for public buyers.

Your sevDesk token never leaves your environment: the worker runs where you run it, reads invoices directly from sevDesk, and only sends the invoice document to beliq for validation and conversion.

Install

npm install -g beliq-sevdesk
# or run without installing:
npx beliq-sevdesk --once

Requires Node.js >= 20.15.

Quick start

Set the two credentials, point it at the invoices you care about, and run it once:

export SEVDESK_API_TOKEN=...   # sevDesk: Settings -> Advanced -> API
export BELIQ_API_KEY=...       # beliq dashboard -> API Keys
export SEVDESK_TARGET_FORMATS=peppol-bis

beliq-sevdesk --once

That polls your Open invoices from the last 30 days, converts each to Peppol BIS UBL (written to ./out), validates each one, prints a one-line summary, and exits with a code you can gate on. Leave SEVDESK_TARGET_FORMATS empty to run validation-only (no conversion, no files written).

Copy .env.example to .env for the full set of settings.

How each invoice is processed

For every invoice newer than the last one it saw (tracked by a persisted high-water-mark, so nothing is processed twice):

  1. Pull the invoice XML from sevDesk (GET /Invoice/{id}/getXml).
  2. Convert it to each configured target format and write the bytes to the output dir as <invoiceNumber>-<target>.<ext> (.pdf for facturx / zugferd, else .xml). Any elements a conversion could not carry across are logged.
  3. Validate the source document and classify it: valid, invalid, or error (the pipeline threw before a verdict).

The high-water-mark advances only across a leading run of invoices that got a verdict. An invoice that errors (and any after it in the same batch) is left for the next poll, which is safe because reprocessing is idempotent.

Run once, or as a daemon

  • --once polls a single time and exits. Use this from cron or CI.
  • With no --once, it loops, polling every SEVDESK_POLL_INTERVAL_SECONDS (default 300) until the process is stopped.
  • --dry-run walks the full pipeline (real API calls, real verdicts) but writes no files and persists no state. Good for a first, safe look.

A cron entry that runs it every 15 minutes:

*/15 * * * * SEVDESK_API_TOKEN=... BELIQ_API_KEY=... SEVDESK_TARGET_FORMATS=peppol-bis /usr/bin/beliq-sevdesk --once >> /var/log/beliq-sevdesk.log 2>&1

Run it in a container

A prebuilt multi-arch image (amd64 + arm64) is published to GitHub Container Registry:

docker pull ghcr.io/beliq-eu/beliq-sevdesk:latest

The image ships only the compiled worker and @beliq/sdk from the public npm registry. No private beliq source is in it: all validation and conversion happen on the beliq API over HTTPS.

The container's entrypoint is the worker, so arguments pass straight through. Run a single poll:

docker run --rm \
  -e SEVDESK_API_TOKEN -e BELIQ_API_KEY -e SEVDESK_TARGET_FORMATS=peppol-bis \
  -v "$PWD/out:/app/out" -v "$PWD/state:/app/state" \
  ghcr.io/beliq-eu/beliq-sevdesk:latest --once

Inside the image the state file defaults to /app/state/state.json and the output dir to /app/out; mount volumes there to persist the high-water-mark and the converted documents across restarts. With no arguments the container loops as a daemon.

Example recipes

Ready-to-copy deployment recipes are in examples/:

Notify on failures

Set a webhook to get a JSON report POSTed after a poll:

export SEVDESK_NOTIFY_WEBHOOK=https://hooks.example.com/your/endpoint
# SEVDESK_NOTIFY_ON=failure (default) posts only when an invoice fails;
# SEVDESK_NOTIFY_ON=always posts after every poll (a heartbeat, good for cron).

The body:

{
  "ok": false,
  "summary": "processed 2 invoice(s): 1 valid, 1 invalid, 0 error",
  "counts": { "valid": 1, "invalid": 1, "error": 0 },
  "invoices": [
    { "id": "10", "invoiceNumber": "INV-10", "classification": "valid" },
    { "id": "11", "invoiceNumber": "INV-11", "classification": "invalid" }
  ],
  "polledAt": "2025-06-15T12:00:00.000Z"
}

Notify is best-effort: a slow, dead, or non-2xx endpoint is logged (host only, so a secret in the webhook path is never printed) and never changes the exit code. The exit code always reflects the invoices, not the notification.

Configuration

Every setting is read from the environment; the flags below override the matching variable.

| Variable | Flag | Default | Description | |---|---|---|---| | SEVDESK_API_TOKEN | --sevdesk-token | (required) | sevDesk API token. | | BELIQ_API_KEY | --api-key | (required) | beliq API key. | | SEVDESK_TARGET_FORMATS | --target-format | (none) | Comma-separated convert targets. Empty = validation-only. | | SEVDESK_TARGET_PROFILE | | (none) | Factur-X / ZUGFeRD profile, for a facturx / zugferd target. | | SEVDESK_INVOICE_STATUS | --status | Open | Draft, Open, Paid, or a numeric code. | | SEVDESK_POLL_WINDOW_DAYS | --poll-window-days | 30 | Only fetch invoices dated within n days back; 0 disables. | | SEVDESK_STATE_FILE | --state | .beliq-sevdesk-state.json | The persisted high-water-mark. | | SEVDESK_OUTPUT_DIR | --output | ./out | Where converted documents are written. | | SEVDESK_POLL_INTERVAL_SECONDS | --interval | 300 | Seconds between polls in daemon mode. | | SEVDESK_PAGE_SIZE | | 100 | Page size for the invoice listing. | | SEVDESK_MAX_RETRIES | | 4 | Retries on a sevDesk 429 / 5xx / network error. | | SEVDESK_NOTIFY_WEBHOOK | --notify-webhook | (none) | POST a JSON poll report here. Empty = no notifications. | | SEVDESK_NOTIFY_ON | | failure | failure (only on a failed invoice) or always (every poll). | | SEVDESK_BASE_URL | | https://api.sevdesk.de/api/v1 | Override for a mock or a future version. | | BELIQ_BASE_URL | | https://api.beliq.eu | Override for a self-hosted beliq. | | BELIQ_AUTH | | header | How the beliq key is sent: header (X-API-Key) or bearer. |

Allowed target formats: cii, ubl, zugferd, facturx, xrechnung, peppol-bis. Allowed profiles: basicwl, en16931, extended, extended-ctc-fr.

Exit codes

Meaningful with --once, so cron and CI can act on the result:

| Code | Meaning | |---|---| | 0 | every processed invoice was valid, or there was nothing to do | | 1 | at least one invoice failed validation | | 2 | config / usage error (missing token or key, bad flag or value) | | 3 | a sevDesk or beliq API error, or an invoice that errored mid-pipeline | | 4 | I/O error (unreadable state file, unwritable output) |

An error (code 3) outranks an invalid document (code 1): not getting a verdict is worse than getting a bad one.

Logging

Structured events are written to stderr, one JSON object per line, for log aggregation. The final human-readable summary is written to stdout, so beliq-sevdesk --once | tail -1 gives you the verdict while logs stay separate.

A note on the sevDesk token

The sevDesk API token is account-wide, unscoped, and does not expire. This worker is built so that token stays on your side: it never sends the token anywhere but sevDesk, and it is not a hosted service holding your credentials. Store it the way you store any production secret.

Development

npm install
npm run build
npm test              # unit tests, no network
npm run scrub:check   # no em-dash

# build the container image locally:
docker build -t beliq-sevdesk .

# live smoke against the real sevDesk + beliq APIs (skipped without creds):
SEVDESK_API_TOKEN=... BELIQ_API_KEY=... npm run test:integration

License

MIT