Output formats
Text for reading, json for piping, and the exit code that carries the rest.
Every command takes -o:
spr get /article/10.1007/s10994-021-05946-3 # text, for reading
spr get -o json /article/10.1007/s10994-021-05946-3 # json, for piping
spr get --body is a third thing: the response body itself, unformatted, so a page or a pdf goes straight to a file or another tool.
spr get --body /journal/10994 > journal.html
spr get --body --kind pdf /content/pdf/10.1007/s10994-021-05946-3.pdf > paper.pdf
The exit code is part of the output
A command that fetched a page successfully and a command that fetched a paywalled page both print something worth reading, so the difference between them is the exit code rather than an error message.
| Code | Meaning |
|---|---|
| 0 | It did what it was asked |
| 1 | A flag or argument this tool does not understand. Nothing was fetched. |
| 2 | The search surface answered with a client challenge and there was no fallback left |
| 3 | The page was fetched and understood and there was nothing in it |
| 4 | The publisher states access=No. The metadata was printed; only the body is missing. |
| 5 | A network failure, a timeout, or a 5xx that outlived the retries |
| 6 | An upstream said, in a header, that the budget is spent |
So this works the way you would want it to:
spr get -o json "$url" || case $? in
4) echo "paywalled, metadata above" ;;
2) echo "challenged, try the rss feed" ;;
esac
Every record carries an envelope
A record is not just fields. spr work -o json puts an envelope next to them:
{
"doi": "10.1007/s10994-021-05946-3",
"title": "Aleatoric and epistemic uncertainty in machine learning: an introduction to concepts and methods",
"envelope": {
"tier": "html",
"urls": ["https://link.springer.com/article/10.1007/s10994-021-05946-3"],
"fetched": "2026-08-18T10:12:04Z",
"status": "ok",
"redirects": 3,
"bytes": 718572,
"via": {
"authors": "linkdata:author[]",
"references": "highwire:citation_reference",
"sections": "region:section[data-title]"
},
"unread": ["MPU1-ad", "access-count", "altmetric-score"]
}
}
There is no missed key on that record because nothing was missed, which is the same rule the fields follow.
| Field | What it is for |
|---|---|
tier |
which surface produced the record: html today, and the api and the open indexes later |
urls |
the requested urls, never the effective ones, because the effective url carries a per request uuid and is not an identifier |
via |
which rung and which exact tag or region answered each field |
missed |
every field that was looked for and did not arrive, each with the reason |
unread |
every region on the page nobody read, so the record never looks more complete than it is |
Absent means absent. A field the page did not carry is left out of the json rather than emitted as null, so .abstract == null and no abstract key are the same answer, and a field in missed is the third case: it should have been there and something stopped it.
Seeing what it did
--debug puts one line per request on stderr, which stays out of the pipe:
$ spr get --debug --no-cache /article/10.1007/s10994-021-05946-3 -o json | jq .bytes
spr: 200 ok 718872 bytes 3 redirects https://link.springer.com/article/10.1007/s10994-021-05946-3
718872