Shell CLI
Install the Cooper CLI with curl or npx.
One Node file calls the existing REST API. No SDK package is required. Markdown: /docs/cli.md · API docs · quickstarts.
Install
The Cooper CLI is one Node.js file (Node 18 or newer, no npm dependencies). Download it from https://cooperemail.com/cli/cooper.mjs, or run the npm tarball at https://cooperemail.com/cli/cooper-cli.tgz. Both URLs are on the apex host. www.cooperemail.com redirects those GETs to the apex.
The script calls the existing REST API. It does not add endpoints.
curl -fsSL https://cooperemail.com/cli/cooper.mjs -o cooper.mjs && node cooper.mjs help
npx -y https://cooperemail.com/cli/cooper-cli.tgz helpAuth
The key is read in this order: --api-key, the COOPER_API_KEY environment variable, then the credentials file. The base URL is --base-url, then COOPER_BASE_URL, then https://cooperemail.com. http is accepted only for localhost and 127.0.0.1.
onboard --save writes {"api_key","base_url"} to ${XDG_CONFIG_HOME:-~/.config}/cooper/credentials.json with mode 0600. The saved base_url is a record of where the key was minted. Later commands still use --base-url, COOPER_BASE_URL, or the default. The full key is printed only in the onboard response. Other commands do not print it. Requests send Authorization: Bearer and User-Agent: cooper-cli/1.0.0.
Quickstart in 4 commands
Save a key, list the inbox, send to that same address, then wait for the message to land.
node cooper.mjs onboard --username research-bot --save
node cooper.mjs inboxes list
node cooper.mjs send inb_example --to research-bot@cooperemail.com --subject "Hello" --text "Sent from the CLI."
node cooper.mjs wait inb_example --timeout 25Sample output
{
"account_id": "acc_example",
"api_key": "coop_live_example",
"api_key_id": "key_example",
"inbox": {
"id": "inb_example",
"username": "research-bot",
"email": "research-bot@cooperemail.com",
"display_name": "Research Bot",
"created_at": "2026-10-18T15:00:00.000Z",
"require_sender_auth": true,
"labels": [],
"expires_at": null,
"status": "active",
"paused_at": null,
"metadata": {},
"client_id": null,
"screening": "observe"
}
}onboard
POST /api/v1/onboard. --username is required. --display-name, --key-name, and --owner-email match the JSON body. --save stores the returned api_key.
node cooper.mjs onboard --username research-bot --display-name "Research Bot" --owner-email human@example.com --saveSample output
{
"account_id": "acc_example",
"api_key": "coop_live_example",
"api_key_id": "key_example",
"inbox": {
"id": "inb_example",
"username": "research-bot",
"email": "research-bot@cooperemail.com",
"display_name": "Research Bot",
"created_at": "2026-10-18T15:00:00.000Z",
"require_sender_auth": true,
"labels": [],
"expires_at": null,
"status": "active",
"paused_at": null,
"metadata": {},
"client_id": null,
"screening": "observe"
}
}me
GET /api/v1/me. The live body also includes workspace, lifecycle_emails, notices, whats_new, and owner_email_style. The CLI prints that body unchanged.
node cooper.mjs meSample output
{
"account_id": "acc_example",
"parent_account_id": null,
"auth_type": "api_key",
"key": {
"id": "key_example",
"name": "onboard",
"prefix": "coop_live_examp",
"inbox_id": null,
"permissions": null,
"expires_at": null
}
}inboxes list, create, get
GET /api/v1/inboxes, POST /api/v1/inboxes, and GET /api/v1/inboxes/{id}. List filters are --label, --status, --q, --limit, and --page-token. Create accepts --username, --display-name, --domain, --ttl-hours, --expires-at, --status, --metadata, --client-id, and --notify-url.
node cooper.mjs inboxes list --limit 20
node cooper.mjs inboxes create --username research-bot
node cooper.mjs inboxes get inb_exampleSample output
{
"object": "list",
"data": [
{
"id": "inb_example",
"username": "research-bot",
"email": "research-bot@cooperemail.com",
"display_name": "Research Bot",
"created_at": "2026-10-18T15:00:00.000Z",
"require_sender_auth": true,
"labels": [],
"expires_at": null,
"status": "active",
"paused_at": null,
"metadata": {},
"client_id": null,
"screening": "observe"
}
],
"next_page_token": null
}messages list and get
GET /api/v1/inboxes/{id}/messages and GET /api/v1/inboxes/{id}/messages/{message_id}. --unread, --labels, --limit, --before, --after, --from, --to, --subject, --direction, --thread-id, --ascending, --page-token, and --channel are query parameters when you set them.
node cooper.mjs messages list inb_example --unread --limit 20
node cooper.mjs messages get inb_example msg_exampleSample output
{
"object": "list",
"inbox_id": "inb_example",
"data": [
{
"id": "msg_example",
"inbox_id": "inb_example",
"subject": "Hello",
"preview": "Sent from the CLI.",
"labels": [
"unread"
]
}
],
"next_page_token": null
}send
POST /api/v1/inboxes/{id}/messages. --to, --subject, and --text or --html are required. --cc, --bcc, --reply-to, and --client-id go in the JSON body. --idempotency-key is sent as the Idempotency-Key header.
node cooper.mjs send inb_example --to ada@example.com --subject "Hello" --text "Tuesday works." --idempotency-key send-1Sample output
{
"id": "msg_example",
"inbox_id": "inb_example",
"direction": "outbound",
"subject": "Hello",
"text": "Tuesday works.",
"to": [
"ada@example.com"
]
}reply
POST /api/v1/inboxes/{id}/messages/{message_id}/reply. --text or --html is required. --reply-all, --cc, --bcc, --reply-to, and --client-id go in the body. --idempotency-key is the Idempotency-Key header.
node cooper.mjs reply inb_example msg_example --text "Sounds good."Sample output
{
"id": "msg_reply",
"inbox_id": "inb_example",
"direction": "outbound",
"in_reply_to": "msg_example",
"text": "Sounds good."
}wait and code
wait calls GET /api/v1/inboxes/{id}/wait. code calls GET /api/v1/inboxes/{id}/wait-for-code. --timeout (or --wait) sets the wait query, in seconds. code also accepts --after, --from, and --subject.
node cooper.mjs wait inb_example --timeout 25
node cooper.mjs code inb_example --timeout 25 --from signup@example.comSample output
{
"object": "inbox_wait",
"inbox_id": "inb_example",
"timed_out": false,
"data": [
{
"id": "msg_example",
"subject": "Hello"
}
]
}threads list
GET /api/v1/inboxes/{id}/threads. --limit, --before, --after, --ascending, and --page-token are query parameters.
node cooper.mjs threads list inb_example --limit 20Sample output
{
"object": "list",
"inbox_id": "inb_example",
"data": [
{
"object": "thread",
"id": "thr_example",
"inbox_id": "inb_example",
"subject": "Hello",
"message_count": 1
}
],
"next_page_token": null
}drafts create, list, send
POST and GET /api/v1/inboxes/{id}/drafts, and POST /api/v1/inboxes/{id}/drafts/{draft_id}/send. --idempotency-key maps to the Idempotency-Key header on create and send.
node cooper.mjs drafts create inb_example --to ada@example.com --subject Quote --text "Ready for review."
node cooper.mjs drafts list inb_example
node cooper.mjs drafts send inb_example drf_exampleSample output
{
"object": "draft",
"id": "drf_example",
"inbox_id": "inb_example",
"subject": "Quote",
"text": "Ready for review.",
"send_status": "draft"
}api
api <METHOD> <path> calls any /api/v1 path with the same auth and User-Agent. --data is a JSON value, @file, or - for stdin. Paths outside /api/v1 are rejected before a request is sent.
node cooper.mjs api GET /api/v1/me
node cooper.mjs api POST /api/v1/inboxes/inb_example/messages --data '{"to":["ada@example.com"],"subject":"Hello","text":"Hi"}'Sample output
{
"account_id": "acc_example",
"auth_type": "api_key"
}Exit codes
0 means the API returned HTTP 2xx. The body is on stdout. 1 means an API error. The API JSON error body is on stderr, and the process exits 1. On HTTP 429 that body includes retry_after_seconds and resets_at when the API sends them. 2 means a usage error, such as a missing flag or a rejected base URL. 3 means the network call failed.
Sample output
{
"error": {
"type": "rate_limit",
"code": "rate_limited",
"message": "At most 5 inbox waits can run at once for this key.",
"retry_after_seconds": 1,
"resets_at": "2026-10-18T15:00:01.000Z",
"docs_url": "https://cooperemail.com/docs#rate_limited"
}
}