The official command-line interface for Membership.io.
mio is agent-first: JSON output by default when piped, environment-variable auth, and stable exit codes so scripts and AI agents can branch deterministically.
- Prebuilt binary / curl install: none — the binary is self-contained and statically linked.
go installor building from source: Go 1.25+ (the module targets Go 1.25).
mio is released. Pick whichever you prefer.
curl -fsSL https://raw.githubusercontent.com/Searchie-Inc/mio-cli/main/scripts/install.sh | shDetects your OS/arch, verifies the SHA-256 checksum, and installs mio into /usr/local/bin (you'll be asked for sudo). Install somewhere you own instead with PREFIX:
curl -fsSL https://raw.githubusercontent.com/Searchie-Inc/mio-cli/main/scripts/install.sh | PREFIX="$HOME/.local/bin" sh
# then make sure that dir is on your PATH, e.g. add to ~/.zshrc:
# export PATH="$HOME/.local/bin:$PATH"Pin a specific version with e.g. VERSION=0.2.0.
Download the archive for your OS/arch from the latest release, extract it, and put mio on your PATH:
- macOS / Linux:
mio_<version>_<os>_<arch>.tar.gz - Windows:
mio_<version>_windows_amd64.zip
where <version> is the release tag, e.g. 0.2.0.
checksums.txt is published alongside for verification.
go install github.com/Searchie-Inc/mio-cli@latestNote:
go installnames the binarymio-cli(after the module path), notmio, and installs it to$(go env GOPATH)/bin(make sure that's on yourPATH). Rename it if you like:mv "$(go env GOPATH)/bin/mio-cli" "$(go env GOPATH)/bin/mio"Or just use the curl / prebuilt-binary install above for a ready-to-go
mio.
brew install searchie-inc/tap/mio isn't enabled yet (the tap is being provisioned). Use the curl installer or a prebuilt binary for now.
git clone https://github.com/Searchie-Inc/mio-cli
cd mio-cli
go build -o mio .
./mio versionVerify any install:
mio version # → mio 0.2.0 (commit …, built …)Update a release install later from the CLI:
mio update # latest release
mio update --version 0.2.1mio update reruns the official installer into the directory containing the
current executable. Use --prefix <dir> to install somewhere else.
# 1. Install (see above) — e.g. the curl one-liner.
# 2. Authenticate (pick one)
mio login # interactive — stores a key in your OS keychain
# …or, for scripts / CI / agents:
export MIO_API_KEY=mio_sk_live_xxxxx # no login step needed
# 3. Confirm setup
mio whoami # prints resolved user, team, hub, api-base, key source
# 4. You're ready
mio contacts listAuth works against https://api.member.dev by default. Point elsewhere with --api-base <url> or MIO_API_BASE_URL (see Authentication).
If you belong to multiple teams, mio login will prompt you to pick one. You can switch later with mio teams switch <team-id>, which updates your local context. For single-team accounts the team is resolved automatically — no manual config step needed.
Run mio login and choose one of two paths:
- Paste an existing API key — a
mio_sk_live_…key, validated against the API and stored. - Email + password — logs you in (JWT), then auto-mints an API key named
mio-cli@<host>on your team and stores it. Your password is never saved — only the minted key is kept.
The key is stored in your OS keychain (with an encrypted-file fallback on headless/CI environments where no keychain is available). mio logout deletes it.
Don't have an account yet? mio register creates one and logs you in in a single step — it mints and stores a key just like mio login, so you end up authenticated as the new account (this replaces any key already stored).
# Interactive: prompts for email, password (typed twice), optional name
mio register
# Headless (CI / scripting)
mio register --email you@example.com --password 's3cr3tpass' --first-name Ada --last-name Lovelace
# …or via env vars
MIO_EMAIL=you@example.com MIO_PASSWORD='s3cr3tpass' mio registerYour password is never echoed or saved. --first-name/--last-name are optional; email-format and password rules (min 8 characters) are enforced by the API.
Skip mio login entirely — just set the environment variable:
export MIO_API_KEY=mio_sk_live_xxxxxThe key is read in this order (first match wins):
--api-key <key> → MIO_API_KEY env → key stored in the OS keychain
mio --api-base https://api.staging.membership.io contacts list
# or
export MIO_API_BASE_URL=https://api.staging.membership.iomio is fully self-documenting:
mio --help # top-level: all resources + global flags
mio <resource> --help # e.g. mio contacts --help — actions for a resource
mio <resource> <action> --help # e.g. mio contacts create --help — flags for an action
mio gen-docs --dir ./docs # generate the full Markdown reference (one file per command)When in doubt, append --help to any command.
# Confirm auth + active team
mio whoami
# Contacts
mio contacts list
mio contacts create --email alice@example.com --first-name Alice --last-name Smith
mio contacts retrieve <id>
mio contacts update <id> --last-name "Jones"
mio contacts delete <id> --yes
# Hubs
mio hubs list
# A new hub is PRIVATE by default. In table mode create prints the slug and how to
# publish on stderr; output also carries a derived `published` (= !is_private).
# No public URL is echoed — the API returns no domain/url field, so the CLI cannot
# derive one; combine the slug with your hub-frontend host.
mio hubs create --name "My Hub" --slug my-hub
mio hubs create --name "My Hub" --slug my-hub --published # publish immediately
# Author the presentation layer at create time (opaque JSONB blobs; inline JSON or @file).
# --logo-url and --favicon-url merge into --branding-json rather than replacing it.
mio hubs create --name "My Hub" --slug my-hub \
--logo-url https://cdn.example.com/logo.png \
--favicon-url https://cdn.example.com/favicon.ico \
--branding-json '{"primary":"#6747E3","heading_font_size":32}' \
--navigation-json @nav.json \
--settings-json '{"policies":{"enabled":true}}' \
--meta-json '{"discussions":{"enabled":true}}'
# retrieve surfaces derived registration_enabled + published booleans.
mio hubs retrieve <hub-id>
# Enable member self-registration (settings.registration.enabled) — read-modify-write,
# sibling settings keys are preserved. Pass =false to disable explicitly.
mio hubs update <hub-id> --registration-enabled
mio hubs update <hub-id> --registration-enabled=false
# Set the favicon on an existing hub (branding.favicon_url; RMW-merges into branding).
mio hubs update <hub-id> --favicon-url https://cdn.example.com/favicon.ico
# DELETE a key from a blob. The *-json flags are merge-only (a null persists as
# literal null, {} is a no-op); --unset is the only real delete. The first dotted
# segment selects the blob (branding/settings/meta); nested paths and comma-lists
# and repeats are supported. Applied AFTER --*-json merges and scalar flags.
mio hubs update <hub-id> --unset settings.registration.enabled
mio hubs update <hub-id> --unset branding.favicon_url,settings.customCss
# Author the header/footer menu on an existing hub. Each item needs a "type"
# (url|page|playlist|discussions) — untyped items are rejected. Whole-blob replace.
mio hubs update <hub-id> --navigation-json '{"header":[{"type":"url","label":"Home","href":"/my-hub/","position":0}]}'
# Products & prices (the product id is a positional argument, not a flag)
mio products list
mio products create --name "Pro Plan" --description "Full access"
mio products prices list <product-id>
mio products prices create <product-id> --amount 4900 --currency usd --interval month
# Segments — preview who matches a condition tree (does not save)
mio segments search --conditions '{"version":1,"groups":[{"logic":"AND","conditions":[{"type":"email","operator":"contains","value":"@example.com"}]}]}'
mio segments members <segment-id>segments search takes the full condition tree (matching the backend write shape), not a flat list. You can also read it from a file and paginate:
mio segments search --conditions @conditions.json --page-size 50 --page-after <cursor>Every command inherits these flags.
| Flag | Short | Default | Description |
|---|---|---|---|
--api-key |
env/keychain | API key. Overrides MIO_API_KEY and the stored key. |
|
--team |
config | Team ID for team-scoped resources. | |
--hub |
config | Hub ID for hub-scoped resources. | |
--output |
-o |
json (piped) / table (TTY) | Output format: json, table, plain. |
--jq |
Filter JSON output with a gojq expression. | ||
--raw |
false | Emit the raw JSON:API envelope instead of the flattened resource. | |
--yes |
-y |
false | Skip confirmation prompts on destructive operations. |
--profile |
default | Named config profile. | |
--api-base |
config | Override the API base URL (MIO_API_BASE_URL). |
By default, mio flattens JSON:API responses into plain resource objects. Use --output to control format:
# Pipe-friendly JSON (also the auto-default when piped)
mio contacts list --output json
# Aligned table for humans
mio contacts list --output table
# key=value pairs (great for grep)
mio contacts retrieve <id> --output plain
# Raw JSON:API envelope (meta, links, included)
mio contacts list --raw
# Extract a field with jq
mio contacts retrieve <id> --jq '.email'
# Pluck IDs from a list
mio products list --jq '.[].id'
# Chain into shell variable
HUB_ID=$(mio hubs list --jq '.[0].id')
mio content list --hub "$HUB_ID"Scripts and agents can branch on these stable codes.
| Code | Constant | Meaning |
|---|---|---|
0 |
— | Success |
1 |
ExitGeneric |
Unexpected / generic error |
2 |
ExitUsage |
Bad flags, missing required argument, or rejected input (400/409/422) |
3 |
ExitAuth |
Missing or invalid credentials (HTTP 401/403) |
4 |
ExitNotFound |
Resource not found (HTTP 404) |
5 |
ExitNeedsConfir |
Destructive op in a non-interactive shell — pass --yes |
6 |
ExitRateLimited |
Rate limited (HTTP 429) |
7 |
ExitServer |
Upstream server error (HTTP 5xx) |
| Resource | Available actions |
|---|---|
login / logout |
Interactive auth |
register |
Create a new account (email + password, optional first/last name) and auto-login — interactive, or headless via --email/--password (or MIO_EMAIL/MIO_PASSWORD). Replaces any stored key. |
whoami |
Print resolved identity — user, team, hub, api-base, profile, key source |
config |
set, get, list |
api-keys |
create, list, retrieve, delete |
teams |
create, list, retrieve, update, delete, switch (server-side switch + updates local context); members list/add/remove |
users |
me, list, retrieve, update |
roles |
create, list, retrieve, update, delete; permissions list |
hubs |
create, list, retrieve, update, delete; policies update; scaffold (one-command full-experience hub from a template), templates (list templates) |
contacts |
create, list, retrieve, update, delete, restore |
contact-attributes |
create/list/retrieve/update/delete defs; options sub-group; hub-config sub-group; values get/set |
tags |
create, list, retrieve, update, delete, assign, assign-bulk, remove |
segments |
create, list, retrieve, update, delete, search, members, count |
content |
create, list, retrieve, children, update, delete, restore, reorder |
pages |
create, list, retrieve (add --tree for raw published node tree), update, delete, home, publish (requires --if-match <draft_version>); tree get/set (author a page's draft node-tree; set takes --file + optional --if-match — omit it for the first tree on a draft-less page, it defaults to 0); sections create/list/update/delete/reorder |
media files |
list, retrieve, durable-url (non-expiring hub-scoped image URL for page trees), update, delete; ingest from the CLI: upload (create → presigned S3 PUT → finalize; auto-multipart), replace, finalize, transcode, register-synthetic; cards get/set, chapters get/set |
media folders |
list, create, retrieve, update, delete, move (--parent-id/--to-root) |
media search |
hybrid search over the team's transcripts (--query, --hub-id, --limit) |
media playlists |
list, create, retrieve, update, delete, set-cover (--file-id); items add/list/remove/reorder — populate a playlist with media files (--playlist-id, --file-id, --position) |
media hub-media |
publish, list, unpublish — publish standalone files to a hub (--file-id) |
media hub-playlists |
publish, list, unpublish — publish playlists to a hub (--playlist-id) |
media attachments |
list, show, update, delete |
media transcripts |
get, vtt, content, versions, edit, revert |
products |
create, list, retrieve, update, delete; prices create/list/retrieve/update/delete |
checkout |
orders list/retrieve; subscriptions list/retrieve/cancel; payments list/retrieve/refund; webhooks list/retrieve/replay; accounts list/retrieve/onboarding-link; stripe-sync import/import-status/adopt-product |
email |
drip-campaigns create/list/retrieve/update/delete/activate/pause (create/update now accept --enrollment-mode, --trigger-event-type, --segment-id, --segment-check-interval-minutes, --allow-reenrollment); steps create/list/update/delete; templates create/list/retrieve/update/delete/preview; config set/get/delete/test; enrollments list/exit; stats get |
access-rules |
rules create/list/retrieve/update/delete; overrides create/list/retrieve/update/delete |
activity |
contact, top-engaged |
Run mio <resource> --help or mio <resource> <action> --help for flag details on any command, or mio gen-docs --dir ./docs to generate the complete reference.
mio stores its config as TOML at ~/.config/mio/config.toml (or $XDG_CONFIG_HOME/mio/config.toml when XDG_CONFIG_HOME is set). It holds non-secret context only — current team/hub, API base, and named profiles. Your API key is never written here; it lives in the OS keychain (managed by mio login / mio logout).
Manage it with the config command (writable keys: team, hub, api-base):
mio config set team <team-id>
mio config set hub <hub-id>
mio config set api-base <url>
mio config get team
mio config list # shows all values and the config file pathMultiple named profiles are supported via --profile.
| Symptom | Likely cause | Fix |
|---|---|---|
Exit code 3 (ExitAuth) |
No key, or the key is invalid/expired | Run mio login, or export MIO_API_KEY=mio_sk_live_…. Run mio whoami to confirm the resolved key and active team. |
Exit code 2 (ExitUsage) |
Bad flag, missing argument, unknown subcommand, or rejected input | Check your flags against mio <resource> <action> --help. Flags are kebab-case (--first-name). |
Exit code 5 (ExitNeedsConfir) |
A destructive op (delete, cancel, refund, or a member-moderation verb: ban/unban/warn/soft-ban, moderation reports resolve) in a non-interactive shell |
Re-run with --yes / -y. |
Exit code 6 (ExitRateLimited) |
Too many requests (HTTP 429) | Back off and retry. |
Exit code 7 (ExitServer) |
Upstream 5xx | Transient — retry; if it persists, the backend is down. |
| Calls fail against production | Wrong API base or stale/revoked key | Verify with mio whoami. Point at a different backend with --api-base <url> or MIO_API_BASE_URL. |
Errors are rendered TTY-aware: on an interactive terminal you get a friendly one-line Error: <detail> plus a dimmed (exit code N); when stderr is piped or otherwise non-interactive you get the machine-readable JSON:API errors array with the exit code echoed in meta.exit_code. Either way the process exit code is the same, so scripts can branch on the exit code (and, off a TTY, on the body).