Skip to main content
Use the mio CLI for creator-side automation, scripted admin workflows, and quick API exploration from a terminal.

Install

On macOS or Linux, install the latest released binary with the official installer:
The installer detects your OS and CPU architecture, downloads the matching GitHub release asset, verifies the SHA-256 checksum when checksum tooling is available, and installs mio into /usr/local/bin. Install somewhere you own with PREFIX:
Pin a release with VERSION:

Update

Release installs can update themselves from the CLI:
mio update reruns the official release installer into the directory containing the current executable. Use --version to pin a release, or --prefix to install into another directory:
If you installed from source with go install, update through Go instead:
mio update also keeps the agent skill in sync: the new binary refreshes each unmodified copy to the version it bundles, and leaves a hand-edited copy untouched. Which copies it refreshes depends on the release that runs the update:
  • From v0.24.0 (the release after v0.23.0): the user copies (~/.claude/skills/mio/SKILL.md, and $CODEX_HOME/skills/mio/SKILL.md for Codex), plus the project copies in the current directory (./.claude/skills/mio/SKILL.md, ./.codex/skills/mio/SKILL.md). Run mio update from a project to refresh that project’s copy; no other directory is searched. Every refresh warning, including the one naming a hand-edited copy and the mio skills install … --force command that would overwrite it, goes to stderr.
  • On v0.23.0 and earlier: the user copies only. A project copy is never refreshed.
The old binary decides which copies to refresh, so updating from v0.23.0 to v0.24.0 still leaves project copies at their old version. Refresh each one once, from its project directory:
That refreshes an unmodified copy. A hand-edited one needs --force added, which overwrites your edits. From v0.24.0, mio skills install also warns on stderr when the other scope holds an older copy of the skill for the same agent, and names the command that refreshes it.

Install the agent skill

If you drive mio from a coding agent (Claude Code or Codex), install the bundled agent skill so the agent knows the CLI-only hub recipe and the render-contract traps before it starts calling commands:
The skill is embedded in the mio binary, so it never needs a separate download and mio update always ships the current version. The install is idempotent and version-stamped: re-running is a no-op, an unmodified install is refreshed in place, and a skill you hand-edited is never overwritten unless you pass --force. Inspect or pipe the skill body with mio skills print.

Authenticate

Interactive users can log in once:
For scripts, CI, and agents, set a team API key:
The CLI reads credentials in this order:

Where the stored key lives

mio login never writes the key to the config file. Where it goes depends on the platform and on how mio was built: The encrypted file lives under the config directory, so a shell or agent whose XDG_CONFIG_HOME or HOME differs from the one you logged in from reads a different, empty store. A command that finds no key exits 3, and its error names the store it looked in:
mio whoami reports where the key it used came from as key_source: flag (--api-key), env (MIO_API_KEY), or for a stored key the store that held it — file keyring (<path>), keychain, secret service, kwallet or wincred. The one other value, none (--anonymous), is what --anonymous resolves to, but whoami prints its output only after the API accepts GET /api/v1/auth/me, which needs a credential, so against the Membership.io API mio whoami --anonymous exits 3 instead.

Export the stored key for a session

To hand the stored key to a script or agent session once, instead of every command reading the store:
mio auth token prints the stored key, and nothing else, to stdout. It never prints the key from --api-key or MIO_API_KEY: if MIO_API_KEY is already set to a different key, it still prints the stored one and adds a note on stderr, because the environment key takes precedence everywhere else. It sends no request and does not check that the key is still valid — run mio whoami for that. With no stored key it exits 3 with empty stdout, and its error names the store it looked in, as above. Keep the assignment and the export as two statements: export MIO_API_KEY="$(…)" returns export’s own status, 0, so set -e never sees the exit 3 and the script carries on with an empty key. mio auth token, the store named in the exit-3 error, and the per-store key_source values arrived in v0.23.0. On v0.22.0 and earlier, mio auth token exits 2 (unknown command "auth" for "mio"), and every earlier release that has whoami reports a stored key as keychain, whatever actually holds it.

Moderation with an API key

By default a team API key cannot run hub moderation — ban/unban/warn/soft-ban a member, resolve reports, or remove/restore content all return 403 for a key. A key gains that power only when it is explicitly granted the can_moderate flag:
Both routes require a real user (JWT) session — a key can never grant itself moderation power, and the flag defaults to off. Once granted, the key can drive the moderation console for hubs under its team, and every key-driven moderation action carries the acting key’s api_key_id so it is never attributed solely to the key’s creator — on the moderation audit log, on the durable moderation events, on the banned-members list (banned_by_api_key_id), and on every report the action auto-dismisses. Member-management (adding members, role changes) and report-reason management already work with any team key and do not require can_moderate. Team-security routes (roles, api-keys, verified domains, providers, OAuth clients) remain web/JWT-only.

API Base

The CLI talks to the production API at https://api.member.dev by default. Override it per command with --api-base <url> or globally with MIO_API_BASE_URL. Current API requests are sent to /api/v1 paths. The backend still accepts older /api paths as a compatibility alias during the alpha period, but /api/v1 is the documented contract.

Output for scripts

Off a terminal the CLI prints JSON. --jq filters it, and -o plain prints a string without JSON quotes, so capture ids with both: ID=$(mio pages create … -o plain --jq .id). From v0.24.0 (the release after v0.23.0):
  • -o plain --jq prints one bare value per line, whatever the count. A result that is a stream or an array of strings, numbers or booleans prints one value per line, and a filter that matches nothing prints nothing, so a loop works the same for 0, 1 or 20 ids. A string that itself contains a newline, such as a policy’s content, spans several lines; use -o json for those. Objects still print as key=value blocks.
  • A list that has more rows says so on stderr. A list command returns one page, usually 20 rows. When the API reports more, the CLI prints one note: line on stderr with the flag and cursor that fetch the next page, and leaves stdout unchanged:
    Paste the cursor as printed: it is single-quoted when a shell would split it. segments search names --page-after instead, and media search, which returns no cursor, suggests a larger --limit. The media lists send only a next-page link, so their note says a link came and the next page may be empty. The four checkout orders|subscriptions|payments|webhooks list commands report nothing, so their note appears on any full page, says more rows may exist, and offers the last row’s id as the cursor. --raw prints no note, because its output carries the API’s own meta and links.
On v0.23.0 and earlier, -o plain --jq prints two or more values as value=X records separated by blank lines (one value prints bare, and no match prints an empty line), and nothing reports that a list stopped at one page: read meta and links under --raw.

Build a hub end to end

The CLI can author a full, render-faithful hub — hub branding, menus, discussion spaces, playlists, media, and the homepage tree — without touching the raw API. Hub authoring is worth doing through the CLI because it validates blob keys, typed menu items, and hub-scoped hrefs before firing a request, and it surfaces the silent render-contract traps that a bare 200 hides. See Create a hub for the ordered recipe, and Media workflow for the media command group.

Contact id vs. contact_id (the id-namespace trap)

mio surfaces two contact identifiers, and mixing them up 404s a live contact:
  • The contacts verbs — plus contact-attributes and tags — operate on the team-contact id, surfaced as .id (route param {team_contact_id}).
  • The member-shaped verbs operate on the global contact id, a separate field surfaced as top-level .contact_id (.data.attributes.contact_id under --raw on contacts retrieve). These are hub-memberships (add / set-role / ban / unban / warn), activity contact, community members (ban / unban / warn / soft-ban), email enrollments (create, list-by-contact), and access-rules overrides create.
Piping a .id into a {contact_id} route 404s. Read the global id from the flattened output first:
The CLI appends a hint to the resulting exit-4 error. From v0.24.0 the hint gives the capture above. On v0.23.0 and earlier it names .attributes.contact_id, which is where the field sits in the API’s response; as a --jq path it prints nothing, so capture .contact_id as shown above.

Recently fixed

These commands regressed or were incomplete and now work as documented:
  • media files update (previously returned 400).
  • tags assign by tag id (--tag-id) and by name/slug (--tag).
  • contact-attributes values set / get, and contact-attributes hub-config create. Hub-config gained per-hub flags --in-profile, --in-onboarding, --required, --read-only, and --searchable (replacing the old --visible). values set now routes each value into the attribute’s typed field (number/boolean/date/text) instead of always sending value_text, so number, boolean, and date attributes are settable from the CLI; an unknown slug or a value that doesn’t parse for its type is rejected up front with no write.
  • content reorder.
  • automations test (dry-run enrollment, no side effects).