MCP tool reference
Suite MCP program (protocol, catalog, identity): see the cross-pillar audit at the Tandem suite root, docs/mcp-protocol-audit.md (Work board: project Suite MCP).
Every Tandem MCP tool, grouped by domain. The authoritative list is whatever tools/list returns on a live connection — this page tracks it, but the wire wins. Each row is a one-line purpose plus its key parameters — * marks a required parameter. Descriptions are faithful to the live tool definitions; call a tool with only its schema-required args to start.
All calls go to POST https://portal.launchtandem.com/mcp as JSON-RPC tools/call with an Authorization: Bearer tdm_… header. Only create_account_and_claim_link and create_own_organization work without auth. See Connecting agents for client setup and Capabilities overview for the resource model.
Common gating shorthand used below: read/manage = the token action required; viewer/developer/admin/owner = the org role required.
Scoping the tool list
tools/list defaults to the full catalog. Trim with ?toolsets=core (hero-flow: identity + zero→live, ~20 tools) or ?toolsets=<comma-separated names> on the endpoint, e.g. POST https://portal.launchtandem.com/mcp?toolsets=deploy,dns,domains. Cursor HTTP MCP often drops the URL query — send the same value as header X-Tandem-Toolsets: core. Scoping only trims what is advertised — every tool remains callable via tools/call regardless of listing. Unknown or empty names are ignored (a typo falls back to the full catalog, never a broken connection), and identity is always included so whoami and bootstrap stay reachable. The server also scopes the list to your credential’s permissions and to product surfaces your org actually uses; entry tools (e.g. provision_email, purchase_domain) always stay visible so capabilities remain discoverable.
| Toolset | What’s in it |
|---|---|
core |
Hero-flow trim: identity + zero→live (~20 tools). Opt in with ?toolsets=core. |
deploy |
Projects, services, deployments, previews, logs, env vars, service commands, cron jobs |
org |
Members, guests, access grants, recovery keys, limits, pillars |
billing |
Billing status, plans, subscription, usage, invoices, payment method |
database |
Managed Postgres/MySQL |
buckets |
S3-compatible object storage |
redis |
Managed Redis/Valkey |
git |
Tandem Git hosting, plus GitHub connect / import / export / mirror |
dns |
DNS zones and record CRUD |
domains |
Registrar (availability, purchase, renewal, transfer), attach / reassign / detach |
email |
Email domains, mailboxes, transactional sending, inbound webhooks |
sms |
Phone numbers, compliance, sending, inbound webhooks |
analytics |
Web analytics |
support |
Support tickets |
identity |
whoami + account bootstrap — always included |
The free trial, in one paragraph
A new organization starts on the preview tier: a 14-day free trial that really deploys, with no payment method at all. Its allowance is 1 service, 1 database, 1 bucket; custom domains, domain purchase/transfer, outbound email and managed Redis are excluded outright (a card does not buy them — a subscription does). Domain renewal is allowed with a card, so a domain you already own can never lapse. At expiry, deploys are blocked (preview_expired) and containers are reclaimed — nothing is deleted: code, databases, files, env vars and domains survive, and create_subscription restores and redeploys everything automatically (resume_service is the per-service fallback). Upgrading is always two calls: set_payment_method, then create_subscription — the card is not the upgrade. Call whoami to read all of this per org before you plan.
Identity, bootstrap & recovery
| Tool | Purpose | Key params |
|---|---|---|
pillar_directory |
Map of the five Tandem pillars (launch/work/speak/grow/profit) — per pillar: name, status (live/beta/coming_soon), base URL, MCP endpoint (null until live; plannedMcpUrl for future homes), and auth. The same tdm_ bearer token works on every pillar’s MCP server. See The Tandem suite. No action gate. |
— |
whoami |
Call this first. Your identity + username (for a human to add you to an org), your credential’s live action mask, and per-org entitlements: tier (preview = 14-day free trial, paid = subscribed), entitlement (trial/trial_expired/paid/comped), trialActive/trialExpiresAt, canDeploy + deployBlockers, trialAllowances (used vs limit for services/DBs/buckets), lockedUntilSubscribed, and nextStep. Plan against these instead of discovering limits by hitting errors. |
— |
change_my_email |
Change your OWN directory handle (for a human, also the sign-in). Agent: applies immediately if your org has proven control of the address’s domain — registered it through Tandem, delegated its DNS, verified it for email, or verified a custom hostname on it; your token keeps working (credentials key on the token hash, not the handle). Human: nothing changes yet — a confirmation link goes to the NEW address and a notice to the old one. No action gate (an account action on yourself, like changing your password). | newEmail*, currentPassword |
confirm_email_change |
Redeem the token from a confirmation link and complete a pending change. Works without a session — holding the token IS the proof you read mail at the new address. | token* |
cancel_email_change |
Cancel your pending change and kill its link. Also the response to a change notice you did not initiate (rotate your password too). | — |
create_account_and_claim_link |
(no auth) Create an org + human owner, email a claim link; anonymous calls mint an agent token bound to this session, authenticated agents are added as admin members of the new org so subsequent calls can access it without a session swap. | ownerEmail*, organizationName*, ownerDisplayName |
create_own_organization |
(no auth) Create an org owned by YOU (the agent) with no human owner; mints an owner token. Then register + verify a recovery key before deploying. | organizationName* |
get_claim_status |
Poll an org’s claim status → { claimed, paid, customDomain }. |
organizationId* |
get_deploy_gate_status |
Check whether an org can deploy → { billingVerified, recoveryVerified, recoveryMethod, trialActive, trialExpiresAt, blockers, allowed }. recoveryMethod is email (verified human owner), auth_key (Ed25519 key), or agent_bootstrap (agent admin during trial — enables zero-click bootstrap deploy). Blockers: recovery_unverified, preview_expired, billing_unverified — the last two are both cleared by set_payment_method then create_subscription (a card alone does not lift the gate). read + membership. |
organizationId* |
register_recovery_key |
Register an Ed25519 public key (SPKI); returns a challenge to sign. manage. | organizationId*, label*, publicKey* |
verify_recovery_key |
Submit the Ed25519 signature over the challenge to activate the key. manage. | keyId*, signature* |
list_recovery_keys |
List an org’s recovery keys (no public-key material). read. | organizationId* |
revoke_recovery_key |
Revoke a verified recovery key. manage. | keyId* |
Projects
| Tool | Purpose | Key params |
|---|---|---|
list_projects |
List projects visible to the caller (scoped by token / memberships). Each row includes an accurate deployment_count. |
— |
create_project |
Create a project in an org; optionally link a connected repo. | slug*, name*, environment, repositoryId, organizationId |
rename_project |
Rename a project; changing slug rewrites every platform hostname + DNS + Caddy and returns hostnameChanges[]. manage + developer. |
projectId*, name, slug |
delete_project |
Cascade-delete a project and every service, bucket, and database in it. Irrevocable. manage + admin. | projectId*, confirmSlug* |
Services
| Tool | Purpose | Key params |
|---|---|---|
list_services |
List services, optionally filtered by projectId. |
projectId |
read_service_config |
Read a service’s type, paths, healthcheck, domains, and env-var keys (values not returned). | serviceId* |
update_service_config |
Patch an existing service’s build/start command, output directory, or healthcheck path. null clears a field back to the per-type default (use this to drop a stale override so the repo’s platform.yml wins again); omitted fields are unchanged. Takes effect on the next deployment, not immediately. manage + developer. |
serviceId*, buildCommand, startCommand, outputDirectory, healthcheckPath |
import_services |
Bulk-create services in a project from service definitions (e.g. read_platform_yml output); auto-attaches platform hostnames. |
projectId*, services* |
list_catalog_apps |
List installable open-source apps (e.g. Twenty CRM) with their inputs, dependencies, and services. Discovery for install_app. read. |
— |
install_app |
One-call install of a catalog OSS app from its published image: repo-less project (platform.yml stored on the project) + image services (with memory) + provision/attach Postgres/Redis/bucket + env wiring + deploy. dryRun previews the plan. manage + deploy. |
app*, organizationId, projectSlug, inputs, dryRun |
restart_service |
Restart the current healthy deployment’s container; Caddy re-points to the new port. restart. | serviceId* |
list_service_commands |
List the commands runnable inside a service’s running container: repo-declared entries from platform.yml commands: plus platform presets attached by detection (e.g. wp:* incl. wp:login on WordPress services), each with description, min role, timeout, and param specs. read. |
serviceId* |
run_service_command |
Run one catalog command in the running container → { exitCode, output, truncated, durationMs } (params appended as discrete argv items — no shell; 256 KiB output cap; one at a time per service; audit-logged). Pass only the params the entry declares — wp:login declares none: the platform resolves the WordPress administrator itself. exec + the command’s own min role. |
serviceId*, command*, params |
list_service_command_runs |
Run history for a service’s commands — every run from both surfaces and every scheduled firing, successes and failures. Without runId: recent runs newest-first (command, source, argv, who ran it, trigger manual/schedule with scheduleId/scheduleName, outcome ok/failed/timeout/exec_failed, exit code, duration). With runId: that one run including its stored output (last 32 KiB of the transcript). Pass scheduleId to see just one schedule’s runs. Last 20 runs per service. read. |
serviceId*, runId, scheduleId, limit |
create_command_schedule |
Run a catalog command on a recurring 5-field UTC cron schedule inside the running container (e.g. nightly wp:cache-flush). params are FIXED at schedule time. Each firing uses the identical exec path as run_service_command and lands in the same run history with trigger=schedule. exec + the command’s own min role — scheduling costs exactly what running costs. Quotas: 5/service, 20/org. |
serviceId*, name*, schedule*, command*, params, enabled |
list_command_schedules |
A service’s schedules: cron expression, command + fixed params, enabled, next/last run, last outcome (+lastRunId), consecutive failures, who it runs as, and disabledReason when the platform turned it off. read. |
serviceId* |
update_command_schedule |
Change the cron expression/name, retarget at another command (with params), or toggle with enabled. Re-authorized on every call — including a bare enabled: true — and the caller becomes the authority the scheduler re-checks. exec + the command’s own min role. |
scheduleId*, name, schedule, command, params, enabled |
delete_command_schedule |
Delete a schedule; its past runs stay in history. Removing a deferred exec is the unprivileged direction, so this needs developer + manage rather than the command’s own role. | scheduleId* |
rollback_service |
Roll back to a previous healthy deployment (reuses the prior image, skips build). rollback. | serviceId*, targetDeploymentId |
resume_service |
Bring a suspended service back online (suspension = the org’s free trial ended and its containers were reclaimed; nothing is deleted). Subscribing resumes automatically, so this is the fallback for when that failed. Requires the org to pass the deploy gate. deploy + developer. | serviceId* |
delete_service |
Scoped cascade-delete of one service (deployments, env vars, domains, attachment rows). Preserves the buckets/DBs/redis/mailboxes/domains themselves; the analytics site is kept as external by default (analytics=keep|delete). manage + admin. |
serviceId*, confirmName, analytics |
Deployments & logs
| Tool | Purpose | Key params |
|---|---|---|
create_deployment |
Queue a deployment for a service. deploy. | serviceId*, gitRef, gitSha |
deploy_preview |
Queue a preview deployment from a branch/ref (metadata.kind='preview'). deploy. |
serviceId*, gitRef*, gitSha |
deploy_project |
Deploy every service in a project at one gitRef (the “Deploy all” fan-out). deploy. |
projectId*, gitRef |
list_deployments |
List recent deployments (newest first) with state, git ref/sha, image ref, timestamps, errorMessage, supersededBy. States include superseded (a newer like-for-like push replaced the build before it finished — not a failure). |
serviceId*, limit |
get_deployment |
Read one deployment incl. buildLog (kept for every built deployment, healthy or failed), containerLog (runtime failures — check both on a failure), timings (per-phase ms + Dockerfile step table), and supersededBy on superseded rows. Log content, so it carries the same gate as view_logs: read + viewer. |
deploymentId* |
view_logs |
Fetch recent container logs for the current healthy deployment. logs + viewer. | serviceId*, tail |
GitHub & repositories
Auto-deploy on push is silent by design when it succeeds — but if the org fails the deploy gate (expired trial, unconfirmed billing, unverified recovery), the push is skipped, not queued. The platform emails the org owners once per day when that happens and writes a github.push.auto_deploy.blocked_notice audit row naming the blocker, the remedy and the skipped services. Pushes resume deploying automatically once the blocker clears; no re-push is needed.
| Tool | Purpose | Key params |
|---|---|---|
list_github_installations |
List GitHub App installations visible to the caller. | — |
list_installation_repositories |
List repos granted to a specific installation (cached 5 min). | installationId* |
list_repositories |
List repos already connected to the caller’s org(s); hasInstallation flags platform.yml / auto-deploy support. |
— |
connect_repository |
Connect a repo from an installation into an org. manage + developer. | installationId*, githubRepoNodeId*, organizationId |
set_project_repository |
Set a project’s deploy source: a connected GitHub/URL repo (repositoryId, manage) OR a Tandem Git repo (tandemGitRepoId, source:write — works for a standalone repo too). + optional auto-deploy branch. developer. |
projectId*, one of repositoryId/tandemGitRepoId, deployBranch |
read_platform_yml |
Fetch + parse platform.yml from a connected repo → services ready to import. |
repositoryId*, ref |
set_project_platform_yml |
Replace the inline platform.yml of a repo-less project (an image app from install_app, config on the project row, not a repo). Validated before saving; next deploy picks it up. Bump an image tag / fix an env alias with no git commit. manage + developer. |
projectId*, platformYml* |
Git repositories (Tandem Git)
Tandem’s own private git hosting: standard HTTPS clone/push (username git, password = an access token). Agents: mint the token with create_git_credential — it returns a ready authenticated clone URL for non-interactive use (an MCP agent cannot see its own bearer to type it as a password; never run a bare git clone without a TTY, the password prompt hangs). Agent credentials gate on the dedicated source:read / source:write / source:admin actions (minted default-on with new tokens); per-repo access = org role (owner/admin → all, developer → read+write, viewer → read) plus explicit share grants. See Tandem Git.
| Tool | Purpose | Key params |
|---|---|---|
create_git_repo |
Create a private repo; init: "template" (default) seeds platform.yml + README + .gitignore so the first push can deploy, "empty" creates a bare repo. Pass asProjectSource: {newProject} or {projectId} to wire the new repo up as a project’s deploy source in the same call — zero GitHub required, and a push to the deploy branch auto-deploys exactly like a GitHub-linked project. source:write + developer. |
name*, organizationId, projectId, description, init, asProjectSource |
list_git_repos |
List repos you can read (all your orgs + repos shared with you; narrow by org/project) with clone URL, default branch, archived flag, and your caps. source:read. |
organizationId, projectId |
read_git_repository |
Read without cloning — what: tree (directory listing, recursive optional), file (base64 content + isBinary), commits (paged history via limit/cursor, optional path filter), branches. source:read. |
repo*, what*, ref, path, recursive, limit, cursor |
commit_git_changes |
Apply up to 200 put/delete/move operations as ONE atomic commit; baseSha gives optimistic concurrency (stale → ref_conflict + currentSha), createBranchFrom creates the branch first. source:write + repo write access. |
repo*, branch*, message*, operations*, baseSha, createBranchFrom |
share_git_repo |
Drive-style sharing: grant independent canRead/canWrite/canAdmin to a principal or email (unknown email → account + setup link), or revoke: true to remove. source:admin + repo admin access. |
repo*, principalId|email, canRead, canWrite, canAdmin, revoke |
create_git_credential |
Mint a tdm_git_ access token for YOURSELF (shown once) plus a ready authenticated clone URL and non-interactive setup recipes (credential-store preseed, Bearer header) and your caps on the repo. Default expiry never (omit expiresAt or null; ISO only for a hard TTL). The PAT inherits this credential’s project/service scope and action mask (OPS-57) so it cannot outrank the minting credential; entry gate still requires source:read + repo read. |
repo*, name, expiresAt |
list_git_credentials |
Your own access tokens’ metadata (name, prefix, created/last-used/expires/revoked) — never the secret. Revoke via portal or DELETE /api/git/tokens/:id. source:read. |
— |
import_repository |
Migrate a repository INTO Tandem Git (one-time copy). source: {githubRepositoryId} copies an existing GitHub-connected repo (repointProject: true switches its project(s) onto the new Tandem repo); source: {url} clones any public https git URL (tandemRepoName required; optional project: {name} or {id}). Async — poll with get_source_status. Never copies refs/pull/*. source:write + developer + unscoped agent credential. |
source*, tandemRepoName, repointProject, project, organizationId |
export_repository_to_github |
Migrate a Tandem repo OUT to GitHub (one-time copy). githubRepoNodeId must name an EMPTY repo you created yourself (App tokens can’t create repos or overwrite history — verified by the worker before pushing). then: "repoint" switches the project(s) to the GitHub repo afterward. Async — poll with get_source_status. source:write + developer + unscoped agent credential. |
repo*, installationId*, githubRepoNodeId*, then |
get_source_status |
Poll a migrate job (jobId) or a repo’s most recent one plus its mirror link state (repo) — state/progress/error, and if mirrored: mode, status, lag, last error, and (two-way) conflicts[] — the held refs with both shas and since. source:read. |
repo|jobId |
configure_git_mirror |
Continuous “Mirror to GitHub” (not the one-time import/export above). mode: two_way (recommended — push to either side; each branch fast-forwards only; a branch diverged on both sides is HELD as a conflict, never force-pushed or auto-merged), tandem_primary / github_primary (one source of truth, the other a force-updated read-only copy). action: enable (requires mode + github: {githubRepositoryId} or {installationId, githubRepoNodeId}); pause/resume; switch_mode (requires mode; final sync first; refused while a two-way mirror holds conflicts); switch_primary (primary↔primary shorthand); resolve_conflict (requires ref + keep: tandem|github — settles one held ref, loser backed up to refs/tandem/backup/*); disable (both repos stay intact). Held conflicts are listed by get_source_status → mirror.conflicts. source:admin + org admin + unscoped agent credential. |
repo*, action*, mode, github, ref, keep |
Environment variables
| Tool | Purpose | Key params |
|---|---|---|
list_env_vars |
List a service’s env-var keys, scopes, and masked previews (never full values). | serviceId* |
set_env_var |
Set/overwrite one env var (encrypted at rest). scope = runtime (default) / build / both. |
serviceId*, key*, value*, scope |
set_env_vars |
Bulk-set env vars in one atomic call — paste a whole .env as content, or pass vars. Prefer this over repeated set_env_var. replaceAll also deletes keys not in the call (file-sync). |
serviceId*, content, vars, scope, replaceAll |
delete_env_var |
Delete an env var by key (applies on next deploy/restart). | serviceId*, key* |
Databases (Postgres / MySQL)
| Tool | Purpose | Key params |
|---|---|---|
list_databases |
Discover databases (the entry point — every other DB tool needs a databaseId). Non-secret metadata + service attachments; org-wide rows add projectSlug/organizationSlug. Omit projectId to list across all visible orgs. |
projectId |
get_database |
Fetch one DB’s non-secret metadata by UUID. | databaseId* |
provision_database |
Provision a tenant DB + user; auto-creates DATABASE_URL on the service. kind picks the engine. |
projectId*, kind, label, attachServiceId |
reveal_database_credentials |
Full internal credential bundle (host/port/db/user/password/URL). Audit-logged. | databaseId* |
reveal_database_public_credentials |
Credentials with the public host for external access (only connects from an allowlisted IP). Audit-logged. | databaseId* |
query_database |
Run SQL against the DB as the tenant role (full read/write; the portal SQL runner’s equivalent). Multiple statements allowed — last result set returned. Audit-logged. manage + developer. | databaseId*, sql*, maxRows |
export_database |
Enqueue a full dump of the DB (schema + data) to a .sql file — the portal’s Export button equivalent. Returns the job (state queued); the worker runs pg_dump/mysqldump. Audit-logged. manage + developer. |
databaseId* |
import_database |
Enqueue a restore of a plain-SQL .sql dump from an https sourceUrl into the DB — the portal’s Import button equivalent. Destructive (runs against the live DB): pass confirmName = the db_name. https-only/SSRF-guarded source, capped by TANDEM_DB_DUMP_MAX_BYTES with a disk-headroom preflight. Worker runs psql/mysql. Audit-logged. manage + developer. |
databaseId*, sourceUrl*, confirmName* |
list_database_allowlist |
List the DB’s public-access IP allowlist (CIDRs). | databaseId* |
add_database_allowlist |
Grant a CIDR public access (firewall + engine-level grant); optional ttlHours. |
databaseId*, cidr*, label, ttlHours |
remove_database_allowlist |
Revoke an allowlist entry by id. | databaseId*, entryId* |
delete_database |
Drop the DB + role, remove DATABASE_URL from attached services. Irreversible. manage + developer. |
databaseId*, confirmName |
Object storage (S3-compatible buckets)
| Tool | Purpose | Key params |
|---|---|---|
create_bucket |
Create a bucket + scoped creds; optionally inject the six S3_* env vars on a service. |
projectId*, label, attachServiceId |
list_buckets |
List buckets in a project and which services each is attached to. | projectId* |
attach_bucket_to_service |
Inject the six S3_* env vars on a service (idempotent). |
bucketId*, serviceId* |
detach_bucket_from_service |
Remove the S3_* env vars; bucket + objects kept. |
bucketId*, serviceId* |
reveal_bucket_credentials |
Full bucket credential bundle (endpoint/region/keys). Audit-logged. | bucketId* |
delete_bucket |
Delete a bucket (must be empty first); revokes creds, removes S3_* vars. No undo. |
bucketId* |
set_bucket_cors |
Replace the bucket’s CORS policy (browser-only; not access control). A "*" origin is rejected unless allowAnyOrigin: true. Audit-logged. |
bucketId*, rules*, allowAnyOrigin |
get_bucket_cors |
Read the bucket’s CORS rules (no endpoint/creds). | bucketId* |
Redis / Valkey
| Tool | Purpose | Key params |
|---|---|---|
provision_redis |
Provision a managed Valkey instance (async — poll list_redis for ready). tier = small/default/large. Subject to deploy gate + RAM budget. |
projectId*, label, tier, attachServiceId |
list_redis |
List instances with status/tier/persistence and attached services (poll for ready). |
projectId* |
reveal_redis_credentials |
Host/port/password + full REDIS_URL (only when ready). Audit-logged. |
redisId* |
attach_redis_to_service |
Inject REDIS_URL on a service (idempotent; must be ready). |
redisId*, serviceId* |
detach_redis_from_service |
Remove REDIS_URL; instance + data kept. |
redisId*, serviceId* |
delete_redis |
Tear down the instance + data volume (async). No undo. | redisId* |
Email is custom-domain-only — onboard a domain before creating mailboxes or sending. See Email for the full flow.
| Tool | Purpose | Key params |
|---|---|---|
add_email_domain |
Onboard a custom domain for send+receive; returns DNS records (MX, DKIM, SPF, DMARC). | projectId*, domain* |
list_email_domains |
List onboarded email domains + SES/DKIM/inbound status. | projectId* |
verify_email_domain |
Re-check a domain’s verification against SES + DNS. | domainId* |
remove_email_domain |
Remove an onboarded domain (refuses if mailboxes remain). | domainId* |
set_dmarc_reporting |
Configure DMARC aggregate-reporting ownership (no lock-in): managed (Tandem collector), external (your own rua), or both. Provisions/removes the external-destination auth record. |
mode*, domainId|domain+projectId, externalRua |
get_dmarc_summary |
Email Health for one domain: DMARC pass-rate, failing sources, reporter coverage, and advisory (safeToQuarantine / safeToReject). Use before tightening policy. |
domainId|domain+projectId, sinceDays |
set_dmarc_policy |
Set the published DMARC policy p= none→quarantine→reject (and back) — managed + reversible. Tightening gated on get_dmarc_summary advisory unless force=true. Managed/both only (external hosts its own _dmarc). Reporting rua preserved. |
policy*, domainId|domain+projectId, force |
set_email_catch_all |
Set the domain’s catch-all: mail to any non-existent address delivers to the given mailbox (must exist on the same domain) instead of being rejected. One per domain; state on list_email_domains. |
address*, domainId|domain+projectId |
remove_email_catch_all |
Clear the catch-all so unknown addresses are rejected again (deleting the destination mailbox also clears it). | domainId|domain+projectId |
set_email_forwarder |
Forward an address on an onboarded domain to an external inbox. With a mailbox at the source, defaults to deliver-and-forward (local copy + relay). Without a mailbox, creates a forward-only address. Loop protection rejects self-referential and ping-pong forwards. | sourceAddress*, destinationAddress*, organizationId|projectId, keepLocalCopy |
list_email_forwarders |
List external forwarders for the org. | organizationId|projectId, domain, sourceAddress |
remove_email_forwarder |
Remove one forwarder by id. | forwarderId* |
create_email_api_key |
Create a transactional send-API key (Bearer for POST /api/email/send); returned once. Optional allowedDomains. |
projectId*, name*, allowedDomains |
list_email_api_keys |
List send-API keys (never the secret). | projectId* |
revoke_email_api_key |
Revoke a send-API key by id. | projectId*, keyId* |
provision_email |
Provision a real mailbox (IMAP+SMTP+JMAP) on an onboarded domain; optionally inject EMAIL_* on a service. |
projectId*, domain*, localPart, label, attachServiceId |
list_emails |
List mailboxes in a project (address, DNS status, attachments). No passwords. | projectId* |
attach_email_to_service |
Inject EMAIL_* (IMAP+SMTP+JMAP) on a service (idempotent). |
mailboxId*, serviceId* |
detach_email_from_service |
Remove EMAIL_*; mailbox + mail kept. |
mailboxId*, serviceId* |
reveal_email_credentials |
Full connection profile (IMAP/SMTP host/port/user/password + JMAP url/token). Audit-logged. | mailboxId* |
rotate_email_credentials |
Rotate the mailbox password (set or auto-generate); re-injects into attached services. | mailboxId*, password |
delete_email |
Delete a mailbox (all stored mail lost); tears down domain DNS if it was the last one. No undo. | mailboxId* |
set_email_inbound_webhook |
Forward inbound mail as HMAC-signed JSON POSTs to a URL; returns a signing secret once. | mailboxId*, url* |
get_email_inbound_webhook |
Show the mailbox’s webhook config (secret never returned). | mailboxId* |
remove_email_inbound_webhook |
Stop forwarding; mail still arrives in the mailbox. | mailboxId* |
share_mailbox |
Grant a principal capabilities on a mailbox: view/categorize/manage/send (independent — send-only and categorize-only are valid). Unknown email → creates a mail-only account (no org) + emails a setup link; they set a password and land in the shared mailbox (invited:true in the response). Re-share replaces caps. |
mailboxId*, principalId|email, canView*, canCategorize*, canManage*, canSend* |
revoke_mailbox_grant |
Revoke a subject’s grant (disappears from their webmail on next sync); history kept. | mailboxId*, principalId|email |
list_mailbox_grants |
Who has access to a mailbox + each subject’s caps, incl. pending invites. | mailboxId* |
start_email_migration |
Migrate a domain’s email INTO Tandem from Google Workspace / cPanel / any IMAP host. Creates the migration + tests the source. Google: one-time Admin-console delegation of our client id, then fully automatic. Org admin; agents need an unscoped credential. | domain*, provider*, organizationId, sourceAdminEmail (google), sourceHost/sourceUsername/sourceApiToken/sourceSecret (cpanel), sourceHost (imap) |
discover_email_migration_mailboxes |
Auto-list the source’s mailboxes + aliases (google/cpanel), external forwarders to off-domain destinations (cPanel), or pass mailboxes explicitly (imap / review edits). Re-callable until sync starts. |
migrationId*, mailboxes |
begin_email_migration_sync |
Provision Tandem twins (aliases included), onboard the domain with inbound DNS deferred (no MX/SPF change — source stays live), and start per-mailbox history syncs. Re-call from synced for delta passes. |
migrationId* |
get_email_migration_status |
THE polling tool: per-mailbox sync table, jobs, provider setup steps, owed DNS records; or the org’s migration list without migrationId. |
migrationId, organizationId |
approve_email_migration_cutover |
Arm the cutover: prechecks → DNS flip (automatic on Tandem-hosted zones; instruction set for external DNS — REPLACE the existing SPF) → final delta → complete. T+1d/T+7d sweeps follow. | migrationId*, scheduledAt |
cancel_email_migration |
Cancel; provisioned mailboxes + synced mail kept, source untouched. | migrationId* |
Domains
| Tool | Purpose | Key params |
|---|---|---|
list_domains |
List everything domain-related for the org in one call: registrations (Tandem-registered apexes — carries the domainOrderId that renew_domain/set_domain_auto_renew take, plus status, expiresAt, autoRenew, attachedServices; in-flight purchases/transfers included so you can poll an approval or transfer), externalZones (DNS hosted here but bought elsewhere), attachedHostnames (hostname → service, kind platform|custom, verified, redirect). Read-only. read + any role. |
organizationId, includeInFlight |
check_domain_availability |
Check availability + registrar price for hostnames or a stem across default TLDs. Read-only. |
hostnames, stem |
purchase_domain |
Register a domain + attach it to a service. Paid plans credit the first year (Pro 1, Scale 3, capped at $15); returns costCents (charged), creditAppliedCents, renewalPriceCents (undiscounted — always disclose it). Owner runs synchronously; others get an approvalUrl. manage; deploy gate + a chargeable card (not covered by the free trial). |
organizationId*, hostname*, serviceId |
attach_domain |
Attach an already-owned or external hostname to a service (auto A record for subdomains of a Tandem-registered apex; else DNS instructions + automatic recheck until it resolves). manage + developer. | serviceId*, hostname* |
redirect_domain |
Point a parked/alias hostname at an arbitrary target with a path-preserving 301 (attaches the alias, or repoints an existing one); no dummy service needed. Target may be off-platform. manage + developer; paid custom-domain plan. | serviceId*, hostname*, targetHostname* |
verify_domain |
Force an immediate DNS recheck for an attached custom hostname (the sweep also rechecks automatically); activates the domain when DNS resolves. manage + developer. | hostname* |
reassign_domain |
Move an attached hostname to another service in the same org in one step (keeps DNS verification; routes refresh automatically). manage + developer. | hostname*, serviceId* |
detach_domain |
Detach a hostname from its service (unpublishes the platform A record, discards verification state; a Tandem registration itself is untouched). Prefer reassign_domain for moves. manage + developer. |
hostname* |
renew_domain |
Renew a Tandem-registered domain one year (payment + approval path). manage + developer; deploy gate + a chargeable card (not covered by the free trial). | organizationId*, domainOrderId, hostname, years |
set_domain_auto_renew |
Toggle auto-renew (no charge, no deploy gate). manage + developer. | organizationId*, autoRenew*, domainOrderId, hostname |
transfer_domain |
Transfer a domain in from another registrar (charges one year; needs EPP authCode). manage + developer; deploy gate + a chargeable card (not covered by the free trial). |
organizationId*, hostname*, authCode*, projectId, serviceId |
set_domain_owner_contact |
Set the org’s domain owner contact — the customer’s legal name/address/email/phone that goes on every registration as the registrant of record (the customer owns their domains, not Tandem). Required once before purchase_domain/transfer_domain. Default also re-registrants existing domains (applyToExistingDomains); the contact email must click a one-time ICANN verification email within 15 days of first use or the registry suspends the domain (Tandem nudges automatically). manage + admin. |
organizationId, firstName*, lastName*, email*, organizationName, address1*, address2, city*, stateProvince*, postalCode*, country*, phone*, applyToExistingDomains |
get_domain_ownership |
Owner contact on file + every registered domain’s ICANN verification status (verified/unverified/pending_suspension/suspended), registrant email, and deadline. Check this after purchases and whenever a domain mysteriously stops resolving. read + viewer. |
organizationId |
resend_domain_verification |
Resend the ICANN verification email (sender donotreply@name-services.com) for a registered domain — also the self-serve fix for a verification-suspended domain (restored minutes after the link is clicked). manage + developer. |
organizationId, hostname* |
SMS & phone numbers (US/CA)
Numbers, A2P compliance, and messaging over Telnyx. Call get_sms_setup_status first for any number — it is the one status tool that says exactly what is pending and which tool resumes it, and whether production sends are allowed yet (canSend). Same org-role gates and Wave-1 stores as the /api/sms/* REST surface. When Telnyx is unconfigured every tool returns sms_unavailable.
Recommended flow: search_phone_numbers → purchase_phone_number → a compliance path (toll-free = fastest/free; or 10DLC = higher throughput) → send_sms. Sends are blocked until compliance is approved (the error carries the setupStatus).
| Tool | Purpose | Key params |
|---|---|---|
search_phone_numbers |
Search buyable US/CA inventory. read + developer. | type* (local|toll_free), countryCode, areaCode, locality, administrativeArea, contains, organizationId |
purchase_phone_number |
Buy a number (lazily creates the org messaging profile). Owner-under-threshold buys synchronously; others get an approvalUrl. manage + developer; deploy gate. |
e164*, numberType*, label, organizationId |
list_phone_numbers |
List the org’s numbers, each with unified setupStatus + attachments. read + viewer. |
organizationId |
release_phone_number |
Detach from all services + give up at the carrier (irreversible). manage + developer. | numberId*, confirm* (the number’s E.164) |
register_sms_brand |
Register the org’s 10DLC brand (one per org). Standard = EIN + companyName; SOLE_PROPRIETOR = name + mobilePhone, returns otpRequired. manage + developer; deploy gate. |
entityType*, displayName*, companyName, ein, firstName, lastName, mobilePhone, address fields, organizationId |
confirm_sms_brand_otp |
Submit the sole-prop mobile OTP PIN (expires 24 h). Degrades to action_required if Telnyx can’t automate it. manage + developer. |
brandId*, pin*, organizationId |
create_sms_campaign |
Submit a 10DLC campaign under an approved brand. Validated locally first (returns errors on failure — no charge); $15/submission so approval-gated (approvalUrl over threshold). manage + developer; deploy gate. |
brandId*, usecase*, description*, sampleMessages*, messageFlow*, keywords, organizationId |
assign_number_to_campaign |
Link a number to a campaign (enforces the 49/campaign T-Mobile cap). manage + developer. | campaignId*, numberId* |
submit_tollfree_verification |
Submit the toll-free verification (fastest/free path). Validated locally (full state name, additionalInformation, ≥1 opt-in evidence URL, plus the carrier’s dedicated columns: entityType, registration number/type/country, privacy + terms URLs — prose in additionalInformation does NOT populate them). manage + developer. |
business/contact fields, phoneNumbers*, useCase*, optInWorkflowImageUrls*, messageVolume*, additionalInformation*, entityType*, businessRegistrationNumber*, businessRegistrationType*, businessRegistrationCountry*, privacyPolicyUrl*, termsAndConditionsUrl*, optional doingBusinessAs/isvReseller/ageGatedContent/opt-in response fields, organizationId |
resubmit_tollfree_verification |
Resubmit (PATCH) an existing toll-free verification after action_required/rejection — same packet as submit against the SAME Telnyx request (avoids the duplicate-resource error). Free and unlimited except terminal High Risk - Fraud. manage + developer. |
submit fields + tollfreeId* |
get_sms_setup_status |
THE status tool — unified state + reason + concrete remediation + the exact next tool + canSend. Call first. read + viewer. |
numberId* |
send_sms |
Send SMS/MMS from an owned number. Compliance-gated (allowPending for a pending toll-free number); enforces rate limit + the acting principal’s spend cap. Returns segments + estimated cost. manage + developer. |
numberId|from, to*, text, mediaUrls, allowPending, organizationId |
list_sms_messages |
List messages (bodies decrypted for the owner), newest first. read + viewer. | numberId, direction, status, limit, organizationId |
attach_phone_number_to_service |
Inject the SMS_* runtime env vars (URL/key/number/webhook-secret) on a service. Idempotent. manage + developer. |
numberId*, serviceId* |
detach_phone_number_from_service |
Remove the injected SMS_* env vars and revoke that attachment’s send key (standalone keys survive). manage + developer. |
numberId*, serviceId* |
create_sms_api_key |
Mint a send key for an app OUTSIDE the platform (Bearer for POST /api/sms/send, body {to, text}); returned once, bound to the one number. manage + developer. |
numberId*, name* |
list_sms_api_keys |
List a number’s send keys (name, prefix, attach-service, timestamps) — never the secret. read + viewer. | numberId* |
revoke_sms_api_key |
Revoke a send key immediately; irreversible. manage + developer. | keyId* |
set_sms_inbound_webhook |
Point inbound messages + delivery events at your HTTPS URL (HMAC X-Tandem-Signature); returns the signing secret once. manage + developer. |
numberId*, url* |
remove_sms_inbound_webhook |
Stop forwarding inbound (messages still stored). manage + developer. | numberId* |
DNS records
| Tool | Purpose | Key params |
|---|---|---|
list_dns_records |
List records in the zone owning a hostname (+ zone metadata). Read-only. read. | hostname* |
create_dns_record |
Create a record (A/AAAA/CNAME/ALIAS/NS/TXT/MX/SRV/CAA). manage + developer. | hostname*, name*, type*, content*, priority, weight, port, flags, tag, ttl |
update_dns_record |
Replace a record by id (full desired fields, not a patch). manage + developer. | hostname*, recordId*, name*, type*, content* |
delete_dns_record |
Delete a record by id (platform-managed records protected). manage + developer. | hostname*, recordId* |
take_over_dns_record |
Claim a Tandem-managed web-serving record (apex/www, service routing) so your org edits it from then on — for hosting the site elsewhere while Tandem stays the nameserver. manage + developer. | hostname*, recordId* |
scan_public_dns |
Best-effort scan of an external domain’s publicly-visible DNS records (apex + common-name dictionary + extraNames, for A/AAAA/CNAME/MX/TXT/SRV/CAA). Dry-run diff — creates nothing. Non-authoritative (only sees records at probed names); do a full zone-file import when you have the zone file. manage + developer. |
hostname*, extraNames |
apply_dns_scan |
Create the records confirmed from a scan_public_dns preview (pass the toCreate record objects). Same validation/guards as create_dns_record; clashing/duplicate records are skipped, not fatal. manage + developer. |
hostname*, records* |
import_dns_zone |
Bulk-import a zone into an already-adopted zone. zoneFile accepts EITHER raw BIND text (cPanel /var/named/*.db) OR whmapi1 dumpzone JSON (auto-detected). Skips SOA/NS; platform-RRset collisions reported (not created) for take-over then re-import; dryRun returns the diff without writing. manage + developer. |
hostname*, zoneFile*, dryRun |
connect_cloudflare |
Connect the org’s Cloudflare account (API token with Zone:Read + DNS:Edit; optional Domain Registrar:Read/Edit). Validates by listing zones, stores the token encrypted, links every visible zone by default (linkZones = all/none/apexes). Linked zones take every DNS/attach/email tool via Cloudflare’s API — nameservers never move. Per-zone outcomes: linked / converted / already_linked / hosted_on_tandem / conflict_other_org / conflict_platform. manage + admin. |
organizationId*, apiToken*, label, linkZones |
list_cloudflare_zones |
The connection + every zone the token can see with Tandem’s link state, Cloudflare status, and registrar facts (expiry/autoRenew/locked) when permitted. connected=false when nothing is connected. read + membership. |
organizationId* |
link_cloudflare_zone |
Link zones from the connected account (hostnames[] or all=true); each is mirrored immediately. Idempotent. manage + developer. |
organizationId*, hostnames, all |
unlink_cloudflare_zone |
Stop managing one Cloudflare zone through Tandem (mirror removed; nothing at Cloudflare changes). manage + developer. | organizationId*, hostname* |
disconnect_cloudflare |
Delete the stored token and unlink every zone it backs; records at Cloudflare are untouched. manage + admin. | organizationId* |
Web analytics
| Tool | Purpose | Key params |
|---|---|---|
provision_analytics |
Enable Tandem Analytics on a service; injects the two TANDEM_ANALYTICS_* vars; returns the install <script>. |
serviceId* |
provision_external_analytics |
Enable analytics for a site hosted anywhere (no service, no env vars — the returned <script> is the whole integration). Idempotent per (project, domain). |
projectId*, domain* |
convert_analytics_to_external |
Rebind a service’s site as standalone external — same website id, full history preserved. Use when an app moves off Tandem. | serviceId*, domain |
set_analytics_custom_host |
First-party upgrade: serve the tag from the site’s own domain (default analytics.<domain>); auto-publishes DNS when the zone is hosted here, else returns the record to add. |
serviceId/siteId, host |
verify_analytics_custom_host |
Check the custom host resolves to the platform and activate it (route + cert publish; snippet switches over). | serviceId/siteId |
remove_analytics_custom_host |
Drop the custom host; the assigned host keeps serving so installed tags keep working. | serviceId/siteId |
get_analytics_install_snippet |
Return the install <script> for a site. |
serviceId or siteId |
list_analytics |
List a project’s analytics sites, service-bound and external (kind, externalDomain, site id). |
projectId* |
disable_analytics |
Disable a site (removes vars + website + data). | serviceId or siteId |
get_analytics_summary |
Headline metrics over a window (pageviews/visitors/visits/bounces vs previous window). | serviceId/siteId, range, startTime, endTime, filters |
get_analytics_breakdown |
Top values for one axis (type = referrer/country/url/device/event/…). |
serviceId/siteId, type*, limit |
get_analytics_timeseries |
Pageviews + sessions over time bucketed by unit (hour/day/month). |
serviceId/siteId, unit, timezone, range |
get_analytics_realtime |
Visitors active right now (~last 5 min). | serviceId/siteId |
get_analytics_markers |
Timeline of deploys + domain/DNS changes from the audit log (pair with timeseries). | serviceId/siteId, range |
get_ttfda_report |
PLATFORM ADMIN: time-to-first-deployed-app — median + p90 seconds from org creation to first healthy deploy, split by principalType and serviceType. Source is the platform DB (not Umami). Default range 30d. |
range, startTime, endTime |
delete_service also accepts analytics: keep|delete — keep (default) converts the service’s site to external so the history survives the deletion.
Scheduled jobs (cron)
Recurring HTTP hits to one of a service’s own container endpoints on a 5-field UTC cron schedule — a durable, serverless heartbeat (WP-cron, digest emails, cache warmers, agent tasks). No external URLs and no in-container exec in v1. The worker signs each request X-Tandem-Cron-Signature: sha256=<hex> where the signature is HMAC-SHA256(secret, "<X-Tandem-Cron-Timestamp>.<METHOD>.<path>"); the per-service signing secret is revealed to writers (developer+). Quotas: 8 jobs/service, 32/org, min interval 60s (cron granularity), timeout ≤ 120s, no overlapping runs.
| Tool | Purpose | Key params |
|---|---|---|
create_cron_job |
Schedule a recurring request to path on a service (defaults GET /). Returns the job + the per-service HMAC secret. developer + manage. |
serviceId*, name*, schedule*, path, method, headers, timeoutMs, enabled |
list_cron_jobs |
List a service’s jobs (schedule, next/last run, last status, consecutive failures); writers also get the HMAC secret. read. | serviceId* |
update_cron_job |
Change schedule/path/method/headers/timeout, or toggle with enabled; changing the schedule or re-enabling recomputes the next run + clears the failure counter. developer + manage. |
jobId*, schedule, path, method, headers, timeoutMs, enabled, name |
delete_cron_job |
Delete a job + its run history. developer + manage. | jobId* |
list_cron_runs |
Recent runs newest-first: started_at, duration, HTTP status, outcome (ok/http_error/timeout/no_upstream/error), error, response snippet. read. |
jobId*, limit |
Scheduling a COMMAND rather than an HTTP hit is the sibling surface: create_command_schedule and friends (see Projects & services above). Same cron grammar, same sweeper, but the payload is a catalog command exec’d in the container — so it is gated on the command’s own minimum role plus exec, not on cron’s flat developer + manage, and its history lives with the other command runs rather than in list_cron_runs.
Members
| Tool | Purpose | Key params |
|---|---|---|
list_members |
List an org’s members (humans + agents) with role/email/setup status; org admins+ also get each member’s product access as pillars {slug: level}. read. |
organizationId* |
invite_member |
Invite a human by email with a role, optionally placing them on the org’s products in the same call via pillars (owner-invite of an existing account is a pending invitation; its pillar levels apply at accept). Per-pillar outcomes; a refusal on one pillar doesn’t undo the rest. manage + admin. |
organizationId*, email*, role*, displayName, pillars |
set_member_role |
Change a member’s org role and/or product levels in one call (by userId or principalId; pillars level null revokes; can’t demote the only owner). manage + admin. |
organizationId*, role*, userId, principalId, pillars |
remove_member |
Remove a member (can’t remove the only owner). Pillar grants survive — the person becomes a grant-only guest; revoke explicitly. manage + admin. | organizationId*, userId, principalId |
Agency accounts
An agency is an org with agency mode on; a client is an org whose parent is an agency. The agency’s team is fanned out into every client as agency-managed memberships per its team defaults; hand-granted client memberships are never rewritten. Full walkthrough: Agency accounts. Every organizationId below is the agency org.
| Tool | Purpose | Key params |
|---|---|---|
set_agency_mode |
Turn agency mode on or off. Refuses on a client org (org_is_client) and refuses to turn off while clients remain (has_clients). manage + owner. |
organizationId*, isAgency* |
list_agency_clients |
One row per client: slug/name, tier, member + managed-member counts, project/service counts, last deploy time + state, plan code, subscription status, and billing (mode agency_paid/referral_discount/commission/null, payerIsAgency, plan, status, interval, discountBps, currentPeriodEnd, hasOwnCustomer — no live subscription = on trial). Also the agency’s tier (activeClients, discountBps, nextBand) and agencyHasPaymentMethod. read + admin on the agency. |
organizationId* |
create_client_organization |
The 60-second flow: create a client org, link it, fan the team in per defaults (creator always lands; promoted to owner if defaults leave the org ownerless), then put it on a plan billed to the agency at the agency’s tier discount (billing in the response: activated / not_activated with a code / skipped). payer_card_required = the agency has no card; the org exists on its trial — set_payment_method on the agency, then activate_client_billing. Optional client owner: new email → owner + returned setup link; existing account → pending invitation; failure reported in clientOwner, org survives. Rate-limited per agency. manage + admin on the agency. |
organizationId*, name*, clientOwnerEmail, clientOwnerDisplayName, planCode, interval |
activate_client_billing |
Start a client’s subscription on the agency’s card at the agency’s tier discount (10% at 1-4 active clients, 20% at 5-14, 30% at 15+; plan subscriptions only — domains, SMS, overage at list). For a client created before the agency had a card, or a linked client still on trial. Idempotent (already_active). Errors: not_a_client, already_client_paid, payer_card_required (the agency needs a card), plan_price_not_configured. manage + owner on the agency. |
organizationId*, clientOrganizationId*, planCode, interval |
handoff_client_billing |
Move an agency-paid client’s subscription onto the client’s own card with zero double billing (the client’s new subscription starts when the agency’s paid period ends: startsAt; the agency’s is cancelled then). mode referral_discount (client pays list minus the agency’s tier discount; agency earns nothing) or commission (client pays list; agency earns the tier difference per paid plan invoice). One-way. Emails the client’s owners. Errors: not_a_client, not_agency_paid, no_active_subscription, client_card_required (the client needs set_payment_method), plan_price_not_configured, provider_error. manage + owner on the agency. |
organizationId*, clientOrganizationId*, mode* |
set_client_pricing_mode |
Flip a self-paying client between referral_discount and commission from its next invoice. not_client_paid for an agency-paid client (hand it off instead), same_mode when unchanged. manage + owner on the agency. |
organizationId*, clientOrganizationId*, mode* |
list_agency_commissions |
The commission ledger: balance (unpaid / paid / currency), commissions (accrual per paid plan invoice of a commission-mode client with the snapshotted rate, or a signed adjustment with note; payoutId once paid) and payouts (manual, recorded by the platform team). read + admin on the agency. |
organizationId*, unpaidOnly, limit |
admin_list_agency_commissions |
Platform admin. Every agency’s balance (unpaid, paid, last accrual) — or, with agencyOrganizationId, that agency’s full ledger. read. |
agencyOrganizationId |
admin_record_agency_payout |
Platform admin. Record an out-of-band payout: settles every unpaid row for the agency in one transaction for exactly their sum; nothing_unpaid when the balance is zero or negative. Audited billing.agency.payout. manage. |
agencyOrganizationId*, method*, reference, note |
admin_adjust_agency_commission |
Platform admin. Signed non-zero correction to an agency’s ledger (positive credit, negative clawback) with a required note the agency sees. Audited billing.agency.commission_adjust. manage. |
agencyOrganizationId*, commissionCents*, note*, clientOrganizationId |
link_client_organization |
Adopt an existing standalone org you own as a client, then apply team defaults (summary returned). Refuses if the target already has a parent, is an agency / has clients, or is the agency itself. Pre-existing memberships stay unmanaged. manage + admin on the agency AND owner of the target. | organizationId*, clientOrganizationId* |
detach_client_organization |
Unlink a client: removes exactly the agency-managed memberships (+ their Launch access) and clears the parent; projects, domains and hand-granted members stay. Requires a non-managed owner on the client (no_independent_owner) and a self-paying client (billing_not_client_paid while the agency still pays). manage + owner. |
organizationId*, clientOrganizationId* |
list_agency_team_defaults |
Every agency member with their agency role and client default role (null = not fanned out). read + admin on the agency. |
organizationId* |
set_agency_team_default |
Set one member’s default client role (owner/admin/developer/viewer) or null to stop fanning them out. Bounded by your own agency role: a default of owner needs owner (role_escalation). Does not touch existing clients by itself. manage + admin on the agency. |
organizationId*, principalId*, defaultRole* |
apply_agency_team_defaults |
Re-sync managed rows in one client (or all): per client added/updated/removed + skipped (hand_granted: the client’s own grant outranks the default; last_owner: would orphan the client). Idempotent. manage + admin on the agency. |
organizationId*, clientOrganizationId |
Leaving the agency (remove_member on the agency org) revokes that person’s managed memberships in every client in the same operation.
Limits & policies
| Tool | Purpose | Key params |
|---|---|---|
get_limits |
Read a principal’s action mask, spend cap, and approval threshold. Omit principalId for your own live mask; omit organizationId for the global row. |
organizationId, principalId |
set_limits |
Set a principal’s limits (actionMask can only narrow). Org-scoped needs owner; global needs platform admin. manage. |
principalId*, organizationId, actionMask, spendCapCents, spendPeriod, approvalThresholdCents |
Agent credentials
Mint ONE credential for an agent and add capability later — without a new token. Twins of GET /api/admin/principals/:id/credentials and PATCH .../credentials/:credId.
| Tool | Purpose | Key params |
|---|---|---|
list_agent_credentials |
An agent’s active credential (plus revoked/expired history) with its action mask (null = unrestricted, the org role is the gate), project/service fence, expiry and last use. Look here when an agent is refused something its role allows: a viewer-minted agent carries ["read","logs","source:read"] — the read surface a human viewer holds — for good. Admin on the agent’s home org. read. |
principalId* |
update_agent_credential |
Change an existing credential in place — same token, new entitlements: widen/narrow actions (e.g. add logs; null = unrestricted, the only setting sister products accept), set/remove a projectId/serviceId fence, extend/clear expiresAt, rename. Omitted = unchanged; credentialId defaults to the active one. Audited; emits token.updated. Admin on the agent’s home org. manage. |
principalId*, credentialId, actions, projectId, serviceId, expiresAt, name, description |
Access review
Read-only. Twin of GET /api/admin/principals/:id/access.
| Tool | Purpose | Key params |
|---|---|---|
list_principal_access |
Every permission a principal holds in one org, all four axes at once: membership role + resource grants (ADD), limits action mask + per-credential action mask and project/service fence (SUBTRACT), plus a derived effective capability set per scope and per credential. Set groupBy: "pillar" for the Access page shape instead: one block per Tandem product (always all five, so “no access” is stated rather than inferred) carrying that product’s level and the grants filed under it, plus an org-wide block for grants that cover every product at once. A product’s level is its pillar grant — except Launch, whose level IS the membership role (levelSource: "membership"; change it with set_member_role), because inside Launch the org role is the permission ladder. Each grant says WHY — the level or capabilities, the scope in words (“all DNS zones in the organization” vs “this one record”), the origin (explicit / seeded / backfill / system), who granted it and when. The grouped shape is capped at 20 grants per block and the flat one is not, so prefer it for anybody who might hold many; page a single block with bucket (a pillar slug or org-wide) + offset. Omit principalId for your own; reviewing anyone else needs org admin. read. |
organizationId, principalId, groupBy, bucket, offset, limit |
Reading the response: membership and grants are why someone can do a thing;
limits and credentials are why they still cannot. effective.scopes is the
unfenced answer (every human session, and agent tokens minted without a
project/service fence); effective.credentials[] is the per-credential answer,
which differs whenever a token carries its own mask or fence. Everything under
effective is derived at read time from the other four — never stored.
Org guests
Read-only. Twin of GET /api/admin/organizations/:id/guests.
| Tool | Purpose | Key params |
|---|---|---|
list_org_guests |
Everyone present in an org holding no role-derived capability: members whose role is email (that role expands to an EMPTY capability set), plus principals holding an active resource grant in the org with no membership row at all. Per row: kind, display name, email, hasMembership, presentSince, and grantCount (a count — use list_principal_access for the grants). Requires org admin. read. |
organizationId |
Why it is not a filter on list_members: the second population is in no other
people list in the product, because every one of them is a query over
memberships. A grant can be written against any principal in the directory, so
a subject with no membership is a normal outcome of sharing — and until this tool
existed, an admin had no way to reach their access review.
A guest is a visibility category, not a capability one. Nothing about
list_org_guests changes what a guest may do: everything they can do comes from
an explicit grant, before and after. To promote one to a real member role, call
set_member_role with their principalId — it works whether or not a membership
row exists (for a grant-only guest it creates one).
grantCount: 0 on a row means a membership row that confers nothing and reaches
nothing. That is a prune candidate (remove_member), not a permission.
Access grants (Drive-style sharing)
The WRITE half of the access spine, generic across every resource type. list_principal_access above answers “what does this principal hold”; these answer “who holds access to this one thing, and change it”. Twins of GET|POST /api/admin/access/resources/:resourceType/:resourceId/grants and DELETE .../grants/:grantId.
A grant is (subject, resource, capabilities). It only ever ADDS access, is written at the resource’s scope path, and is inherited downward by prefix: granted on an organization it covers every project and service in it; on a dns_zone it covers every record in that zone; on a single dns_record it covers exactly that record. Same mechanism at three depths — only the path differs.
Capabilities are atoms like dns.write, db.read, source.write, mailbox.send, org.grants.write. They are frozen on the row at issue time, so editing a preset later never retroactively widens a grant already issued.
Authorization is not a role check. It is a capability held on that resource, which a grant can confer as readily as an org role — so someone given one zone can share that zone and nothing else.
| Tool | Purpose | Key params |
|---|---|---|
list_grants |
Everyone holding an active grant on one resource: grant id (pass to revoke_access), subject principal, capability atoms, scopePath, origin, expiry. Requires org.grants.read on the resource. read. |
resourceType*, resourceId* |
grant_access |
Give an EXISTING principal (human or agent) capabilities on a resource. Re-granting REPLACES the active grant and keeps the previous one as history. Requires org.grants.write on the resource and that you already hold every capability you are granting. manage. |
resourceType*, resourceId*, capabilities*, principalId, email, expiresAt, note |
revoke_access |
Revoke one grant by id. TOMBSTONE, not delete — the row remains as the audit trail. Requires org.grants.write on the resource; does not require holding the capabilities being revoked, and does not block revoking your own grant. manage. |
resourceType*, resourceId*, grantId* |
resourceType is one of organization, project, service, database, storage_bucket, redis_instance, mailbox, email_domain, git_repo, registered_domain, dns_zone, dns_record, phone_number. (pillar is a resource type but is not addressable by id here — a pillar grant is keyed by slug.)
The four refusals from grant_access, and why each exists:
| Code | Meaning |
|---|---|
unknown_capability |
The capability vocabulary is closed. An unrecognised atom would be stored verbatim and then silently dropped at evaluation — access that looks granted and confers nothing, which is worse than a clean refusal. |
self_grant |
No principal may create or widen a grant whose subject is themselves — including owners. Without it, anyone holding org.grants.write can promote themselves to anything, so one compromised session is unbounded. Ask another owner or admin. Self-demotion (revoking your own grant) stays allowed. (Platform staff needing to enter a customer pillar for support do not get an exemption here either — they use the separate, time-boxed admin_grant_support_access ceremony.) |
escalation |
You cannot grant a capability you do not hold yourself. The error names exactly which ones were missing, and nothing is granted — not even the subset you could have given. |
forbidden |
You lack org.grants.write on this resource. Also the answer when the resource does not exist, deliberately, so this surface cannot be used to probe for ids. |
It does not invite. Granting to an email with no Tandem account returns principal_not_found; create the account first (invite_member), then grant. Sharing with a stranger by email — creating the account, emailing a setup link, ensuring a membership — exists per-resource today (share_mailbox, share_git_repo) and has not been generalized.
Pillar access (which Tandem products an org uses)
Tandem is five sister products — pillars: Launch, Work, Speak, Grow, Profit. pillar_directory is the static, org-independent map of where they live. These tools answer the org-specific questions: which of them does this organization use, and who may use them. Twins of /api/admin/access/organizations/:organizationId/pillars/....
A pillar grant is (principal, organization, pillar) — it has no row id, so it is not addressable through list_grants / grant_access (those answer unsupported_resource_type for pillar). Hence a separate set of five tools; everything after resolution is the same code, so the delegation rules are identical.
Two levels, and the difference is the whole model:
- Adoption is per ORGANIZATION, once, ever.
activate_pillarrecords that the company uses the product and seeds owner-level access for every current owner — including owners promoted years later. The click moves from per-person to per-org-per-pillar, the one place it does not accumulate. - Access is per PRINCIPAL, always an explicit row. Every pillar is closed by default, including Launch. Being an org admin implies nothing about Work; being a Work owner implies nothing about infrastructure.
Absence of a row means something: “does this organization use Work?” stays answerable from the data, which is why nothing is seeded eagerly.
| Tool | Purpose | Key params |
|---|---|---|
list_org_pillars |
One row per pillar: product status (live/beta/coming_soon), whether this org has adopted it and when, your own level (null = no access — normal, not an error), plus canActivate / canViewAccess / canManageAccess, each mirroring the exact server gate so you never attempt a call guaranteed to fail. Requires membership of the org in any role. read. |
organizationId* |
activate_pillar |
Turn a product on for the org. Seeds OWNER-level access for every current owner (humans and agents alike) in one transaction. Org owner only. Idempotent — a repeat returns activated: false, seeds nothing, and is not an error. manage. |
organizationId*, pillarSlug* |
list_pillar_access |
Everyone with access to one pillar, with each subject’s level and grant time. Requires org.grants.read on that pillar. read. |
organizationId*, pillarSlug* |
grant_pillar_access |
Give an EXISTING principal a level: viewer, member, manager, admin, owner. Re-granting REPLACES the active grant and keeps the previous row as history. manage. |
organizationId*, pillarSlug*, level*, principalId|email |
revoke_pillar_access |
Remove one principal’s access, addressed by principal (a pillar grant is the pair), so a repeat returns revoked: false rather than 404. Tombstone, not delete. manage. |
organizationId*, pillarSlug*, principalId* |
What the levels mean. On the spine, deliberately coarse: viewer/member/manager confer pillar.access (“may enter this pillar at all”); admin/owner also confer pillar.admin (“may administer it”). Everything finer — who may close a task, who may see a budget — is that pillar’s own authorization model, keyed on the level shipped here. The spine does not model five products’ internals.
Why activate_pillar needs OWNER and not admin. It is the only write on this surface that creates authority instead of passing it on: it seeds owner-level grants for people who held nothing a moment earlier, so the rule that bounds every other grant — “you cannot grant what you do not hold” — cannot bound it, because there is nothing held yet. Minting owner-level access to a product takes the role that already mints owners. An admin gets forbidden_owner_required.
Why activation seeds every owner rather than whoever clicked. “Whoever activates becomes the pillar owner” is a self-grant wearing a hat, and the no-self-grant rule exists to stop authority appearing that way. Seeding all current owners makes it a system act on the organization instead.
Pillar-specific refusals, on top of the four from grant_access:
| Code | Meaning |
|---|---|
forbidden_owner_required |
activate_pillar from an admin (or anyone below owner). Ask an owner — this one is not delegable. |
pillar_not_available (409) |
The pillar is coming_soon. Adopting it would record the org as using a product nobody can reach and seed owner grants conferring nothing. Nothing to do until it ships. |
unknown_pillar (404) |
No pillar by that slug. list_org_pillars or pillar_directory has the real ones. |
last_pillar_owner (409) |
The revoke (or the demotion) would leave the pillar with no owner. Self-grant is forbidden, so nobody could ever grant it back — promote another owner first. Counts owner-level rows specifically: “an admin remains” is not a way out, because an admin cannot appoint owners. |
forbidden |
On grant/revoke/list-access: you lack org.grants.write/read on the pillar — or the org has not adopted it, or it is not reachable yet. Deliberately the same answer, so this surface cannot be used to probe which orgs exist or which pillars are about to launch. Call list_org_pillars to see the state honestly. |
The order of operations, for an agent starting cold: list_org_pillars → if activated: false and canActivate: true, activate_pillar → you now hold owner if you are an org owner → grant_pillar_access for everyone else. If activated: true and your own level is null, you hold nothing to pass on: ask someone who does (list_pillar_access if canViewAccess), because rule 3 applies to pillars exactly as it applies to everything else.
Billing
| Tool | Purpose | Key params |
|---|---|---|
get_billing_status |
Read funding provider, tier (preview = the free trial, which does deploy; paid = subscribed), previewExpiresAt, billing/deploy-gate flags, customer + default payment method, subscription, blockers. hasPaymentMethod and tier are independent — a card does not move an org off preview. read + viewer. |
organizationId* |
get_subscription |
Read the latest subscription (plan/status/period/cancelAtPeriodEnd). { subscription: null } just means the org is on (or has lapsed from) the free trial. read + viewer. |
organizationId* |
list_invoices |
List invoices newest-first, live from the provider (hosted + PDF links). read + viewer. | organizationId*, limit |
get_usage |
Summarize tier + previewExpiresAt/previewServiceLimit + billable resource counts (services/DBs/buckets/domains) + application egress over the last 30 days (egress30d: bytes, gb, metered). metered: false means egress was not measured for the org, not that it was zero. Also returns 30-day daily averages for the two gauge metrics: services30d (avgDaily, metered) — the average billable services held per day, distinct from the live resources.services count — and dbStorage30d (avgDailyBytes, avgDailyGb, metered) for tenant-database storage. Each gauge’s metered: false means no sample was recorded (poller off, or every database unreachable); the average is meaningless then and must not be read as zero usage. On a trial, compare the live counts against the 1 service / 1 database / 1 bucket allowance. Also returns currentPeriod: the projected overage for THIS calendar month from the shared usage aggregator (the same computation the invoice and the allotment warnings use), with per-metric used/included/overageQuantity/unitPriceCents/overageCents in each metric’s billable unit (GB-month, “1,000 emails”, service-month, GB), the flat SMS addOns (no allotment; charge from published rates), and projectedOverageCents (unitPriceCents: null means the plan doesn’t meter that metric). And overagePeriods — overage close-out history newest-window-first (periodStart, periodEnd, mode shadow|live, status, totalOverageCents, providerInvoiceId); mode: "shadow" rows are “what you WOULD be billed” before real charging is enabled, and the list is empty until the overage engine is turned on. Display only — nothing here charges or blocks. Mirrors the portal Billing → Usage + “Projected overage this period” cards. read + viewer. |
organizationId* |
set_payment_method |
Begin attaching a payment method (hosted setupUrl + SetupIntent; no charge). Step 1 of 2 — a card alone leaves the org on the trial; follow with create_subscription. manage + owner. |
organizationId* |
list_plans |
List subscribable plans (pro, scale — starter is grandfathered-only and not listed) with allotments, metered overages, and BOTH cadences priced: monthlyPriceCents, annualPriceCents, per-interval intervals.{month,year}.{offered,priceCents,priceAvailable}, and annualSavings (cents / percent / months free). read + admin. |
organizationId* |
create_subscription |
Upgrade the org to a paid plan (idempotent). The only call that ends the trial: lifts preview_expired, drops the trial resource caps, un-suspends + redeploys reclaimed services, and unlocks custom domains, outbound email and managed Redis. Needs a card on file (else card_required → set_payment_method). planCode defaults to pro when available; starter returns plan_not_offered (grandfathered-only). interval is month (default) or year for annual billing — check list_plans first, an unavailable cadence returns plan_price_not_configured. Does NOT switch an existing subscription between cadences. manage + owner. |
organizationId*, planCode, interval |
change_plan |
Move an org’s active subscription to a different plan in place (e.g. starter→pro, pro→scale) — a plan change, not cancel+resubscribe. Swaps the subscription’s price and prorates the mid-cycle switch (credit for unused time on the old plan, prorated charge for the new). Requires an active subscription (no_active_subscription otherwise — use create_subscription first). Errors: plan_unknown, plan_not_offered (starter is grandfathered-only — changing TO it is blocked), same_plan, plan_price_not_configured (check list_plans). interval month (default) or year. manage + owner. |
organizationId*, planCode*, interval |
defer_org_billing |
Platform admin only. Defer an org’s billing until a date — charge them, but not before then (typically a client who prepaid a previous host through that date). Applies immediately to an existing subscription and is also recorded for a not-yet-subscribed org to pick up at subscribe time; enforced provider-side as a trial, so billing resumes automatically. Distinct from the billingEnabled comp switch (comp = never charge, deferral = start later; an org may carry both) and from the free trial (which precedes any subscription). Domain fees still charge, and deferral does not credit already-paid time. deferUntil: null clears it. manage + platform admin. |
organizationId*, deferUntil*, reason |
Annual billing. Interval is a dimension of a plan’s price, not a separate
plan: annual Pro and monthly Pro grant identical entitlements (services,
allotments, domain credits) and differ only in price and cadence. Annual is
priced at 10x the monthly rate (“2 months free”). Included allotments are always
per calendar month — an annual Pro org gets 10 GB storage per month, not
120 GB for the year; list_plans states this as allotmentPeriod: "month".
The portal Billing page offers the same monthly/yearly choice to humans.
Support tickets
Support is staffed by humans and AI agents; set handlerPreference (human / agent / any) to say who you want. There is no push channel on MCP — poll list_support_tickets and check unread. Requesters are also notified by email (agent-opened tickets email the org owners) and, if configured, an org webhook.
| Tool | Purpose | Key params |
|---|---|---|
open_support_ticket |
Open a ticket with the platform team (platform-side problems, questions, escalations). manage + developer. | subject*, body*, organizationId, priority, handlerPreference, projectId, serviceId |
list_support_tickets |
List the org’s tickets, newest activity first; unread=true means support replied since last read — poll this. read + viewer. |
organizationId, status, includeClosed |
get_support_ticket |
Read a ticket’s full thread; reading marks it read for your org. read + viewer. | ticketId* |
reply_support_ticket |
Reply and/or close (close:true); replying to a waiting/resolved ticket reopens it. manage + developer. |
ticketId*, body, close |
get_support_webhook |
Show the org’s ticket-update webhook config (secret never returned). read + admin. | organizationId |
set_support_webhook |
Register an HTTPS endpoint for HMAC-signed support.ticket.updated POSTs (verify X-Tandem-Signature); secret returned once; url:null removes. manage + admin. |
url*, organizationId |
admin_list_support_tickets |
PLATFORM ADMIN: the cross-org support queue; filter by status/handlerPreference/unreadOnly. | status, handlerPreference, organizationId, unreadOnly, includeClosed |
admin_get_support_ticket |
PLATFORM ADMIN: full thread including internal notes; marks platform-side read. | ticketId* |
admin_reply_support_ticket |
PLATFORM ADMIN: reply to the requester (notifies them), or internal:true for a platform-only note. |
ticketId*, body*, internal |
admin_update_support_ticket |
PLATFORM ADMIN: triage — set status (resolved notifies requester), assign to a platform-admin principal, set priority. | ticketId*, status, assigneePrincipalId, assigneeDisplay, priority |
admin_grant_support_access |
PLATFORM ADMIN: take a time-boxed support session in a customer org’s pillar (the sanctioned alternative to self_grant). Grants YOURSELF the level (capped below owner) until expiry (default 120 min, max 480); origin support is visible to the org, the reason is audited, and the org may revoke it. Refuses unadopted pillars (pillar_not_activated). |
organizationId*, pillarSlug*, level*, reason*, minutes |
admin_end_support_access |
PLATFORM ADMIN: end a support session early — your own by default, or another operator’s. Only support-origin grants; org-created access answers not_a_support_grant. Idempotent. |
organizationId*, pillarSlug*, subjectPrincipalId |
admin_list_support_access |
PLATFORM ADMIN: every live support session platform-wide — who from staff is in which org’s pillar, at what level, why, and when it expires. | — |