mo is a command-line client over the MerchantOps API. You authenticate once
with OAuth 2.0 (PKCE) — a browser sign-in that stores tokens in your OS
keychain — and the CLI then rides the same authenticated, organization-scoped
API as the dashboard. It holds no database credentials and no service secrets:
every request carries your own token and is bounded by your own roles.
It covers three kinds of work:
- Catalog reads and writes — derived from the API itself, so a new endpoint is reachable as soon as the CLI is updated.
- Onboarding and bulk imports — validate a set of catalog files locally, preview exactly what would change, then load them in a safe order.
- Job watching — follow a long-running job to completion in a script.
Install
Prerequisite: Python 3.11 or newer.- macOS:
brew install python@3.11, or the installer from python.org - Linux (Debian/Ubuntu):
sudo apt install python3.11 - Windows: the installer from python.org
mo (and mo-admin, see below) into an isolated environment and puts both on
your PATH automatically. If you don’t already have uv, see
Installing uv — it has a one-line
installer for macOS, Linux and Windows.
Alternative — pipx:
pipx warns that its bin directory isn’t on your PATH, run pipx ensurepath once and
open a new shell.
Alternative — a plain virtualenv:
mo on your PATH by itself. Either run it by its full path
(~/.venvs/mo/bin/mo), or add the venv’s bin directory to your PATH — for example, by
adding export PATH="$HOME/.venvs/mo/bin:$PATH" to your shell profile (~/.zshrc,
~/.bashrc, or your shell’s equivalent) and opening a new shell.
Verify the install with:
First run
--env qa instead of --env prod if you’re working against a test
organization.
Once logged in, confirm the CLI can reach the API as you:
MERCHANTOPS_TOKEN environment variable to a bearer token
instead of logging in interactively.
Two of the onboarding skills — merchantops-onboard-from-site and
merchantops-competitive-analysis — additionally need Node.js, the firecrawl-cli package, and
your own Firecrawl API key, under your own key and budget. Installing merchantops-cli installs
none of those; they’re needed only if you run those two skills.
From source (MerchantOps-supported customers and contributors)
Install from source only if you work against an unreleased engineering build rather than the releasedmerchantops-cli — MerchantOps-supported customers
with access to the engineering repository, and contributors to the CLI itself.
Everyone else should use the published package above. Ask your MerchantOps
contact for access, then run this from the clone’s root:
Troubleshooting the install
is not a valid editable requirement
is not a valid editable requirement
-e paths in that command are
relative to your current directory, so it only works from the directory
that contains merchantops-sdk and merchantops-cli. cd into the clone’s
root and run it again.mo is not found after installing
mo is not found after installing
mo somewhere that isn’t on your PATH. After pipx, run
pipx ensurepath once and open a new shell. After a plain virtualenv, call
~/.venvs/mo/bin/mo by its full path or add that bin directory to your
PATH. uv tool install does this for you.Configure
--env local|qa|prod selects an environment preset that bundles three settings
together — the API base URL, the OAuth issuer, and the API audience — so an
environment is normally the only thing you pick:
--env prod points at https://api.merchantops.ai and --env qa at
https://api-qa.merchantops.ai; --env local points at an API you run
yourself. Picking an environment sets the issuer and the audience along with the
URL, so you never set them one at a time.
Ask your MerchantOps contact which environment to use and, if your organization
is served from a different endpoint, what to set
--api-url to. The audience is
an identifier, not necessarily a URL you can open; it has to match exactly what
the API accepts.mo tells you so and names the
two manual overrides below, rather than guessing.
Every setting resolves in this order, first hit wins:
--env prod is an
explicit instruction to talk to production, and a stale MERCHANTOPS_API_URL
left in a shell must not silently redirect it.
Sign in
auth login --no-browser prints the authorization URL instead of opening a
browser — useful over SSH.
auth status always exits 0 — “not logged in” is a fact, not a failure. It
never refreshes and never validates the token.
auth whoami is a live call, not a token decode: your roles are never
carried inside the token, so the CLI asks the API. Three outcomes are normal:
Which credential is used
Resolved in this order, first hit wins:--token <bearer>— for one-off debugging only. A command-line argument is visible in the process list on a shared machine.MERCHANTOPS_TOKEN— a bearer token, used as-is and never refreshed. This is the credential for CI and containers.MERCHANTOPS_API_KEY— a server API key.- The keychain token written by
auth login— the only one that refreshes.
Catalog commands
Every API operation is available asmo api <tag> <command>:
- Output is JSON on stdout, human-readable progress on stderr — so you can
pipe stdout straight into
jqwithout stripping anything first. - Path parameters are arguments; query parameters are options.
- A failure still prints JSON on stdout — an object carrying the error, the HTTP status, the detail, and the exit code.
Nothing is written without --execute
Every command that is not a plain read requires --execute. Without it, the
CLI prints the request it would send and sends nothing:
Request bodies
Files in and out
A few operations move files rather than JSON:--file PATHuploads a real file on the multipart endpoints, such asproducts upload-base-products-xlsx.--out PATHwrites a download to disk and prints a small summary — used by the product, pricing, product-type and property-definition exports.--out -writes the raw bytes to stdout instead, the one case where stdout is not JSON.
What mo does not carry
mo registers 295 of the 386 operations the API exposes across 34 tags. The
other 91 are absent from mo entirely — not hidden, not permission-gated,
simply not registered. Running one is an ordinary unknown-command error. They
are:
- every delete, and the batch-delete and batch-archive families;
- anything that changes members, integrations, API keys or billing;
- anything that changes organization settings;
- approving, publishing, promoting or republishing a price or catalog batch — the steps that can put changes in front of customers.
Onboarding
onboarding status reports the organization’s current shape — how many property
definitions, product types, categories, display groups, brands, products,
variants and price records exist, and which system properties are missing. It is
the first thing to run before proposing any schema, because it is the difference
between designing a property dictionary
from scratch and extending the one already in place.
onboarding snapshot writes the current schema out in the same bundle layout
described below, so “what you have” and “what you propose” can be compared with
an ordinary diff. onboarding template writes a header-only CSV or skeleton
JSON for one artifact and makes no network call at all.
Imports
mo import loads catalog data from files. The bundle layout and the safe load
order are the same thing — properties before the types that use them, types
before the products that reference them:
mo import order to get that order as JSON, so no script has to hardcode it.
Validate, plan, then load
1
Validate — entirely local
sha256, and every error and warning.2
Plan — reads the server, writes nothing
3
Load
--execute, load is exactly plan.4
Or load a whole bundle in order
property-definitions, property-values,
property-overrides, categories, product-types, type-properties,
brands, products, products-base, variants, prices.
Validation is the real safety net. The import endpoints have no server-side
dry-run, and a bulk load is not a transaction — the server enforces each row’s
rules only after it has written the rows before it.
validate re-checks those
rules on your machine, before anything is sent. When it cannot read a
cross-reference to check it against, it says so rather than reporting a pass.--confirm — copy the printed value
validate and plan print a sha256. Pass that exact value back to
load --execute --confirm <value>, and the CLI re-computes the hash and refuses
with exit 8 — having sent nothing — if it no longer matches.
This is what makes “load exactly the files that were reviewed” enforceable.
Editing, adding or removing any file in a bundle after it was reviewed
invalidates the hash.
Enrichment is off by default
The API enriches uploaded products by default.mo import load turns that
off unless you pass --enrich, for two reasons:
- Enrichment is metered and billed per product, and a 5,000-product load would bill 5,000 enrichments before you have a property dictionary worth enriching against.
- With enrichment on, the import’s “skip unchanged rows” behavior does not apply, so re-running a load mints a new version of every product.
Resuming an interrupted load
Some data can only be inserted once — categories, brands, the standalone variants CSV and prices.import load writes a .receipt.json next to the file
recording what succeeded, and prints its path before the first call, so a run
that dies still tells you where its record is.
--resume skips what the receipt already records as loaded.
The receipt is not the safety net — live server state is. If the receipt is
missing but the target already holds rows this file would insert,
plan and
load refuse and name the collision. --force overrides only that specific
refusal; it never overrides the --confirm check and never overrides a
validation error.--stop-on-error halts at the first file with a failed row;
without it the walk continues and the summary lists every file’s outcome. Either
way, a halted bundle is resumable.
Prices
mo import load prices sends prices in as drafts in an automatically created
price batch, grouped so that one load produces one
reviewable batch per effective date. Nothing goes live until a human approves
that batch in the dashboard.
MAP policies are not loadable through mo import. Use the
lakehouse document flow for those.
Watching a job
Bulk operations run in the background as jobs.mo jobs watch
polls one to completion, streaming progress to stderr and printing the final job
JSON on stdout.
status, not on the exit code alone:
awaiting_review means the run finished but items need a human decision, so
polling stops. The job carries a count of the items waiting for you. See
reviewing results.
A transient blip while polling — a 5xx, a dropped connection, a timeout — is
retried until --timeout elapses. An authentication, permission or not-found
error stops immediately.
Exit codes
Exit codes are a stable contract, so a script can decide what to do next without parsing any text.mo-admin
The same package also installs mo-admin: the same client with nothing withheld
from its command tree, for human operators at a terminal who need an operation
mo does not register. The difference between the two scripts is which commands
exist in the tree, not what you are allowed to do — every call from either one is
still bounded by your own permissions on the MerchantOps API, resolved
server-side from your own credential, exactly as the dashboard is. Commands in
the destructive families additionally require --execute plus a typed
confirmation at your terminal before anything is sent.
Next steps
Skills and the Claude plugin
Drive these same workflows conversationally from Claude.
MCP server
Connect Claude to your catalog with no local install.
Server API keys
Long-lived credentials for CI and services you run.
Uploading products
The same imports, from the dashboard.