Usage
ddb — a CLI for the Deutsche Digitale Bibliothek (DDB) v2 API. This is the
use-case-driven cookbook; for the option reference see the
README, and for domain terms the Glossary.
ddb [global options] <command> [command options]
The v2 read routes are public — no API key. Just run the commands.
Global options
| Option | Description |
|---|---|
--base-url <url> |
API base URL (default …/2; only http:/https: accepted; plain http: to a non-loopback host logs one WARN record of ddb.http, … unencrypted …, on stderr) |
--timeout <ms> |
per-request timeout in ms (0 = no timeout) |
--user-agent <ua> |
User-Agent header value |
--max-retries <n> |
retries for transient 429/503 responses and reset connections (0..10; each waits the server’s Retry-After, up to 30 s; each retry logs one WARN record of ddb.http before it waits) |
--max-response-bytes <n> |
cap the response body size in bytes (0 = unlimited; default 100 MiB) |
--compact |
print JSON on a single line (for piping to jq) |
--log-format <format> |
How errors, warnings and notes are written to stderr: text (default; log4j style, 2026-10-09T14:03:12.481Z WARN [ddb.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected |
-o, --output <file> |
write output to a file instead of stdout (- = stdout; refuses to overwrite an existing file unless --force, and a directory always — checked before any request, exit 1) |
--force |
overwrite the --output file if it already exists (needs --output) |
-V, --version / -h, --help |
version / help |
An option that takes one value may be given once: a second --rows, --sort,
--base-url … is a usage error (exit 2) instead of silently replacing the first.
--filter and --facet are the exceptions: they collect every value.
A --base-url on plain http: to a host other than the loopback interface gets one warning
on stderr before the first request, a WARN record of ddb.http, e.g. … WARN [ddb.http] requests to mirror.example are sent
unencrypted (http:, not https:), or … WARN [ddb.http] the base URL's credentials are sent unencrypted
to mirror.example (http:, not https:) when it carries a user:password@ (never printed).
stdout and the exit code are unchanged; --help, --version and usage errors never warn.
search — find objects
ddb search <query> [options]
v2 search is a Solr passthrough: the query and --filter use Solr syntax, and
the response is native Solr JSON. Some starting points:
ddb search Goethe # simple keyword
ddb search "Weimarer Republik" # phrase (quote it in your shell)
ddb search 'Goethe AND Faust' # boolean operators
ddb search '*:*' # match everything (browse mode)
ddb search 'title:Faust' # field-scoped query
| Option | Description |
|---|---|
--rows <n> |
number of documents (Solr rows, default 10; at most 2147483647) |
--offset <n> |
offset of the first document (Solr start, paging; at most 2147483647) |
--sort <spec> |
Solr sort, e.g. "score desc" or "id asc" |
--fields <list> |
fields to return (Solr fl), e.g. id,label,type |
--filter <fq> |
Solr filter query (repeatable), e.g. type_fct:mediatype_002 |
--facet <field> |
return counts for this facet field (repeatable) |
--facet-limit <n> |
cap the number of values per facet (needs --facet; Solr’s default is 100, -1 = all, still sorted by count) |
--collection <name> |
Solr collection (default search) |
--handler <name> |
Solr request handler (default select). Only handlers that return Solr’s standard response envelope (numFound, docs) are supported; any other answer fails (exit 1) with an error naming the handler |
The result shape is native Solr:
{
"responseHeader": { "status": 0, "params": { "q": "Goethe", "rows": "10" } },
"response": {
"numFound": 99866,
"start": 0,
"docs": [ { "id": "…", "label": "…", "type": ["mediatype_002"] } ]
},
"facet_counts": { "facet_fields": { "type_fct": ["mediatype_003", 50024] } }
}
Filtering with facets
--filter <fq> narrows the result set with a Solr filter query; --facet <field>
asks Solr to return value counts for a facet (to build the next filter):
# Images (a media-type code) from Berlin
ddb search '*:*' --filter type_fct:mediatype_002 --filter 'place_fct:"Berlin"'
# Two places at once (OR inside one filter query)
ddb search Bauhaus --filter 'place_fct:("Berlin" OR "Dessau")'
# Ask for the object-type distribution of a query, top 5 values
ddb search Goethe --rows 0 --facet objecttype_fct --facet-limit 5 \
| jq '.facet_counts.facet_fields.objecttype_fct'
Facet counts arrive as a flat array [value, count, value, count, …], which
can include values with count 0. To turn one into {value, count} objects and
drop the zeros:
ddb search Bauhaus --rows 0 --facet place_fct --facet-limit 10 \
| jq '.facet_counts.facet_fields.place_fct
| [range(0; length; 2) as $i | {value: .[$i], count: .[$i+1]}]
| map(select(.count > 0))'
Paging
# First page (1–10) then the next page (11–20)
ddb search Goethe --rows 10
ddb search Goethe --rows 10 --offset 10
When more documents match than were returned, ddb logs a note (an INFO record of ddb.api) like
… INFO [ddb.api] 99866 documents match; 10 shown. to stderr (past the first page with the
positions, e.g. 10 shown (11–20)) — page with --offset or
narrow with --filter. Read response.numFound for the true total. A --rows 0
facet query gets no note.
Paging past the end — an --offset at or beyond the total — is not an error: Solr
answers with an empty docs and ddb exits 0, but stderr says so, naming the total
and the offset asked for:
ddb search 'Goethe Faust' --offset 1000000 --rows 2 --fields id
# stderr: … INFO [ddb.api] 6584 documents match; --offset 1000000 is past the end, so none are shown.
A paging loop can stop when response.docs is empty or --offset reaches
response.numFound.
item — object detail
ddb item <id> [--part <component>] [--lang <code>]
<id> is the exact 32-character id from a search result’s id (upper-case
letters and digits; anything else is a usage error, exit 2). --part
selects which component to fetch — most are JSON, a few are XML / a plain file
and print raw (so > file.xml and piping keep them intact). With -o, a
redirect or a pipe you get the upstream bytes exactly — any charset, CRs included,
nothing appended; only on a terminal is the body decoded (by its declared charset, so a
Latin-1 record shows its umlauts) and control characters removed first:
--part |
Returns | Format |
|---|---|---|
view (default) |
the data set a DDB frontend object page is built on | JSON |
aip |
the full Archive Information Package | JSON |
edm |
the Europeana Data Model record | RDF/XML |
binaries |
related binary files (thumbnails, media) and their URLs | JSON |
children |
child items (accepts --rows/--offset, the only part that does) |
JSON |
parents |
the chain up the hierarchy: the object itself first, the institution last | JSON |
source |
the ingest source metadata | JSON |
source-description |
a description of the source record | JSON |
source-record |
the raw provider record (METS/MODS, LIDO, MARCXML); see below for sub-items | XML |
iiif |
the IIIF Presentation manifest (only where present → else 404) |
JSON |
citation |
a newspaper-issue citation file (only where applicable) | BIB file |
A sub-item’s source-record may belong to an ancestor. Sections of a digitised
book and units of an archive finding aid have no provider record of their own: the API
answers 303 and points to an ancestor’s record (in a Location without the /2
prefix, which can’t be followed as sent). ddb fetches that ancestor’s record through
the base URL — up to maxRedirects (5) levels — prints it, and notes on stderr whose it
is (an INFO record of ddb.api): item JG3Y… has no source-record of its own; this is the source-record of its
ancestor JY7H…. That file is the whole parent record (the volume’s METS), not one for
the section. If no ancestor has one, the 404 (exit 4) names the chain:
… item N4N7… has no source-record of its own; the API points to its ancestors (N4N7… →
NSNK… → RQBX…), and the last has none either.
--lang <code> sets the preferred label language for
view/aip/edm/binaries/source/source-description (with any other part
it is a usage error, exit 2, as are --rows/--offset outside children). An object without a
record in that language answers 404 (exit 4); --lang en did so for every
object tried on 2026-09-15, so retry without --lang before treating the id as
wrong.
ID=$(ddb search Goethe --fields id | jq -r '.response.docs[0].id')
ddb item "$ID"
ddb item "$ID" --part edm -o object.edm.xml # RDF/XML → file
ddb item "$ID" --part binaries | jq '.'
ddb item "$ID" --part children --rows 20 # first 20 children
version — connectivity check
ddb version # e.g. 7.5
Useful to confirm the API is reachable and the CLI is wired up. Anything but a short
version token — an HTML page from a captive portal or proxy login, JSON, an empty body —
fails with exit 1 (“expected a version string such as “7.5””), so a wrong --base-url
that answers 200 doesn’t pass the check.
Scripting recipes
# Object ids + labels of the current page
ddb search Goethe | jq -r '.response.docs[] | "\(.id)\t\(.label)"'
# Total number of matches
ddb search Goethe | jq '.response.numFound'
# Top 10 providers for a query (flat facet array)
ddb search Goethe --rows 0 --facet provider_fct --facet-limit 10 \
| jq '.facet_counts.facet_fields.provider_fct'
# Save a full result page to disk
ddb --output goethe.json search Goethe --rows 100
Exit codes
| Code | Meaning |
|---|---|
0 |
success (help/version included); an empty result also exits 0 |
1 |
API/logical error, or a catch-all (includes an unexpected 403) |
2 |
usage error (bad flags, unknown command, wrong-length item id, bad --base-url) |
4 |
HTTP 404 (not found) |
6 |
network / transport failure (DNS, connection, timeout, response size-cap) |
A reader that stops early (ddb search … | head) ends the run quietly with exit 0.
If stderr’s reader is gone (2>&1 | true), a failed run still exits with its own code.
Notes
- No API key. The read routes are public; a
403means a custom--base-urlhit an authenticated endpoint or the item component is access-restricted. - Metadata returned by this CLI is CC0 (no attribution required); object media (not downloaded here) carry per-object rights — see DATA_LICENSE.md.
searchreturns native Solr JSON. Hits live underresponse.docs[], the total isresponse.numFound, facet counts underfacet_counts.facet_fields.- Docs can be verbose — use
--fields/florjqto project what you need. - The API exposes only CC0 metadata, a narrower set than the DDB web portal.