An unofficial command-line interface to Picnic's HTTP/JSON API, built for AI agents.
python3 (3.12+). That's it.
Clone the repo, then install it as an editable tool with
uv. This puts a picnic command on your PATH
while the code keeps running straight from the checkout — so git pull is all
it takes to update, and any edit takes effect on the next call:
git clone https://github.com/mac-cain13/picnic-cli.git
cd picnic-cli
uv tool install --editable .
picnic help # confirm it runsThe install is editable and the package has zero runtime dependencies (it's
stdlib-only), so nothing is vendored or copied into site-packages — the picnic
command is just a launcher pointed back at this directory.
No uv? The checkout is the runtime — run the ./picnic shim directly, no
install step at all:
git clone https://github.com/mac-cain13/picnic-cli.git
cd picnic-cli
./picnic helpPut it on your PATH with a symlink or alias to call it from anywhere:
ln -s "$PWD/picnic" /usr/local/bin/picnic # then just: picnic help
# or, in your shell rc: alias picnic="$PWD/picnic"# Authenticate (stores the auth key under ~/.config/picnic-cli/session.json).
export PICNIC_PASSWORD='your-password'
./picnic login you@example.com
# If 2FA is required, the login response says so:
./picnic generate-2fa SMS
./picnic verify-2fa 123456
# Now use it.
./picnic search "affligem blond" # raw search page (incl. promos)
./picnic cart # current cart
./picnic cart-add s1234567 2 # add 2× a product
./picnic slots # delivery slots
./picnic deliveries CURRENT # in-flight deliveries
# Discover the whole surface:
./picnic help # human-readable, grouped
./picnic commands # the same as a JSON arrayEvery command prints one JSON value, so pipe straight into jq:
./picnic cart | jq '.items[].display_price'
./picnic -c deliveries | jq '.[0].delivery_id' # -c = compact, single line| Channel | Content |
|---|---|
| stdout | Exactly one JSON value — the data on success, {"error": {...}} on failure. |
| stderr | Diagnostics only (a one-line error summary; request traces with -v). |
Exit codes: 0 ok · 1 upstream HTTP error · 2 usage error · 3 auth
(unauthorized / login failed) · 4 network / timeout.
Error codes (the error.code field): usage, unknown_command,
unauthorized (401 — re-login), forbidden (403 — often rate-limiting; back
off, don't re-login), not_found, http_error, io_error, login_failed,
network_error, timeout. On an HTTP error the full upstream body is preserved
under error.response.
Any endpoint — mapped or not — is reachable with raw:
./picnic raw GET /cart --picnic
./picnic raw POST /cart/add_product --body '{"product_id":"s123","count":1}' --picnic
./picnic raw GET /my_store?depth=0
echo '{"channel":"SMS"}' | ./picnic raw POST /user/2fa/generate --body-file - --picnic--picnic adds the x-picnic-agent/x-picnic-did headers some routes need;
--no-auth drops the auth header for public endpoints. raw is always live
(never cached), so debugging a route never sees stale data.
Slow-changing reads are cached on disk so repeated calls are instant and don't hammer the API. Three fixed classes: volatile (cart, slots, live tracking, mutations) is never cached; slow (search, products, pages, history) is cached 6 h; immutable (completed deliveries, wallet transactions) is cached forever.
A served-from-cache call prints when the data was generated, on stderr — stdout stays the raw JSON:
picnic: from cache — generated 2026-07-02T18:32:01Z (2h14m ago); --no-cache for live
The cache lives in cache/ inside the checkout (gitignored) — ls cache/ to see
it, cat cache/<file> to read an entry, rm -rf cache/* to clear it. Pass
--no-cache to fetch live and refresh the stored copy. See
docs/cache-spec.md.
Precedence: CLI flag → environment variable → session file → default.
| Setting | Flag | Env | Default |
|---|---|---|---|
| Country | --country |
PICNIC_COUNTRY |
NL (also DE, FR) |
| API version | --api-version |
PICNIC_API_VERSION |
15 |
| Auth key | --auth-key (one-shot) |
— | session file — see below |
| Base URL | --url |
PICNIC_URL |
derived from country + version |
| Device id | --device-id |
PICNIC_DEVICE_ID |
3C417201548B2E3B |
| Agent | --agent |
PICNIC_AGENT |
30100;1.228.1-15480; |
| Timeout (s) | --timeout |
PICNIC_TIMEOUT |
30 |
| Session file | — | PICNIC_SESSION |
~/.config/picnic-cli/session.json |
| Password (login) | — | PICNIC_PASSWORD |
— |
./picnic whoami shows the resolved session at a glance, including how many
days of auth runway remain.
The auth key is a JWT that Picnic silently rotates — each response to an
aged token carries a fresh one with a new ~180-day expiry. The CLI captures that
rotated token and writes it back to the session file, so normal use keeps you
logged in indefinitely and you never re-run login (which needs SMS 2FA).
The token lives only in the session file — there is no PICNIC_AUTH_KEY
env var. Bootstrap a machine either with login, or, if you already hold a
valid token (e.g. captured from the app), seed it directly:
pbpaste | ./picnic auth-import # or: ./picnic auth-import "<token>"--auth-key remains a one-shot override for debugging (it never rotates or
persists). Only if the CLI goes fully unused for >180 days does the token lapse
and require one re-login; whoami and every command warn on stderr once fewer
than 30 days remain. See docs/token-rotation.md.
picnic # bash shim → python3 -m picnic_cli
picnic_cli/
config.py # settings resolution + session persistence + defaults
http.py # urllib transport, header signing, error → contract
commands.py # the command surface — one small function per endpoint
cli.py # arg dispatch + the stdout/stderr/exit contract
tests/ # pytest + recorded JSON fixtures
Each file reads in under a minute — that's a feature, because when a route
breaks an agent has to locate and fix it fast. Start in commands.py.
python3 -m pytest # needs pytest (dev-only; not a runtime dependency)Fixtures live in tests/fixtures/. To add a real recording, drop the upstream
JSON there and assert non-lossy passthrough — see test_cli.py.
- The checkout is the runtime. Run it via the
./picnicshim →python3, or install it editable (uv tool install --editable .) so apicniccommand just points back at the checkout. Either way the code you run is the code you edit — no build step, no compile, edits take effect on the next call. - Stdlib-only.
urllib+json, zero runtime dependencies. Clone and run. - Raw & non-lossy. stdout is the upstream JSON, untrimmed. Responses are
never modelled or filtered —
searchreturns the whole search page, promos and all (the old wrapper dropped them). - Machine contract. stdout is exactly one JSON value; stderr is diagnostics
only; errors come back as
{"error": {...}}with a stablecode.
This unofficial picnic-cli is not affiliated with Picnic in any way.
There is absolutely no warranty in whatever way on this tool. The Picnic API is undocumented and Picnic can change it without notice breaking this tool.
Picnic-cli is based on the picnic-api project from MRVDH and released under the MIT license.