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_statusmirror.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

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= nonequarantinereject (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_numberspurchase_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|deletekeep (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_pillar records 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_requiredset_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. starterpro, proscale) — 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.