CLI
sentroy is the official Sentroy command-line interface — a single binary that ships with @sentroy-co/client-sdk. Use it for ad-hoc inspection, CI/CD scripts, AI-agent automation, and one-shot recipes that would otherwise require a tiny throwaway Node project.
Overview#
The CLI is a thin wrapper around the same REST endpoints the SDK uses. Every dashboard read operation has a CLI equivalent, and most writes do too. Output is table by default for human eyes, or --output=json for piping into jq, GitHub Actions, or an LLM tool.
Why a CLI? Three audiences:
1. Shell scripting — pull the inbox of a transactional domain every morning and pipe the deltas to Slack.
2. CI/CD — pull the Env Vault before integration tests so the test runner sees production-shaped config without committing secrets.
3. AI agents — Claude / Cursor / Windsurf can shell out to sentroy through their built-in Bash tool. Combined with the Sentroy skill (sentroy ai install), agents get production observability without an SDK roundtrip.
Install#
The CLI is the bin entry of the official SDK package. No separate install — choose local, global, or one-off npx.
# Install alongside the SDK (recommended for CI)
npm install @sentroy-co/client-sdk
# Then either:
npx sentroy --help
# …or add a script in package.json:
# "scripts": { "env:pull": "sentroy env pull .env.local" }Authentication#
Three env vars cover every subcommand. Flag forms exist for one-off overrides and CI matrix jobs.
The CLI accepts the same stk_… Bearer tokens the SDK uses. Tokens are company-scoped — every call needs a token and the company slug that token belongs to. For sentroy env … commands, swap the company token for a vault-scoped SENTROY_ENV_API_KEY instead (the project + environment is implicit in the token).
Environment variables#
| Name | Type | Description |
|---|---|---|
| SENTROY_API_KEYrequired | string (stk_…) | Bearer access token generated under Company → API keys. Returned plaintext only once at create time. |
| SENTROY_COMPANY_SLUGrequired | string | The URL slug of the company the token belongs to (e.g. acme). Read from the company switcher in the dashboard. |
| SENTROY_API_URL | string | Platform root URL. Defaults to https://sentroy.com. Override for staging or local-dev (http://localhost:3000). |
| SENTROY_ENV_API_KEY | string (stk_env_…) | Vault-only token used by every sentroy env … subcommand. Scope (project + environment) is encoded in the token itself — no slug needed. |
Flag overrides#
| Name | Type | Description |
|---|---|---|
| --token | string | Overrides SENTROY_API_KEY. |
| --company-slug | string | Overrides SENTROY_COMPANY_SLUG. |
| --url | string | Overrides SENTROY_API_URL. Handy for CI matrices that hit staging and prod from the same job. |
| --output | table | json | Global. Default table; json emits one JSON document on stdout for piping into jq. |
Output formats#
The same command, two shapes — pick the one your consumer prefers.
--output=table (default) prints a human-readable ASCII table with ANSI colors when stdout is a TTY.--output=json emits one JSON document on a single line for unambiguous parsing. Tables are for humans; JSON is for everything else.
# Human-readable
sentroy mail templates list
# Pipe into jq
sentroy mail templates list --output=json | jq '.data[] | .name'
# Or grab a single field
ID=$(sentroy mail templates list --output=json | jq -r '.data[0].id')
sentroy mail templates get "$ID" --output=json | jq '.subject'Env Vault#
Sync .env files between your project and the Sentroy vault. Full reference: /docs/env-vault.
The env subcommand uses SENTROY_ENV_API_KEY — a separate, narrower token type. The token already carries {project, environment}; no slug needed.
push — upsert local file to vault#
/api/env-vault/push# Upsert only — vault keys absent from .env are kept
sentroy env push .env.production
# Full sync — vault keys absent from .env are deleted (prompts first)
sentroy env push .env.production --delete-missing
# Dry-run — print the diff without writing
sentroy env push .env.production --dry-runpull — write vault to local file#
/api/env-vault/fetch# Refuse to overwrite an existing file by default
sentroy env pull .env.local
# --force overwrites; --public-only fetches the browser-safe subset that
# strips secrets — useful for .env.public files committed to the repo
sentroy env pull .env.public --force --public-onlylist — print vault keys#
sentroy env list # keys only
sentroy env list --values # KEY=value lines (decrypted)
sentroy env list --public-only # browser-safe subsetdiff — compare local file with vault#
# Exit code 0 if identical, 1 if any add/update/delete
sentroy env diff .env.productionUse the exit code in pre-deploy hooks to fail fast when a developer forgets to push their local change.
Mail#
Every mail dashboard surface is mirrored as a CLI subcommand. Auth uses SENTROY_API_KEY + SENTROY_COMPANY_SLUG.
templates list / get#
/api/mail/companies/[slug]/templatessentroy mail templates list
# Columns: ID | NAME | SUBJECT | DOMAIN | CATEGORY | CREATED
# JSON item: { id, name, subject, domainId, category, createdAt }
sentroy mail templates get tpl_a1f9 --output=jsondomains list#
/api/mail/companies/[slug]/domainssentroy mail domains list
# Columns: ID | DOMAIN | STATUS | ASSIGNED | CREATED
# JSON item: { id, domain, status, isAssigned, createdAt }mailboxes list#
/api/mail/companies/[slug]/mailboxessentroy mail mailboxes list
# Columns: ID | EMAIL | DOMAIN | CATCHALL | CREATED
# JSON item: { id, email, domainId, isCatchAll, createdAt }inbox list — filterable feed#
/api/mail/companies/[slug]/inbox| Name | Type | Description |
|---|---|---|
| --mailbox | Only messages delivered to this mailbox. | |
| --folder | string | Restrict to a folder (e.g. inbox, spam, archive). |
| --unread | flag | Only messages without a read receipt. |
| --q | string | Full-text search over subject + body. |
| --page | number | Page index (1-based). |
| --limit | number | Default 50, max 500. |
sentroy mail inbox list --mailbox hello@mail.acme.com --unread --limit 50suppressions list#
/api/mail/companies/[slug]/suppressionssentroy mail suppressions list --output=json | jq '.data | length'
# Columns: ID | EMAIL | REASON | DOMAIN | CREATED
# JSON item: { id, email, reason, domainId, createdAt }logs list / get#
/api/mail/companies/[slug]/logs# Last 100 sends — columns: ID | MESSAGE | TO | STATUS | DOMAIN | CREATED
sentroy mail logs list --limit 100
# Failures only, bounded by date range (ISO timestamps; flags --from / --to)
sentroy mail logs list --status failed --from 2026-05-29 --to 2026-05-30
# Full event timeline for a single send
sentroy mail logs get log_8af2 --output=json
# JSON item: { id, messageId, to, status, domainId, createdAt }webhooks list#
/api/mail/companies/[slug]/webhookssentroy mail webhooks list
# Columns: ID | URL | DOMAIN | ACTIVE
# JSON item: { id, url, events, domainId, active }analytics#
/api/mail/companies/[slug]/analytics# 7-day aggregate (flag is --days=<n>, NOT --since)
sentroy mail analytics --days=7
# JSON shape (--output=json):
# { "windowDays": 7,
# "overview": { "sent", "delivered", "bounced", "complained", "opened", "clicked" },
# "daily": [ /* per-day buckets */ ],
# "domains": [ /* per-domain rollups */ ] }Storage#
Bucket and media inspection. Uploads still go through the SDK (multipart pool); the CLI is read + lightweight admin.
buckets list / get#
/api/storage/companies/[slug]/bucketssentroy storage buckets list
# Columns: ID | NAME | SLUG | PUBLIC | USED | FILES | CREATED
sentroy storage buckets get avatars --output=jsonmedia list / get#
/api/storage/companies/[slug]/buckets/[bucket]/media# media list takes the bucketSlug as a positional arg
sentroy storage media list avatars --limit 20
# Columns: ID | NAME | TYPE | MIME | SIZE | FOLDER | PUBLIC
# media get requires BOTH bucketSlug AND mediaId
sentroy storage media get avatars med_3f2a --output=jsonusage#
# Storage rollup for the company — quota + per-bucket + byType + time series + recent uploads
sentroy storage usage
# Response shape (--output=json):
# { "quota": { "used": 2503671808, "limit": 107374182400, "remaining": 104870510592 },
# "buckets": [ { "id": "bkt_a1f2", "name": "Avatars", "slug": "avatars",
# "storageUsed": 327155712, "fileCount": 1204 } ],
# "byType": { "image": { "count": 9012, "size": 1872310912 },
# "video": { "count": 102, "size": 631360896 } },
# "timeSeries": [ { "date": "2026-05-29", "size": 41943040, "count": 21 } ],
# "recent": [ /* most recent Media[] */ ] }quota#
# Plan-level storage cap + headroom (bytes)
sentroy storage quota --output=json
# {
# "used": 2503671808,
# "limit": 107374182400,
# "remaining": 104870510592
# }
# Convert to GB client-side as needed:
sentroy storage quota --output=json | jq '.used / 1e9'AI Skill install#
One-shot installer that drops the Sentroy skill file into the right place for your AI editor.
The skill teaches Claude / Cursor / Windsurf how the SDK and CLI fit together — endpoint shapes, auth model, common recipes. Full reference: /docs/ai-skills.
# Autodetect: scans the cwd for .claude/, .cursor/, .windsurf/, AGENTS.md
sentroy ai install
# Target specific editors
sentroy ai install --claude --cursor
# All four targets at once
sentroy ai install --claude --cursor --windsurf --agentsScripting recipes#
Real patterns we run in production. Copy, paste, adapt.
Daily bounce report to Slack#
Cron job: every morning, post yesterday's bounced sends to a Slack channel. Pure shell — no Node, no SDK.
#!/usr/bin/env bash
set -euo pipefail
# Bound the window with ISO timestamps (CLI takes --from / --to)
FROM=$(date -u -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ')
TO=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
COUNT=$(sentroy mail logs list \
--status failed \
--from "$FROM" \
--to "$TO" \
--output=json | jq '.data | length')
if [[ "$COUNT" -gt 0 ]]; then
curl -sS -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\": \":warning: $COUNT mail bounces in the last 24h\"}"
fiCI/CD: pull env before tests#
GitHub Actions step that fetches production-shaped env from the vault, runs integration tests against it, and cleans up. No secrets committed to the repo.
# .github/workflows/integration.yml — step body
- name: Pull Sentroy vault
env:
SENTROY_ENV_API_KEY: ${{ secrets.SENTROY_ENV_API_KEY_STAGING }}
run: npx -y @sentroy-co/client-sdk sentroy env pull .env.test --force
- name: Run integration tests
run: bun run test:integration
- name: Cleanup
if: always()
run: rm -f .env.testStorage cost guard#
Nightly check that storage utilization hasn't crossed a plan threshold. Exits non-zero so the cron host can page you.storage quota returns { used, limit, remaining } in bytes — convert to GB client-side.
#!/usr/bin/env bash
THRESHOLD_GB=80
USED_GB=$(sentroy storage quota --output=json | jq '.used / 1e9')
awk -v u="$USED_GB" -v t="$THRESHOLD_GB" 'BEGIN { exit !(u > t) }' && {
echo "storage ${USED_GB}GB exceeds ${THRESHOLD_GB}GB threshold" >&2
exit 1
}Troubleshooting#
The five errors you'll actually hit, with the fix on the same line.
Error: SENTROY_API_KEY is not set#
No token resolved from env or flags. Either export SENTROY_API_KEY=stk_… or pass --token=stk_… on the call. For sentroy env … the variable is SENTROY_ENV_API_KEY (vault-scoped, not company-scoped).
Error: SENTROY_COMPANY_SLUG is not set#
Token is present but the company slug isn't. Set SENTROY_COMPANY_SLUG=acme or pass --company-slug=acme. The slug is visible in the dashboard URL: sentroy.com/d/acme/….
401 Unauthorized#
The token is wrong, revoked, or expired. In the dashboard go to Company → API keysand verify the prefix (the first 12 chars) against what you have locally — if it doesn't match, the token was rotated. Generate a fresh one and update wherever you store it.
403 Forbidden#
Token is valid but lacks the permission needed for this command. Open the token in the dashboard, toggle the missing permission on (e.g. templates.manage for mail templates create), and retry. Read-only tokens can list and get; write commands need the matching .manage permission.
404 Not Found#
Usually the wrong company slug, occasionally a typo'd resource id. Run sentroy mail templates list (or equivalent) to confirm the resource exists in the company the token belongs to — tokens from another company will 404, not 403.
Verbose mode#
A first-class --verboseflag is on the roadmap. Until then, lean on Node's built-in HTTP tracer to see raw fetch calls, or pipe through tee to capture stdout/stderr alongside the command output.
# Node's built-in HTTP debug output
NODE_DEBUG=http sentroy mail templates list
# Tee stdout + stderr to a log for an issue report
sentroy mail templates list --output=json 2>&1 | tee /tmp/sentroy-cli.log