Skip to main content
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
Recommended:
This installs 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:
If pipx warns that its bin directory isn’t on your PATH, run pipx ensurepath once and open a new shell. Alternative — a plain virtualenv:
This does not put 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

This opens a browser for a MerchantOps sign-in — you’ll need an existing MerchantOps account and organization; ask whoever administers your organization’s MerchantOps account for access if you don’t have one. Use --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:
For unattended or CI use, set the 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 released merchantops-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

You are not in the clone’s root. Both -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.
The install put 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.
The CLI does not ship its OAuth client id. It fetches the one for the environment you selected from the API itself, over an unauthenticated discovery endpoint. If that API does not publish one yet, mo tells you so and names the two manual overrides below, rather than guessing. Every setting resolves in this order, first hit wins:
A preset deliberately outranks the environment variables: --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.
The stored token is keyed by the API URL, so switching environments asks you to sign in again rather than silently presenting one environment’s token to another.
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:
  1. --token <bearer> — for one-off debugging only. A command-line argument is visible in the process list on a shared machine.
  2. MERCHANTOPS_TOKEN — a bearer token, used as-is and never refreshed. This is the credential for CI and containers.
  3. MERCHANTOPS_API_KEY — a server API key.
  4. The keychain token written by auth login — the only one that refreshes.
The last two environment options exist so the CLI needs no keychain at all in CI or inside a container. A machine with no keychain gets an ordinary message, not a crash.
Your token is never logged, never written into a receipt file, and never included in an error message.

Catalog commands

Every API operation is available as mo api <tag> <command>:
  • Output is JSON on stdout, human-readable progress on stderr — so you can pipe stdout straight into jq without 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:
--execute is misfire prevention — it stops a casually-typed command. It is not a permission boundary. The real boundary is your own role: the API checks your permissions on every request, whatever asked. See Team and roles.

Request bodies

Files in and out

A few operations move files rather than JSON:
  • --file PATH uploads a real file on the multipart endpoints, such as products upload-base-products-xlsx.
  • --out PATH writes 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.
Reads of those areas are still available: listing your members or integrations during an onboarding is fine; changing them from the CLI is not. Approve and publish a batch in the dashboard, where the full review context is in front of you.

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:
Run mo import order to get that order as JSON, so no script has to hardcode it.

Validate, plan, then load

1

Validate — entirely local

Checks schema and cross-references against the organization’s live data, and sends no writes. Prints whether the file is valid, how many rows it has, a sha256, and every error and warning.
2

Plan — reads the server, writes nothing

Validates, then compares against what already exists, and reports what would be created, updated, skipped, and what conflicts.
3

Load

Without --execute, load is exactly plan.
4

Or load a whole bundle in order

Validates every present file first, then loads them in the safe order. A slot you did not fill is reported as absent, never as an error.
Available artifacts: 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.
Copy the printed value. Do not re-derive it yourself: a single file, a whole bundle and a provisioning plan each hash different things, so a hash you computed by hand will not match.
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.
Load the catalog first, review the dictionary, then enrich. See how enrichment works.

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.
On a bundle, --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.
Branch on the job’s 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.
Exit 8 is the one to design around: it always means the CLI stopped before contacting the API, so nothing changed and a retry is free.

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.