Skip to content
controlresellPublic

About

Command-line interface for the ControlResell public API

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ControlResell CLI

cr is the command-line client of the ControlResell public API: read your marketplace accounts, your stock and your buyer conversations from a terminal, a script or a CI job. It is a native executable for macOS, Linux and Windows, built with Kotlin Multiplatform on the ControlResell Kotlin SDK. No JVM is required.

The commands below describe the first release, 1.0.0. Releases are listed on the GitHub releases page.

Install

Each release ships signed, version-pinned installers. These commands install the latest release:

curl -fsSL https://github.com/controlresell/cli/releases/latest/download/install.sh | sh
& { $f = Join-Path $env:TEMP ([IO.Path]::GetRandomFileName() + '.ps1'); try { Invoke-WebRequest -UseBasicParsing 'https://github.com/controlresell/cli/releases/latest/download/install.ps1' -OutFile $f; & ([scriptblock]::Create([IO.File]::ReadAllText($f))) } finally { Remove-Item $f -ErrorAction SilentlyContinue } }

The installer verifies the release signature against the public key embedded in the executable, installs it in your user's application data and makes cr available by name (a ~/.local/bin link on macOS/Linux, your user PATH on Windows). Set CONTROLRESELL_INSTALL_ROOT to choose another directory, or CONTROLRESELL_INSTALL_BUNDLE to install from a downloaded release directory without network access. Running the installer again keeps your saved login.

The script you download is pinned to the release it comes from, so every file it fetches matches that exact version. latest serves the newest stable release only. To install a specific version or a pre-release, replace releases/latest/download/ with releases/download/<version>/, without a v prefix (for example releases/download/1.0.0/).

The installer runs cr install, which you can also run yourself: --version picks a release, --bundle DIR installs a downloaded release directory, --root DIR chooses the directory. The default directory is ~/Library/Application Support/ControlResell on macOS, ~/.local/share/controlresell on Linux and %LOCALAPPDATA%\ControlResell on Windows. To uninstall, run cr update --automatic uninstall if you enabled automatic updates, then delete that directory, and ~/.config/controlresell to remove the saved login.

The command needs no sudo or administrator rights. On macOS and Linux it is a symbolic link ~/.local/bin/cr to the stable launcher (<root>/bin/cr), never to a versioned payload, so updates and rollback need no relinking; when ~/.local/bin is not in your PATH, the installer prints the exact line to add (export PATH="$HOME/.local/bin:$PATH") and never edits your shell profile. On Windows the launcher directory is appended to your user PATH (HKCU\Environment); open a new terminal to see it. Running the installer again changes nothing, and it never overwrites a file or link it did not create: if ~/.local/bin/cr is something else, the installation finishes, nothing is replaced and the installer exits with an error. cr install --no-path (not available through the install scripts) skips this step. To undo it, delete ~/.local/bin/cr (macOS/Linux) or remove the launcher directory from your user PATH (Windows).

Log in

Create an API key in the ControlResell app, with the scopes of the commands you use (accounts:read, items:read, conversations:read, orders:read, customers:read, webhooks:read, and items:write, conversations:write, webhooks:write for the commands that change something; a missing scope makes the command exit with 3). Keys start with cr_. Pass it on standard input. It is checked with the API before it is saved, and never appears in your shell history or in the process list:

cr login            # paste the key; input is hidden on a terminal
printf '%s\n' "$KEY" | cr login
cr whoami
cr logout           # removes the saved key; revoke the key itself in the app

The key is saved in $XDG_CONFIG_HOME/controlresell/config.json, falling back to ~/.config/controlresell/config.json (%USERPROFILE%\.config\controlresell\config.json on Windows), readable only by you: directory 0700 and file 0600 on macOS and Linux, a private access list on Windows. In CI or scripts, set CONTROLRESELL_API_KEY instead: it takes precedence over the saved key, and no file is written. cr login --api-url URL targets another API origin; remote origins must use HTTPS. The variable uses the origin saved by an earlier cr login --api-url, if any, and the default API otherwise; cr logout removes the file, not the variable.

Commands

cr accounts list
cr accounts get ACCOUNT_ID
cr items list [--sold true|false] [--account ACCOUNT_ID]
cr items get ITEM_ID
cr items listings ITEM_ID
cr orders list [--bucket to_send|to_pickup] [--platform VINTED] [--account ACCOUNT_ID] [--from 2026-10-01T00:00:00Z] [--to ...]
cr orders get ORDER_ID
cr orders labels ORDER_ID
cr customers list [--account ACCOUNT_ID] [--from 2026-10-01T00:00:00Z] [--to ...]
cr customers get CUSTOMER_ID
cr catalogs categories [--parent CATEGORY_ID] [--platform VINTED]
cr catalogs category CATEGORY_ID
cr catalogs fields CATEGORY_ID
cr catalogs brands [--q nik]
cr catalogs sizes|colors|conditions|package-sizes|materials [--category CATEGORY_ID]
cr favorites list [--account ACCOUNT_ID] [--item ITEM_ID] [--from 2026-10-01T00:00:00Z] [--to ...]
cr items favorites ITEM_ID [--from ...] [--to ...]
cr conversations list [--unread true|false] [--account ACCOUNT_ID] [--buyer BUYER_ID] [--item ITEM_ID] [--offer-status PENDING|ACCEPTED|REJECTED|CANCELED]
cr conversations get CONVERSATION_ID
cr conversations messages CONVERSATION_ID
cr items create --name NAME [--sku S] [--description D] [--photo-id ID ...] [--category-id N] [--brand-id N] [--size-id N] [--color-id N ...] [--package-size-id N] [--material-id N ...] [--measurement-length CM] [--measurement-width CM] [--enum KEY=VALUE ...] [--attribute KEY=VALUE ...] [--custom-field ID=VALUE ...] [--purchase-place-id N] [--selling-place-id N] [--state N] [--sex S] [--label L ...] [--sold true|false] [--estimated-price 12.50] [--purchase-price P] [--selling-price P] [--purchase-date T] [--selling-date T] [--currency EUR]
cr items update ITEM_ID [--name NAME] [the same fields as create; the ones you leave out keep their value]
cr items publish ITEM_ID --account ACCOUNT_ID [--account ...] [--cancel-before-proceeding] [--scheduled-at 2026-11-02T10:00:00Z]
cr items price-drop ITEM_ID --type ABSOLUTE|PERCENTAGE --value 3 [--round-type UP|DOWN|NEAREST] [--round-step 0.5] [--account ACCOUNT_ID] [--scheduled-at T]
cr items republish ITEM_ID [--account ACCOUNT_ID] [--price-variation -5] [--allow-if-deleted] [--drop-type T --drop-value V] [--scheduled-at T]
cr items delete ITEM_ID [--yes]
cr conversations send CONVERSATION_ID --text "Hello!"
cr conversations offer CONVERSATION_ID --price 12.50 --currency EUR [--transaction TRANSACTION_ID]
cr files upload FILE [--filename NAME] [--content-type image/jpeg|image/png|image/webp|image/heic|image/heif] [--idempotency-key K]
cr files get FILE_ID
cr webhooks list
cr webhooks get ENDPOINT_ID
cr webhooks create --url https://example.com/hooks --event item.sold [--event ...] [--description D] [--payload light|full]
cr webhooks update ENDPOINT_ID [--url U] [--event ...] [--description D] [--payload light|full] [--enable|--disable]
cr webhooks delete ENDPOINT_ID [--yes]
cr webhooks secret ENDPOINT_ID
cr webhooks rotate-secret ENDPOINT_ID [--expires-in-hours 24]
cr webhooks test ENDPOINT_ID
cr webhooks deliveries ENDPOINT_ID [--status pending|delivered|failed] [--type item.sold]
cr webhooks retry ENDPOINT_ID DELIVERY_ID
cr webhooks recover ENDPOINT_ID --since 2026-10-23T00:00:00Z
cr webhooks events [--since 2026-10-23T00:00:00Z] [--type item.sold ...]
cr webhooks event-types
cr version

Writing

The write commands need a key with a write scope: items:write for cr items create|update|publish|price-drop|republish|delete, conversations:write for cr conversations send|offer. They act on your real marketplace accounts: the publication, price drop and republication run in your accounts' browsers, a message or an offer reaches the buyer. Read what you are about to send.

  • cr items delete asks [y/N] on standard error before deleting, and deleting cannot be undone. Pass --yes to skip the question in a script; with no input to answer it (a closed standard input) and no --yes, it exits with 2 and does nothing. An item still online is refused by the API (item_still_listed, exit 7).
  • cr items update changes the item in ControlResell only, not its listings on the marketplaces; a list you give (--label, --color-id, --photo-id, --material-id, --enum, --attribute, --custom-field) replaces the whole list, and clearing a field is not available from the CLI yet.
  • cr files upload sends a photo (jpeg, png, webp, heic or heif, at most 20 MiB) and prints its id, which cr items create|update --photo-id ID then uses; it needs items:write, and cr files get needs items:read (its status is PENDING until the bytes are stored).
  • --scheduled-at T on publish, price-drop and republish runs the action at that ISO-8601 instant (with its offset, at most 30 days ahead) instead of now; without it the action runs now.
  • Every write that creates something (create, publish, price-drop, republish, send, offer, files upload) carries an Idempotency-Key, random by default. Pass --idempotency-key KEY (1 to 255 characters) to retry a command safely: the API answers the first response again instead of writing twice.

Every command accepts --help. Read commands print a table or a summary; --json prints the API's JSON response unchanged, for jq and scripts.

Lists are paginated. --limit sets the page size (1 to 100, 50 by default). When more results exist, the cursor of the next page is printed on standard error; run the same command again with --cursor. --all reads every page, 100 per request unless --limit is given, and stops at 10,000 elements. With --json, it prints a single {"data": [...], "nextCursor": ...} document; its cursor is null unless the 10,000 ceiling stopped the reading, and --all --cursor continues from there.

Rate-limited requests are retried after the delay the API asks for. Standard output carries only the command's result; errors and notices go to standard error, as error: CODE: MESSAGE (request REQUEST_ID) for an API error. Quote the request identifier when you contact support.

Exit codes

Code Meaning
0 Success
1 Other failure: network, file permissions, unexpected response
2 Invalid command line
3 No API key, or the key is invalid or not allowed (401, 403)
4 Not found (404)
5 Payment required: the subscription does not include this (402)
6 Rate limited after the retries (429)
7 Invalid request (400, 409, 422)

Updates

An installed cr keeps its previous version, so updates are reversible offline:

cr update --check          # compare with the latest signed release
cr update                  # install it
cr update --version 0.2.0  # choose a version, including a downgrade
cr update --rollback       # return to the previous version, without network access
cr update --bundle DIR     # install a downloaded release directory

On a terminal, cr mentions on standard error when a newer release exists; cr update --notifications off disables this. Automatic updates are off by default: cr update --automatic on registers an hourly job for your user (launchd on macOS, a systemd user timer on Linux, Task Scheduler on Windows); --automatic status, off and uninstall manage it. An update never interrupts a running command: while one runs, activation is deferred to the next attempt. Set CONTROLRESELL_DISABLE_AUTOUPDATE=1 to turn off release checks and automatic updates for a process.

Releases are downloaded through the GitHub API (api.github.com/repos/controlresell/cli/releases/assets/<id>), without a GitHub login. GitHub limits unauthenticated API requests to 60 per hour per IP address: cr update --check costs 3 requests (the release, manifest.json, manifest.sig) and cr update 4 (plus the binary); the hourly automatic job costs 5 (its check, then the tag lookup and the binary). When the limit is reached, the command fails with release_rate_limited and says when it resets. Other failures keep a distinct code (release_timeout, release_dns_failure, release_tls_failure, release_proxy_failure, release_connection_failure, release_connection_closed, release_response_invalid, release_write_failure, release_network_failure) and print a short cause.

cr verify-release --manifest manifest.json --signature manifest.sig [--binary FILE --target TARGET] verifies a downloaded release by hand, without login or network access.

Build from source

Use JDK 21 and the checked-in Gradle wrapper. Until the Kotlin SDK is on Maven Central, publish it to your local Maven repository first:

scripts/publish-sdk-locally.sh
./gradlew :cli:jvmTest
./gradlew :cli:linkReleaseExecutableMacosArm64   # or LinuxX64, LinuxArm64, MingwX64
python3 scripts/smoke-native.py --binary cli/build/bin/macosArm64/releaseExecutable/cr.kexe

smoke-native.py runs the native executable against a local fake of the public API (scripts/fake-api.py), with an isolated configuration directory and synthetic keys. Tests never call the real API.

SmokeFakeApiContractTest (part of :cli:jvmTest, so of every pull request) decodes every payload the fake serves (python3 scripts/fake-api.py --dump) with the pinned Kotlin SDK models, so a drift between the fake and the SDK fails the PR instead of the native smoke.

Pull requests run the JVM tests. Native tests, signed bundles and installation acceptance on all four targets run on version tags and on a manual full validation; publishing a release is a separate, manual step.

Security and contributing

See SECURITY.md to report a vulnerability, and CONTRIBUTING.md before opening a pull request.

License

Apache License 2.0. See LICENSE.

About

Command-line interface for the ControlResell public API

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages