Usage

Real, use-case-driven examples for the jobsuche CLI — a command-line client for the Bundesagentur für Arbeit Jobsuche API (Germany’s largest job database). It exposes the API’s two read-only endpoints: search job listings (Stellenangebote) by keyword/location/radius, and fetch full job details by reference number (referenznummer, the refnr).

Every command prints pretty JSON to stdout (use --compact for a single line), so the examples pipe to jq where it helps.

Install

npm i -g @maschinenlesbar.org/jobsuche-cli

The installed binary is jobsuche. Without a global install you can run the built CLI directly with node apps/cli/dist/src/cli/index.js (substitute that for jobsuche in any example below).

The API needs an X-API-Key. No key is bundled: the public one is published upstream, and obtain-key reads it for you. Set it once per shell before the examples below:

eval "$(jobsuche obtain-key --export)"    # sets JOBSUCHE_API_KEY

# Or stored once in a credentials file (typed without echo, or piped in)
jobsuche config set api-key
jobsuche obtain-key | jobsuche config set api-key

Precedence is --api-key > JOBSUCHE_API_KEY > the credentials file > none. jobsuche config keeps the key in $XDG_CONFIG_HOME/jobsuche/credentials (else ~/.config/jobsuche/credentials), mode 0600, written atomically; config set reads the value from a prompt without echo or from stdin (at most 64 KiB), never from the command line (an extra argument, or a key given with --api-key, is a usage error, exit 2, and is not repeated; so is a mistyped name or a surplus argument to any config command — the error names the valid names, never what was typed); config get shows it masked (abcd…wxyz; **** below 20 characters, so for the published key; --reveal prints it whole); config list and config unset do what they say. A file that others can read is refused, and only when it is needed. So is a hand-edited value config set would refuse (blank, whitespace inside, a control character), naming the file (exit 1; config get and config list refuse it too, so config get api-key exits 0 only for a usable key).

Without a key the header is left out and the API answers 401/403 (exit code 3).

Use cases

1. Find jobs by keyword and city

Quickest way to see what’s available for a role in a place.

jobsuche search --was Informatiker --wo Berlin --size 10

--was is the keyword/title, --wo the location, --size caps the page size. The result is a JobSearchResult object: ergebnisliste (the listings — absent, not [], when nothing matched), plus maxErgebnisse (total matches), page, size, woOutput and facetten. woOutput.bereinigterOrt is the place the API actually searched: it corrects a typo silently and may resolve a garbled name to another town, or answer suchmodus: "UNGUELTIG" with an empty result. In those cases the CLI logs a WARN record on stderr (exit 0); check the place, or use a postcode.

2. Search within a radius of a location

Catch listings in the surrounding area, not just the exact city.

jobsuche search --was Pflegefachkraft --wo "München" --umkreis 50

--umkreis is the radius in km around --wo (0–200; the API rejects a larger one, so the CLI does too). Listings found around a --wo carry entfernung (distance in km, a number) you can sort on:

jobsuche search --was Pflegefachkraft --wo "München" --umkreis 50 \
  | jq '.ergebnisliste | sort_by(.entfernung)
        | .[] | {titel: .stellenangebotsTitel, ort: .stellenlokationen[0].adresse.ort, km: .entfernung}'

3. Only recently published listings

Skip stale postings — show what went live in the last week.

jobsuche search --was Data --wo Hamburg --veroeffentlicht-seit 7

--veroeffentlicht-seit <days> filters to listings published within the last N days (0–100; the API ignores a larger value, so the CLI rejects it). Combine with --umkreis for a tight, fresh local search.

4. Page through a large result set

Total matches can run into the thousands; walk them a page at a time.

# page 1
jobsuche search --was Projektmanager --size 25 --page 1
# page 2
jobsuche search --was Projektmanager --size 25 --page 2

--page is 1-based and --size is the page size. Check maxErgebnisse against page * size to know when you have reached the end:

jobsuche search --was Projektmanager --size 25 --page 1 \
  | jq '{total: .maxErgebnisse, page, size, returned: (.ergebnisliste // [] | length)}'

5. Extract just the reference numbers

Get a clean list of reference numbers to feed into details (use case 7).

jobsuche search --was Elektroniker --wo Köln --size 20 \
  | jq -r '.ergebnisliste[]?.referenznummer'

referenznummer (the refnr, e.g. 10001-1002716922-S) is the stable id for each listing and the input for the details command.

6. Filter by offer type (Angebotsart)

Separate regular job vacancies from apprenticeships/trainee postings.

# regular job vacancies (Arbeit)
jobsuche search --was Mechatroniker --wo Stuttgart --angebotsart 1

# apprenticeships / dual-study (Ausbildung)
jobsuche search --was Mechatroniker --wo Stuttgart --angebotsart 4

--angebotsart <code> takes one of the API’s documented offer-type codes: 1 regular vacancy, 2 self-employment, 4 apprenticeship/dual-study, 34 internship/trainee. Any other code is a usage error (the API would answer it with an empty result).

7. Fetch full details for a listing

Get the complete payload for one job — description, contact, dates, and more.

jobsuche details 14225-dafcdd47aabe512d-S

Pass the referenznummer from any search result; the CLI base64-encodes it into the API’s encryptedJobCode for you. A purely numeric refnr (e.g. 1002716922) or an already-encoded code also works. Exit code 4 means the listing was not found (404).

8. Search jobs at a specific employer

Narrow to one company’s openings in a region.

jobsuche search --arbeitgeber "Deutsche Bahn AG" --wo Frankfurt --umkreis 30

--arbeitgeber filters by employer name (matched exactly and case-sensitively against the full registered name, so use the exact spelling, e.g. "Deutsche Bahn AG"; a partial or lower-case name finds nothing). Pull a quick title-and-city overview:

jobsuche search --arbeitgeber "Deutsche Bahn AG" --wo Frankfurt --umkreis 30 \
  | jq -r '.ergebnisliste[]? | "\(.stellenangebotsTitel) — \(.stellenlokationen[0].adresse.ort)"'

9. Browse an occupational field, with or without temp-work agencies

Cast a wider net across a whole field, then leave out the staffing agencies.

jobsuche search --berufsfeld "Altenpflege" --wo Leipzig \
  --umkreis 40 --no-zeitarbeit

--berufsfeld searches a broad occupational category (vs. the more specific --was). Temp-work / staffing-agency (Zeitarbeit) listings are included by default; --no-zeitarbeit leaves them out, and --zeitarbeit returns only them.

10. Search → pick first result → fetch its details (one-liner)

Chain a search straight into a detail lookup without copy-pasting a reference number.

jobsuche details "$(jobsuche search --was Informatiker --wo Berlin --size 1 \
  | jq -r '.ergebnisliste[0].referenznummer')"

Useful in scripts. Add --compact to either call for single-line JSON when feeding another tool.

Global options

These apply to every command and may be given before or after the command name (e.g. both jobsuche --compact search … and jobsuche search … --compact):

Option Description
--base-url <url> API base URL (default https://rest.arbeitsagentur.de; http(s), a path prefix is fine, no ?query or #fragment, no surrounding or inner whitespace; a % in a user:password@ part must be an escape, %25 for a literal one). A user:password@ part is sent but shown as ***@ in everything the CLI prints, usage errors included. Plain http: to a host other than loopback (localhost, 127.0.0.0/8, ::1) logs one WARN record of jobsuche.http (… sent unencrypted to <host> (http:, not https:)) on stderr per run, naming the API key and the base URL’s credentials when they travel (never their value); stdout and the exit code are unchanged
--api-key <key> the X-API-Key to send (env JOBSUCHE_API_KEY, else the key stored with jobsuche config set api-key; no key is bundled, see obtain-key); blank/whitespace is ignored and the env var or the stored key is used, else no key is sent; a key that can’t be sent as a header is a usage error (exit 2) that never repeats the key
--timeout <ms> time limit per request in milliseconds, reading the whole response included (at most 2147483647)
--user-agent <ua> User-Agent header value (not blank; no control characters or characters above U+00FF, which a header cannot carry)
--max-retries <n> retries for transient 429/503 responses (0–10, default 2); each waits the server’s Retry-After (up to 30 s; a longer one is not retried, and the error names it) or else backs off linearly. Each retry logs one WARN record of jobsuche.http before it waits (HTTP 503 from host: retry 1 of 3 in 2 s)
--max-response-bytes <n> cap response body size in bytes (0 = unlimited; default 100 MiB)
--compact print JSON on a single line instead of pretty-printed
--log-format <format> How errors, warnings and notes are written to stderr: text (default; log4j style, 2026-10-09T14:03:12.481Z WARN [jobsuche.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected
-V, --version print the version
-h, --help show help (also jobsuche <command> --help)

Exit codes: 0 success; 2 usage errors (bad flag/value, missing command); 3 on 401/403; 4 on 404; 1 for any other error (network, parse, etc.).

View the source on GitHub →