Plinth

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

~/.cursor/mcp.json
{
  "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. 1

    onboarding.discover_organization

    Check whether the work email domain can claim a workspace.

  2. 2

    onboarding.request_email_verification

    Email a one-time verification code (purpose domain_claim).

  3. 3

    onboarding.verify_email

    Consume the inbox token.

  4. 4

    onboarding.register_workspace

    Create the workspace (or request_access if already claimed).

  5. 5

    onboarding.select_stack

    Pick app modules and provider brands.

  6. 6

    provider_connections.connect_workspace

    Seal shared Brevo / Resend keys once at workspace level (use connect per-app only for overrides).

  7. 7

    mcp.create_app_connection

    Mint an app + scoped API key (shown once).

  8. 8

    apps.set_email_routing

    Optional: round-robin / failover across email providers.

  9. 9

    regulation.apply_presets

    Apply TOS rate-limit presets (also auto on connect).

  10. 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.send

    Send transactional mail (html/text/body).

  • emails.send_template

    Send using a saved template key.

  • emails.ensure_template

    Fetch-or-create a template (agent recovery primitive).

  • emails.get_message_status

    Delivery / engagement for one message id.

  • apps.get_email_routing

    Read pool, strategy, failover settings.

  • regulation.preview

    Remaining quota before a blast.

  • governance.export_audit_log

    Export 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_bootstrap

    Return 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_catalog

    Show 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_organization

    Look 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_verification

    Send 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_email

    Consume 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_workspace

    Create 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_access

    Ask 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_stack

    Pick 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_status

    Where 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.whoami

    Tell 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_members

    List 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_member

    Invite 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_routing

    Show 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_routing

    Configure 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_allocation

    Show 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_allocation

    Reserve 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_mode

    Choose 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.connect

    Connect 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_workspace

    Connect 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.send

    Send 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_template

    Send 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_template

    Create 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_template

    Idempotent 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_presets

    Browse 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_template

    Conversational 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_templates

    Show 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_template

    Delete 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_contact

    Add 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_contacts

    List 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.unsubscribe

    Suppress 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_newsletter

    Compose 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_newsletter

    Broadcast 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_newsletters

    Show 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_stats

    Report 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_status

    Fetch 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_messages

    Paginated 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_endpoint

    Show 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_webhook

    Register (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_secret

    Issue 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_senders

    List 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_events

    Recent 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_safety

    List 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_safety

    Update 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_events

    Paginated 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_limit

    Create 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_limits

    List 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_limit

    Remove 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.preview

    Preview 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_presets

    Re-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_costs

    Read 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_budget

    Create 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_spend

    Estimate 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_log

    Export 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.get

    Read 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.update

    Update 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_connection

    Mint 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_signup

    Capture 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_stats

    Return public marketing stats (total signups + redacted recent list). Used by the public landing page; safe to call without a key.

  • auth.create_user

    Create 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_sso

    Wire 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_plan

    Create 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_page

    Publish 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_post

    Queue 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_post

    Create 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_page

    Create 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.deploy

    Trigger 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_domain

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

konsole.oneAgents & MCP — self-register, connect, send · Plinth