Usage
Use-case-driven examples for ausbildungssuche-cli, a command-line client for the
Bundesagentur für Arbeit Ausbildungssuche API (the German federal
apprenticeship / vocational-training catalogue). It searches training offers and
fetches the full details of a single offer.
Install
npm i -g @maschinenlesbar.org/ausbildungssuche-cli
The installed binary is ausbildungssuche. All examples below use it. No key
is bundled: supply the public, documented X-API-Key via --api-key or the
AUSBILDUNGSSUCHE_API_KEY env var, or store it once with ausbildungssuche config set
api-key, before running (see the API-key section below).
Output is pretty-printed JSON on stdout (--compact for a single line), which
makes the examples pipe cleanly into jq.
The commands are:
ausbildungssuche obtain-key [--export] # print the public API key (run once first)
ausbildungssuche config set|get|unset|list api-key # keep the API key in a credentials file
ausbildungssuche search [filters…] # search offers (HAL+JSON envelope)
ausbildungssuche details <id> # full details for one offer
Use cases
1. Search apprenticeships for one occupation
Filter by occupation id (--ids, Berufs-id) — the quickest way in. 9162 is
Staatlich anerkannter Erzieher.
ausbildungssuche search --ids 9162 --size 10
The CLI also has a free-text flag (--sw, Suchwort), but the API currently
ignores it: any keyword returns the same offers as none. Use --ids to filter
by occupation (see use case 8).
search returns a HAL+JSON envelope: _embedded holds the offer objects,
_links carries paging links, and page carries paging metadata
(size, totalElements, totalPages, number). Pull out just the embedded
offers with jq:
ausbildungssuche search --ids 9162 --size 10 | jq '._embedded'
2. See how many results a search has before fetching them all
Read the page block to size up a query without downloading every page.
ausbildungssuche search --ids 9162 | jq '.page'
This prints { "size": …, "totalElements": …, "totalPages": …, "number": … },
so you know how many pages exist before paging through them.
totalElements is capped at 10000: the unfiltered catalogue and any broad
filter report exactly 10000, which means “10000 or more”, not an exact count. Rows
past the first 10000 cannot be fetched either — (page + 1) × size must stay at or
below 10000 (the last page at the default size 20 is 499); the CLI rejects a later
page (exit 2), which the API would answer with HTTP 500. Narrow the filters until
the count drops below 10000 to get an exact figure.
3. Search near a location within a radius
Scope a search to a place (--orte, a Name_lon_lat location string with the
longitude first) and a --uk radius in kilometres (10, 25, 50 or
100; the CLI rejects other values, which the API would answer with HTTP 400) — useful when a trainee can only travel so far.
The two only filter together: the API ignores a km radius without a place, and a
place without a radius does not restrict the search at all, so the CLI rejects
either one alone (exit 2).
ausbildungssuche search --ids 9162 --orte "Köln_6.957_50.938" --uk 50
With the latitude first (Köln_50.938_6.957) the API silently returns 0
results. On a place search each offer carries its distance from the place in
abstaende[].abstandInKm.
--uk accepts the literal Bundesweit to search the whole country with no
radius limit — on its own it is the same as leaving --uk out, and next to
--orte it keeps each offer’s distance from the place:
ausbildungssuche search --ids 9162 --uk Bundesweit
ausbildungssuche search --ids 9162 --orte "Köln_6.957_50.938" --uk Bundesweit
4. Page through a large result set
Walk results in fixed-size pages with --page (0-based) and --size (1..20).
The server serves at most 20 rows per page — it clamps a larger size to 20, so
--page would count in pages of 20 — and the CLI rejects a --size above 20.
# first page
ausbildungssuche search --ids 9162 --size 20 --page 0
# next page
ausbildungssuche search --ids 9162 --size 20 --page 1
Extract just the ids and self-links from a page to feed a follow-up details
call:
ausbildungssuche search --ids 9162 --size 20 --page 0 \
| jq '._embedded'
5. Fetch full details for one offer
After a search, take an offer id (a numeric id, as returned under
._embedded.termine[].id) and get its complete record. The details endpoint
returns plain JSON (a JSON array of matching records, not the HAL envelope).
ausbildungssuche details 365241044
Pipe to jq to inspect or reshape the detail object — the title lives at
angebot.titel and the provider at angebot.bildungsanbieter.name:
ausbildungssuche details 365241044 | jq '.[0] | {titel: .angebot.titel, anbieter: .angebot.bildungsanbieter.name}'
The id must be non-empty; an empty id is rejected client-side as a validation error before any request is made.
6. Filter to education-voucher–eligible offers
Restrict results to offers eligible for a Bildungsgutschein (--bg) — a
state-issued voucher that funds an approved training measure.
ausbildungssuche search --ids 9162 --bg
--bg is a boolean flag (no value); include it to turn the filter on.
7. Filter by region, offer type and start date
Combine the structured filters: --re (Bundesland code, e.g. SLH for
Schleswig-Holstein), --sty (offer type, 0..3), --bart (training type,
Bildungsart) and --bt (start-date code, Beginntermin: 2 for earlier dates,
101..112 for January..December of the following year; 0 and 1 filter too,
with an undocumented meaning — not a date).
The --re codes are BAW, BAY, BER, BRA, BRE, HAM, HES, MBV,
NDS, NRW, RPF, SAA, SAC, SAN, SLH and THÜ; several can be
comma-separated (--re NRW,BAY). An offer’s code is in
adresse.ortStrasse.land.code.
ausbildungssuche search --re SLH --sty 0 --bt 0
ausbildungssuche search --ids 9162 --bart 102 --re SAC
8. Search by profession id
Scope by occupation id (--ids). The id is the dkzId in an offer’s
angebot.systematiken[], next to the occupation name (kurzbezeichnung), so
you can read it from any search result:
ausbildungssuche search --re SAC --size 20 \
| jq -c '[._embedded.termine[].angebot.systematiken[] | {dkzId, kurzbezeichnung}] | unique'
Then filter on it; several ids can be comma-separated:
ausbildungssuche search --ids 9162 --uk Bundesweit
ausbildungssuche search --ids 9162,9106 --uk Bundesweit
9. Get compact, line-delimited output for scripting
Use --compact to emit single-line JSON — handy in pipelines, logs, or when
combined with jq -c.
ausbildungssuche --compact search --ids 9162 --size 5
The global option also works after the subcommand (commander hoists it):
ausbildungssuche search --ids 9162 --size 5 --compact | jq -c '._embedded'
10. Supply the API key against a custom base URL
No key is bundled: supply the public, documented key (or your own) via --api-key
or the AUSBILDUNGSSUCHE_API_KEY env var, and/or point at an alternative host with
--base-url — e.g. for a proxy or a staging endpoint. Prefer the env var for a
private key (an --api-key argument is visible in ps/shell history).
ausbildungssuche --api-key "$MY_KEY" search --ids 9162
AUSBILDUNGSSUCHE_API_KEY="$MY_KEY" ausbildungssuche search --ids 9162
ausbildungssuche --base-url https://proxy.internal.example search --ids 9162
# Or stored once in a credentials file (piped in, or typed without echo)
ausbildungssuche obtain-key | ausbildungssuche config set api-key
ausbildungssuche config set api-key
Precedence is --api-key flag > AUSBILDUNGSSUCHE_API_KEY env var > the credentials
file > no key. ausbildungssuche config keeps the key in
$XDG_CONFIG_HOME/ausbildungssuche/credentials (else
~/.config/ausbildungssuche/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
(a key given with --api-key is a usage error, exit 2, and is not repeated);
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. On a
redirect that crosses an origin boundary the client strips credential headers, so
a private key is never forwarded to another host.
A base URL on plain http: to a host other than loopback (localhost, 127.0.0.0/8,
::1) works, but the CLI writes one warning record to stderr before the first request, naming the
host and what travels unencrypted without printing it, e.g.
… WARN [ausbildungssuche.http] the API key is sent unencrypted to proxy.internal.example (http:, not https:)
(or requests to … are sent unencrypted with no key, or the base URL's credentials for a
user:password@ part). stdout and the exit code are unchanged.
Global options
These apply to every command and may be given before or after the subcommand, once
each (a repeated global option is a usage error, exit 2):
| Option | Description |
|---|---|
-V, --version |
Print the version number |
--base-url <url> |
API base URL (default https://rest.arbeitsagentur.de). A user:password@ part is sent but shown as ***@ in everything the CLI prints, usage errors included; write a literal % in it as %25 (an unescaped % is a usage error, exit 2) |
--api-key <key> |
X-API-Key header value (env AUSBILDUNGSSUCHE_API_KEY); no key is bundled. A rejected key is never repeated in the error, and a key typed without --api-key (as a command or an extra argument) is not echoed either |
--timeout <ms> |
Time limit per request in milliseconds, reading the whole response included (default 30000; at most 2147483647; 0 = no limit) |
--user-agent <ua> |
User-Agent header value |
--max-retries <n> |
Retries for transient 429/503 responses and reset connections (0..10); each waits at least the linear backoff, longer when the server’s Retry-After asks (up to 30 s; a longer one is not retried, and the error names the requested wait). Each retry logs one WARN record of ausbildungssuche.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 [ausbildungssuche.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected |
-h, --help |
Show help for the program or a command |
search filters
| Flag | Meaning |
|---|---|
--sw <text> |
search keyword (Suchwort); currently ignored by the API, use --ids |
--sty <n> |
offer type 0..3 (Suchtyp) |
--orte <loc> |
location as Name_lon_lat, longitude first, e.g. Köln_6.957_50.938, or several places comma-separated, searched together (Ort); needs --uk |
--re <code> |
Bundesland code, e.g. BAY, NRW, THÜ; comma-separated or repeated for several (Region) |
--uk <radius> |
radius around --orte: 10, 25, 50, 100 km, or Bundesweit (Umkreis); a km radius needs --orte |
--ids <id> |
numeric occupation id(s), comma-separated or repeated (Berufs-id, the dkzId); a non-numeric id or a trailing comma is a usage error (exit 2) |
--bart <type> |
numeric training type, the bildungsart.id, e.g. 102 Berufsausbildung (Bildungsart); anything else is a usage error (exit 2) |
--bg |
only education-voucher–eligible offers (Bildungsgutschein) |
--bt <code> |
start-date code(s): 2 earlier dates, 101..112 January..December of the following year; 0 and 1 are accepted and filter too (each keeps about half of the offers; their meaning is undocumented) (Beginntermin) |
--page <n> |
0-based page index |
--size <n> |
page size, 1..20 (the server serves at most 20 rows per page) |
Exit codes: 0 success, 2 usage/argument errors, 3 on 401/403 (1 when a
redirect to another origin dropped the key first), 4 on
404, 5 on 406 (Accept negotiation), 6 on a network/transport failure,
1 for any other error.