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.).