For agents & automation
Plinth for agents.
Self-register a workspace, seal provider keys, send mail, and manage quotas — entirely over MCP or REST. One bootstrap call gives your agent endpoints, config snippets, and the full registration playbook.
MCP at https://plinth.tools/mcp · same tools via REST
How it works.
- 1
Fetch bootstrap
GET /api/v1/agent/bootstrap returns endpoints, MCP client JSON, registration steps, and grouped tools. No auth required for the static half.
- 2
Paste MCP config
Drop the Cursor or Claude snippet into your agent. Set PLINTH_API_KEY to the app key from /apps (shown once at create time).
- 3
Call workspace.whoami
Smoke-test the key, then follow registrationFlow for a new org or dailyOperations for sends and quotas.
Endpoints (this deployment)
MCP Streamable HTTP
https://plinth.tools/mcp
POST · JSON-RPC · Bearer plinth_…
REST shim
https://plinth.tools/api/v1/call
POST · { "name", "arguments" }
Agent bootstrap
https://plinth.tools/api/v1/agent/bootstrap
GET · public or Bearer for workspace stats
Tool catalog
https://plinth.tools/api/v1/tools
GET · flat list + grouped categories
Health
https://plinth.tools/api/health
GET · fast probe
Connect your agent.
Pick your runtime. Every snippet uses this deployment's live URLs — copy as-is, swap in your API key.
Add to ~/.cursor/mcp.json (or project .cursor/mcp.json). Set PLINTH_API_KEY in your shell or Cursor env.
{
"mcpServers": {
"plinth": {
"url": "https://plinth.tools/mcp",
"headers": {
"Authorization": "Bearer ${PLINTH_API_KEY}"
}
}
}
}Self-register a workspace.
New orgs prove business-email ownership once. An agent with a workspace key can then manage apps, providers, routing, and mail without opening the dashboard. Follow registrationFlow from bootstrap — or this checklist:
- 1
onboarding.discover_organization
Check whether the work email domain can claim a workspace.
- 2
onboarding.request_email_verification
Email a one-time verification code (purpose domain_claim).
- 3
onboarding.verify_email
Consume the inbox token.
- 4
onboarding.register_workspace
Create the workspace (or request_access if already claimed).
- 5
onboarding.select_stack
Pick app modules and provider brands.
- 6
provider_connections.connect_workspace
Seal shared Brevo / Resend keys once at workspace level (use connect per-app only for overrides).
- 7
mcp.create_app_connection
Mint an app + scoped API key (shown once).
- 8
apps.set_email_routing
Optional: round-robin / failover across email providers.
- 9
regulation.apply_presets
Apply TOS rate-limit presets (also auto on connect).
- 10
workspace.whoami
Smoke-test the key before sending mail.
Example — step 1 (discover organization)
curl -s -X POST https://plinth.tools/api/v1/call \
-H "Authorization: Bearer $PLINTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "onboarding.discover_organization",
"arguments": { "email": "[email protected]" }
}' | jq .Free-mail domains (gmail.com, etc.) cannot claim a workspace. Matches the Get started browser flow.
After you're set up.
Common agent tasks — also listed as dailyOperations in bootstrap:
emails.sendSend transactional mail (html/text/body).
emails.send_templateSend using a saved template key.
emails.ensure_templateFetch-or-create a template (agent recovery primitive).
emails.get_message_statusDelivery / engagement for one message id.
apps.get_email_routingRead pool, strategy, failover settings.
regulation.previewRemaining quota before a blast.
governance.export_audit_logExport recent audited actions.
Full tool catalog.
67 tools across 12 groups. Live schemas via /api/v1/tools or MCP tools/list.
Agent bootstrap1 tools ▾
agent.get_bootstrapReturn everything an autonomous agent needs to wire Plinth MCP: endpoint URLs, Cursor/Claude config snippets, the workspace registration flow, daily-operation shortcuts, and the full tool catalog grouped by vertical. Call this first when onboarding a new agent or CI job. Pass `baseUrl` only when the public origin differs from the deployment default. Requires a Bearer API key; unauthenticated callers should use GET /api/v1/agent/bootstrap for the static half.
Onboarding & workspace8 tools ▾
onboarding.get_catalogShow what apps and providers a tenant can pick during setup. Use when answering 'what can Plinth do?' or 'which providers are supported?' before kicking off registration.
onboarding.discover_organizationLook up an email's domain and tell me whether it's free for a new workspace, already claimed (the user should request access), or a personal/free-mail domain (rejected). Use before calling register_workspace; the discovery decides which path to take.
onboarding.request_email_verificationSend a one-time code to an email so the user can prove ownership. Use before claiming a domain (purpose=domain_claim), requesting access to an existing org (purpose=access_request), or signing in passwordless (purpose=magic_link). The token is delivered out-of-band; this call just returns the TTL.
onboarding.verify_emailConsume a verification token returned from the user's inbox. After this succeeds, register_workspace / request_access / login can use the same (email, token) within the freshness window.
onboarding.register_workspaceCreate a new tenant workspace and atomically claim the owner's email domain. Use when the user says 'create my workspace', 'sign up our company', or 'set up Acme Inc on Plinth'. Pass a fresh verificationToken from verify_email. Errors: DOMAIN_ALREADY_CLAIMED if someone beat you to it, PERSONAL_EMAIL_REJECTED for free-mail domains.
onboarding.request_accessAsk to join an existing workspace already registered for this email's domain. Use when discover_organization returned 'claimed' and the user wants in. Requires a fresh email verification.
onboarding.select_stackPick which app modules and provider stack the tenant wants Plinth to manage (e.g. 'we want auth + email through Brevo'). Use after register_workspace, when the user is choosing capabilities.
onboarding.get_setup_statusWhere am I in the onboarding flow? Returns registration, provider, and app-connection completeness. Use to drive a checklist UI or to answer 'what's left to set up?'.
Workspace identity1 tools ▾
workspace.whoamiTell me which workspace, user, and roles this API key is bound to. Use this when the agent needs to confirm context before doing anything destructive, when the user asks 'who am I?' or 'which workspace?', or as a smoke test that the key works.
Team2 tools ▾
team.list_membersList human members of the authenticated workspace (email, role, status, last seen). Use when an agent needs the org roster, when reconciling access, or before inviting someone who might already be a member. Requires a workspace API key with this tool allowed.
team.invite_memberInvite a teammate by email with role admin or member (owner cannot be assigned via invite). Sends the same invite email as the dashboard when mail is configured. The API key must have been minted with the inviter's Plinth account email as actorId (the dashboard does this automatically when you create an app key while signed in).
Apps & routing5 tools ▾
apps.get_email_routingShow how this app is currently routing email sends — which connected providers it may use (the pool), the rotation strategy, optional rate-limit failover across the pool, and what the workspace default would be if the pool is empty. Use to answer 'is this app on Brevo or Resend?' or before flipping with apps.set_email_routing.
apps.set_email_routingConfigure how this app picks an email provider per send. Strategies: 'single' uses providers[0] (or the workspace default when providers is empty); 'round_robin' atomically cycles through providers[] using a per-app cursor (even split across providers per send, deterministic); 'random' picks uniformly per send. When `failoverOnRateLimit` is true and at least two providers are in the pool, a send that hits a Plinth `RATE_LIMITED` gate (strict daily quota / token bucket) on one provider automatically retries the next pool member before failing the call. Validates each brand is actually connected for this workspace and resets the rotation cursor on save. Use when the user says 'split this app's email between Brevo and Resend' or 'move this app to Resend only'. Audited as mcp.app_connection.email_routing_changed.
apps.get_email_allocationShow workspace-wide per-app email allocation: whether the UTC-day pool split is enabled, the effective daily pool cap (explicit override or sum of workspace-wide regulation send limits), each app's share percent, today's accepted send count vs that app's cap, and optional per-recipient reservations inside each app's share. Use before changing shares or when debugging `app_email_allocation_exceeded` / `app_recipient_allocation_exceeded` declines.
apps.set_email_allocationReserve a percentage of the workspace's UTC-day email pool for each app. Shares must sum to at most 100%. When enabled, sends that would push an app past floor(pool × sharePercent / 100) accepted messages today are politely declined with `declineReason='app_email_allocation_exceeded'` (whole batch when no per-recipient reservations exist). Optionally reserve sub-caps inside an app's share for specific recipient addresses via `recipientReservations`; reserved caps must sum to at most that app's daily cap, and remaining capacity is shared by all other recipients. Per-recipient over-cap sends are declined with `declineReason='app_recipient_allocation_exceeded'` (partial batch allowed). `dailyPoolCap` overrides the auto-derived pool (sum of workspace-wide regulation `sends` burst capacities); omit or null to use auto. Audited as email.app_allocation.updated.
apps.set_provider_credential_modeChoose whether an app inherits workspace-level provider credentials (default) or uses only its own sealed keys. When inheriting, sends use workspace keys for any brand the app has not connected itself. Audited as mcp.app_connection.provider_inheritance_changed.
Provider connections2 tools ▾
provider_connections.connectConnect a third-party provider (Brevo, Stripe, Resend, …) by sealing its API key onto a SPECIFIC app. Use when the user says 'connect Brevo to my marketing app' or 'add Stripe keys to the checkout app'. Apps that inherit workspace providers use `provider_connections.connect_workspace` for shared keys instead. Per-app credentials — every credential pair belongs to exactly one app, so two apps in the same workspace can each carry a distinct Brevo / Stripe / Clerk subscription when not inheriting. Credentials are encrypted with XChaCha20-Poly1305 + per-workspace HKDF; only redacted previews are echoed back. The owning `appId` is REQUIRED and must be one this workspace already owns; pass the calling app's id when in doubt. For Resend, optional `resendAccountTier` (`free` default, or `paid`) controls which regulation presets are auto-applied — Plinth cannot read Resend billing from the API.
provider_connections.connect_workspaceConnect a provider at workspace level so every app that inherits workspace keys can send through it without pasting the same API key again. Seals credentials on the hidden workspace self-app; visible apps with `inheritWorkspaceProviders=true` (the default) resolve these keys automatically unless they define their own override for that brand. Use before creating apps when the operator says 'connect Brevo once for the whole org'. Same validation and audit as `provider_connections.connect`.
Email send & templates25 tools ▾
emails.sendSend an email right now to one or more people. Use when the user says 'email Alice the invoice', 'send a one-off message to [email protected]', 'reach out to the support team', or 'fire off this update to <list>'. Don't use a template — just paste the body. Pass `body` as plain text or HTML and Plinth picks the right field; or pass `html` / `text` explicitly. `to` accepts a single address, an array, `{ email }`, an array of `{ email }`, or 'Name <email>' format. For saved templates and `templateKey`, use `emails.send_template` instead — this tool rejects unknown keys (e.g. `templateKey`) so mistakes fail fast. `from` defaults to the workspace's verified sender, and keeps a display name when given as 'Customer Care <[email protected]>'. `cc` / `bcc` take the same address forms as `to`; `attachments` takes { filename, content (base64), contentType? }.
emails.send_templateSend an email using a saved template, filling in {{variables}}. Use when the user says 'send the welcome email to [email protected]', 'fire the receipt template at this customer', or 'use my magic-link template for bob'. Pick the template by `templateKey` (e.g. 'welcome', 'receipt'). Pass any handlebars values in `variables`. `to` accepts one address, many, `{ email }`, or `[{ email }]`. For one-off HTML/text without a saved template, use `emails.send`. Unknown keys are rejected.
emails.save_templateCreate or update a reusable email template. Use when the user says 'save this as the welcome email', 'make a template for password resets', or 'update the receipt template's wording'. Templates are workspace-wide — any app with the Manage templates capability can send them. Use {{handlebars}} for variables; Plinth auto-detects them if you don't pass `variables` explicitly. Re-saving an existing key OVERWRITES it (this is the edit path). For an idempotent 'create-only-if-missing' primitive that won't clobber a hand-tuned production template on retry, use `emails.ensure_template` instead.
emails.ensure_templateIdempotent fetch-or-create for an email template. The agent recovery primitive: when `emails.send_template` returns `TEMPLATE_NOT_FOUND`, follow the `details.recovery` hint and call this — it returns the existing template if one is already saved at `key`, otherwise installs a curated preset (`fromPreset`) or creates from your inline payload (`inline`). Never overwrites an existing template — pick `emails.save_template` if you want the upsert. Returns `{ template, created, source }` so an agent can log whether the call was a no-op (`source: "existing"`), a preset install (`"preset"`), or a fresh inline create (`"inline"`).
emails.list_presetsBrowse Plinth's curated, opinionated email-template presets (welcome, magic-link, OTP, click-to-verify, receipt, password reset, trial-expiring, …). Use to discover what `fromPreset` keys `emails.ensure_template` accepts, before deciding whether to install a preset or compose your own. Read-only and stateless — safe to call any time. Filter by `category` ("auth" | "lifecycle" | "transactional" | "marketing") to narrow the list.
emails.compose_templateConversational email-template composer. Use when the user says 'draft a welcome email', 'help me write a receipt template', or any open-ended template ask. Pass the full conversation history each turn (user + assistant); Plinth either replies with `{ kind: 'ask', followups }` to gather more context or `{ kind: 'draft', draft }` once it has enough. The returned draft is NOT saved automatically — call `emails.save_template` with the draft fields to persist it. Uses the workspace's connected LLM provider (e.g. OpenAI in /connections/llm) when present; falls back to a deterministic heuristic and returns `heuristic: true` when no LLM is connected.
emails.list_templatesShow every email template saved in this workspace. Use when the user asks 'what templates do I have?', 'list my emails', or before picking one for emails.send_template. Newest first.
emails.delete_templateDelete a saved email template by key. Use when the user says 'remove the old welcome template' or 'delete the receipt one'. Idempotent for callers — sending after delete will fail with NotFoundError.
emails.add_contactAdd or update an email contact, optionally putting them on contact lists. Use when the user says 'add [email protected] to my newsletter list', 'create a contact for bob with name Bob Smith', or 'put this person on the customers list'. Idempotent on email; calling again updates name/attributes/lists. Pass `lists` as a string ('newsletter') or array (['newsletter','customers']).
emails.list_contactsList contacts, optionally scoped to a single list. Use when the user asks 'who's on my newsletter list?', 'show all contacts', or before sending a one-off campaign. Pass `list` to filter ('list: "newsletter"').
emails.unsubscribeSuppress an email address so future sends skip them. Use when the user says 'unsubscribe [email protected]', 'this address keeps bouncing', or after a webhook reports a complaint. Idempotent. Suppressions persist forever unless the user explicitly removes them via the dashboard.
emails.create_newsletterCompose a newsletter as sections (intro + items + outro). Use when the user says 'draft this week's newsletter', 'create a digest about X', or 'put together our monthly update'. Sections render to a clean responsive HTML wrapper at send time — you don't write HTML yourself. Re-saving the same key overwrites it (drafting workflow).
emails.send_newsletterBroadcast a saved newsletter to every active contact on a list. Use when the user says 'send the may digest to my newsletter list', 'broadcast our launch update', or 'fan out the digest to subscribers'. Honors the suppression list automatically. Returns total / sent / failed / skipped counts.
emails.list_newslettersShow every newsletter saved in this workspace. Use when the user asks 'what newsletters do I have?' or before picking one to send.
emails.get_tag_statsReport how many emails each tag sent, and how they landed. Use when the user asks 'how many invoice emails went out this month', 'which campaign sent the most', 'break my sends down by tag', or 'how did the onboarding tag perform'. Scoped to the calling app by default — pass `appConnectionId: null` for the whole workspace. Returns per-tag totals plus delivered / opened / clicked / bounced / declined counts, and an `untagged` figure so you can see how much traffic carries no tag at all. A message with several tags counts under each one, so the columns add up to more than the message total. Tag your sends via the `tags` field on `emails.send` / `emails.send_template` for this to be useful.
emails.get_message_statusFetch the live delivery + engagement status of one previously-sent email. Use when the user (or your own retry loop) asks 'did the welcome email to [email protected] land?', 'has the receipt been opened?', or 'what's the state of message X?'. Pass the `messageId` returned by `emails.send` / `emails.send_template` (NOT the provider's own id). Plinth stitches three sources: the local message row, any webhook events recorded so far, and a live lookup against the configured provider (Brevo `/smtp/statistics/events`, Resend `GET /emails/:id`). Local data is returned synchronously; the provider lookup happens automatically when status is still 'sent' with no engagement and there's a providerMessageId. Set `refresh: true` to force a fresh provider call even when local data looks complete (counts toward the email vertical's budget). Returns `{ message, state, events, providerLookup }` — `state` is the highest-reached state across the merged sources (queued → sent → delivered → opened → clicked → bounced/complained/failed). `providerLookup.notFound` indicates the provider has no record (e.g. message id outside their retention window) — agents should not retry that lookup.
emails.list_messagesPaginated message log for the current workspace. Use when the user asks 'what did we send today?', 'show me everything that bounced this week', 'find sends to [email protected]', or your agent needs to drain the recent log to reconcile state. Filters compose: any combination of `from` / `to` (ISO timestamps, half-open window), `status` (queued/sent/rejected/failed at submit time), `kind` (transactional/newsletter/template), `templateKey`, `newsletterKey`, `toEmail`, and `appConnectionId` (the AppConnection.id of the originating app — pass null to scope to dashboard sends). Returns `{ messages, nextCursor }`; thread `nextCursor` back into a follow-up call to drain pages. Each row carries the merged engagement state (deliveredAt / openedAt / clickedAt / openCount / clickCount) so you don't need a follow-up `get_message_status` per row. Per-message provider lookups are NOT performed by this tool — call `emails.get_message_status` on a specific row when you need fresh provider state.
emails.get_webhook_endpointShow the webhook URL Plinth listens on for a given email provider on a SPECIFIC app, plus whether the signing secret is wired and whether the operator has to paste anything into the provider's dashboard. Per-app credentials (0013): each app's Resend / Brevo subscription gets its own URL + secret, so two apps in the same workspace never share a webhook. Use when the user asks 'what's my Resend webhook URL?', 'is the Brevo webhook configured?', or before troubleshooting why deliverability events aren't flowing. `appId` defaults to the calling app when omitted.
emails.ensure_webhookRegister (or refresh) Plinth's webhook on the provider's side for ONE app. For Resend this is a real `POST /webhooks` API call that returns the Svix signing secret — Plinth seals it into the same credentials envelope as your API key. For Brevo (no public webhook-management API) this just generates a random shared token; the operator must paste it + the URL into Brevo's dashboard. Idempotent: if an endpoint already exists for this app, the existing registration is updated in place rather than duplicated. Per-app credentials (0013): app A and app B can each register a Resend webhook for their own subscriptions and they won't collide. Use when the user says 'set up Resend webhooks for my marketing app' or after a connect that didn't auto-register. Audited as `email.webhook.ensured`. `appId` defaults to the calling app when omitted.
emails.rotate_webhook_secretIssue a fresh signing secret for the provider's webhook on ONE app (Resend rotates the Svix signing key in place; Brevo regenerates the shared token). Per-app credentials (0013): rotation only affects the named app's webhook — sibling apps' secrets are untouched. Use during routine secret rotation, after a suspected compromise, or when migrating between environments. Audited as `email.webhook.rotated`. NOTE: for Brevo, the rotated token must be pasted into the dashboard before it takes effect — until then, inbound webhooks will 401. `appId` defaults to the calling app when omitted.
emails.list_verified_sendersList the senders / domains the upstream provider considers verified for ONE app — the addresses the operator can actually send `from` without bouncing on a 'sender not authenticated' error. Per-app credentials (0013): the lookup is scoped to a single (appId, provider) pair so two apps' Brevo / Resend subscriptions never bleed verified-sender state across each other. Use BEFORE composing a send when you need to pick a `from` address you didn't already pin on the provider connection — much faster than asking the operator to retype, and avoids the deliverability quota burned by 'sender not verified' rejections. Also surfaced in the dashboard's 'fix sender' dialog so the operator can pick from a verified list with one click. Returns `{ providers: [{ provider, ok, supported, senders: [{ kind, email?, domain?, name?, verified, active, status, providerId }] }] }` — `kind: 'address'` is a specific email (Brevo's per-sender verification), `kind: 'domain'` covers any local-part of that domain (Resend / SES). When `provider` is omitted Plinth queries every email connection on the app and merges the results; pass `provider: 'brevo'` (or `'resend'`) to scope to one. Adapters that don't expose a verified-sender list (e.g. legacy SMTP) return `supported: false` so the caller can degrade gracefully. `appId` defaults to the calling app when omitted; required from dashboard contexts where the request isn't already bound to one.
emails.list_recent_eventsRecent EmailEvent rows across the workspace (delivered / opened / clicked / bounced / complained / unsubscribed). Use to confirm webhooks are firing at all, to spot a deliverability cliff, or to debug why a specific message hasn't been marked delivered. Returns at most 500 rows, defaults to the last 24h. For per-message detail, call `emails.get_message_status`.
emails.list_provider_safetyList the deliverability safety profile for every email provider connected on ONE app. Returns one row per (app, provider) with the persisted policy, the per-provider Plinth preset (defaults + ceilings + which toggles are mandatory), and the well-meaning defaults so the caller can render a UI or compare to current overrides without a second round-trip. Use BEFORE composing a bulk send if the caller wants to know the per-domain cap or hard-bounce auto-pause threshold for the active provider; use after a deliverability decline to read what policy fired. `appId` defaults to the calling app when omitted; required from dashboard / cross-app contexts.
emails.update_provider_safetyUpdate the safety profile for ONE (app, provider). Plinth ENFORCES ceilings — values outside the per-provider ceiling are REJECTED with a structured `BAD_REQUEST` (details include `{ field, requested, maxAllowed, reason }`). Mandatory toggles (hard-bounce, complaint, per-domain cap) cannot be turned off; attempts return the same structured error. Audited as `email.safety_policy_updated`. Idempotent: calling twice with the same values is a no-op. Tip: call `emails.list_provider_safety` first to read the per-provider ceiling — picking values inside the envelope avoids the rejection round-trip.
emails.list_deliverability_eventsPaginated read of deliverability decisions Plinth made: declines (`domain_daily_cap_exceeded`, `soft_bounce_cooldown`, `provider_backlog`, `suppression_list`, `provider_paused`), bounces classified by hard/soft, and provider auto-pauses. Use to drive a deliverability report (CSV / JSON), to debug why a specific recipient wasn't sent to, or to confirm the per-domain cap is actually firing on the live route. Filter combinations match the `/insights/email` Deliverability tab so the UI and the export return the same rows. Returns `{ events, nextCursor }`. Each event carries `{ at, kind, reason?, appConnectionId?, provider?, recipient?, recipientDomain?, retryAfter?, messageId? }` — `kind` is one of `decline | bounce | provider_paused`. Cross-link `messageId` to `emails.get_message_status` when you need the original send context.
Quotas & rate limits5 tools ▾
regulation.set_limitCreate or update a token-bucket rate / quota rule. Use when the user says 'cap Brevo at 300 emails per day for this app', 'limit Resend to 5 sends/sec workspace-wide', 'soft-warn at 1000 emails/day on the marketing app'. Three-axis scope: omit `appId` to apply workspace-wide; omit `provider` to apply across all providers in the vertical; pass both for the tightest scope. `ratePerSecond` is tokens added per second (for daily quotas use `dailyQuota / 86400`); `burstCapacity` is the bucket cap. `hardStop: true` throws RATE_LIMITED with retry-after when the bucket is empty; `false` audits a warning but lets the send through (mirrors how budgets work). Idempotent on the (workspace, appId, provider, vertical, metric) tuple — re-calling with the same scope upserts the existing rule.
regulation.list_limitsList configured regulation rules. Use to answer 'what rate limits are in effect?', 'which apps have provider-specific caps?', 'show me everything that could throttle a Brevo send'. Optional filters narrow by app, provider, vertical, or metric. Returns each rule with its current bucket state (`tokensRemaining`, `lastRefillAt`) so the dashboard can render live capacity.
regulation.delete_limitRemove a regulation rule by id. Use when the user says 'remove the daily cap on Brevo' or 'lift the rate limit on the marketing app'. Audited as `regulation.deleted` so the chain shows when a rule was lifted (compliance evidence).
regulation.previewPreview the *current* bucket state without consuming tokens. Use to answer 'how many sends do I have left right now?', 'will this 1000-recipient blast pass Brevo's daily quota?', or before showing the operator a 'fits' / 'will throttle' badge in a UI. Computes the refill curve from `lastRefillAt` to now and returns the would-be decision (`wouldAllow` + `retryAfterMs`) for the requested qty against every applicable rule.
regulation.apply_presetsRe-apply the curated regulation preset bundle for a provider. Used to refresh defaults after a Plinth release that bumped them, or to re-seed limits after they were deleted. Operator-edited rules (source='operator') are NOT touched unless `force: true`. Auto-fired on every `provider_connections.connect`, so manual calls are normally only needed when restoring defaults. For Resend, pass `resendAccountTier: 'paid'` when the API key is on a paid transactional plan so the free-tier daily send cap is not re-seeded.
Audit, usage, cost4 tools ▾
governance.get_costsRead cost rollups for the workspace. Use when the user asks 'how much have I spent this month?', 'what's email costing me?', or 'show Brevo spend this week'. Filter by `provider` or `vertical` to narrow.
governance.set_budgetCreate or update a budget that gates future provider calls. Use when the user says 'cap email spend at $50/mo', 'set a hard stop on Stripe writes', or 'budget the LLM tag'. `hardStop: true` aborts in-flight calls; default is soft (alert only).
governance.simulate_spendEstimate whether a planned set of changes would breach budgets. Use before bulk operations: 'will sending this newsletter to 50k people put me over budget?'.
governance.export_audit_logExport the audit chain for an interval (CSV or JSON). Use when the user says 'pull last quarter's audit log' or 'I need our SOC2 evidence'. Result is verifiable client-side via AuditService.verifyChain.
Brand & styling2 tools ▾
branding.getRead a brand profile (colours, fonts, logo URL, footer text, social links). Brand identity is per-app — every AppConnection owns its own brand row, so the marketing app and the transactional app in the same workspace can run completely different palettes / footers / logos. When `appId` is omitted the call defaults to the brand of the app whose API key authenticated the request (the natural shape for an agent: 'show me my brand'). Use when the user asks 'what does this app's brand look like?', 'what colour are the marketing emails?', or before composing any branded artifact. Returns sensible Plinth defaults for any field the app hasn't customised yet, so the response is always fully-populated.
branding.updateUpdate a brand profile. Brand is per-app: changes only affect the named AppConnection (or, when `appId` is omitted, the calling app — useful for an agent that wants to brand its own surface). All fields optional; pass only the ones you want to change (PATCH semantics). Use when the user says 'make this app's brand purple', 'use this logo URL for the marketing app', 'set the footer to my company address'. Hex colours like '#5B3DEC'. `socialLinks` is an array of {label,url}. After saving, every subsequent render under that app substitutes the new {{brand.*}} tokens at render-time — no need to re-edit each saved template / receipt / page. Other apps in the same workspace are unaffected.
App connections1 tools ▾
mcp.create_app_connectionMint a new API key bound to a specific app, environment, and allow-list of tools. Use when the user says 'create an app for our backend' or 'I need a key for staging'. The raw key is shown exactly once; store it immediately.
Other11 tools ▾
marketing.capture_signupCapture a product-updates / newsletter signup. Use when the user fills the marketing form. Normalizes the domain to eTLD+1 and flags free-mail addresses.
marketing.get_statsReturn public marketing stats (total signups + redacted recent list). Used by the public landing page; safe to call without a key.
auth.create_userCreate or upsert an end-user inside the workspace's connected auth provider. Use when the user says 'add user [email protected]' or 'invite a teammate'. Idempotent on email — calling twice updates metadata, doesn't create duplicates.
auth.configure_ssoWire up an OIDC or SAML SSO connection for the workspace. Use when the user says 'enable Google SSO' or 'connect Okta'. Pass `metadataUrl` if the IdP exposes one; otherwise the dashboard prompts for raw config.
payments.create_planCreate a pricing plan with one or more tiers. Use when the user says 'add a $20/month Pro plan' or 'launch a yearly tier'. Amounts are in the smallest currency unit (cents).
payments.publish_pricing_pagePublish a hosted pricing page tied to a plan. Use when the user says 'go live with our pricing' or 'show pricing on our site'.
social.schedule_postQueue or send a multi-platform social post. Use when the user says 'tweet this', 'post to LinkedIn and X', or 'schedule this thread for tomorrow morning'. `variants` lets you tailor the copy per platform (X gets short, LinkedIn gets long-form).
content.create_blog_postCreate or update a blog post (MDX). Use when the user says 'publish this post', 'draft a blog about X', or 'update the launch post'. Idempotent on `slug`.
landing.create_pageCreate a landing page from a JSON section schema, with optional A/B variants. Use when the user says 'make a landing page for our launch' or 'set up a /trial page with two variants'.
hosting.deployTrigger a deployment from a Git ref or pre-built artifact. Use when the user says 'deploy main', 'ship to production', or 'roll out the staging build'. `production: true` is the live cutover; default is preview.
hosting.attach_domainAttach a custom domain to a hosting project. Use when the user says 'point acme.com at this project'. Returns the DNS records the user needs to set on their registrar.
FAQ.
Do I need MCP or can I use REST only?
Either. POST /api/v1/call runs the same dispatch table as POST /mcp. MCP is better for Cursor/Claude; REST is better for scripts, CI, and custom backends.
Can an agent register a workspace without a human?
Almost. The agent drives discover → verify → register over MCP/REST, but a human must paste the one-time email code from their inbox (onboarding.verify_email). After that, the agent can operate autonomously with its app key.
Where do I get PLINTH_API_KEY?
Create an app on /apps (or call mcp.create_app_connection). The raw key is shown exactly once — store it in your secret manager or agent env.
What's agent.get_bootstrap vs GET /api/v1/agent/bootstrap?
Same JSON shape. The HTTP endpoint is public-friendly (optional Bearer for workspace stats). The MCP tool requires auth and is what agents call after they're wired up.
How do I limit what one agent can do?
Set allowedTools when minting the app key (or tighten in the dashboard). Defaults to * (all tools). Use capability bundles for email-only or team-only keys.
Where is the integration skill file?
Per-app: /api/v1/skill.md with Bearer auth. Repo skill for agents: skills/agent/SKILL.md. Also see Help → MCP vs REST.
Ready to wire your agent?
Fetch bootstrap, paste config, call whoami. Same APIs the dashboard uses — audited and rate-limited.