Plinth

Help

How Plinth works

Plinth sits between your apps (and your agents) and the email providers you already pay for — Brevo, Resend, your in-house SMTP. Below is the shortest possible explanation of how that works. No jargon. Real examples you can copy. If something is still confusing, that's a bug — tell us and we'll fix the page.

The pieces

Four things to keep straight. Once you've got these, the rest is just URLs.

Workspace
Your team's account on Plinth. Holds your real provider keys, sealed in a vault.
Provider
An email rail you already pay for: Brevo, Resend, your in-house SMTP. Connect it once on /providers and Plinth seals the credential. The raw key never leaves the vault. Connect a second provider and Plinth treats them as a pool.
App
Anything that calls Plinth. Your website's transactional sender. Your marketing-newsletter cron. Your AI assistant in Cursor. A Python script. Each app gets its own API key, its own brand identity, and its own routing strategy — so marketing and transactional can ride different rails without forking your code.
API key
The bearer token an app uses to prove who it is. Looks like plinth_production_xyz…. Shown once when you create the app, then only as a hash in our database.
Picture it like this: your workspace is a safe. Inside the safe sit your real Brevo and Resend keys. You hand out lots of app badges — one per thing that needs to send email. Apps swipe a badge to ask Plinth “send this template to that address.” Apps never see what's in the safe, and they don't pick which provider — the routing strategy on the app does.

What happens when an app sends a single email

Here's every step, in order, for one emails.send_template call:

  1. 1Your app says “send the welcome template to [email protected] with { firstName: "Ada" }.”
  2. 2Plinth checks the API key — yes, this app belongs to your workspace, yes it's allowed to call emails.*.
  3. 3Plinth looks at the app's routing strategy (single, round-robin, or random) and picks a provider from your pool — say, Resend.
  4. 4Plinth checks the regulation buckets for Resend on this workspace. Daily quota OK? API rate limit OK? If not, fail over to the next provider in the pool. If everything's exhausted, the call fails with a clear error before any traffic leaves us.
  5. 5Plinth opens the safe, reads your Resend key, renders the template against the app's brand identity, and calls Resend. Gets a message ID back.
  6. 6Plinth writes a tamper-evident audit row (App X sent template “welcome” via Resend at 10:32) and a usage event. Returns the result to your app.
Your app never touched Resend directly. If Resend has an outage at 11am, the next call routes to Brevo automatically — your code doesn't change. If your Resend key leaks, rotate it once on /providers — every app keeps working. If an app misbehaves, revoke its badge on /apps — every other app keeps working.

Why a pool, instead of just one provider

One email provider is fine until the day it isn't. The most common ways “the day it isn't” arrives:

  • Free-tier daily quota exhausted. Brevo free is 300 sends/day. Resend free is 100 sends/day. Your one-provider setup silently drops everything for the rest of the day.
  • API rate limit tripped. Resend caps at 10 req/s. A burst from a backfill kills your transactional flow with no backoff.
  • Reputation drop on one IP / domain. You don't want to find out the morning of a launch.
  • The provider has an incident. Status pages exist for a reason.

Plinth's answer is a per-app provider pool with a routing strategy. single is the default (one provider, like the products you're used to). round_robin rotates evenly so reputation builds across two rails. random uniformly samples — useful when you're A/B-testing inboxing across providers.

On every call, Plinth checks the regulation buckets before it dispatches. If the chosen provider would exceed its quota, the next one in the pool gets the send. The decision shows up in the audit row so you can review “why did this go through Brevo and not Resend” later.

The TOS presets are auto-applied on every connect. You don't have to know that Brevo's free tier is 300/day or Resend's API limit is 10/s — Plinth carries that knowledge so your agents don't need to.

Four ways your apps can use Plinth

Same key works on all four. Pick whichever fits the job.

1

From inside Cursor or Claude Desktop

When to use: composing templates, debugging deliverability, one-off resends from your laptop.

Paste this in ~/.cursor/mcp.json:

{
  "mcpServers": {
    "plinth": {
      "url": "https://plinth.tools/mcp",
      "headers": { "Authorization": "Bearer ${PLINTH_API_KEY}" }
    }
  }
}

Restart Cursor. Now you can say “set the marketing app to round-robin between Brevo and Resend, and resend the welcome template to the addresses that bounced yesterday” and Cursor actually does it. It sees the email tools (send, compose, set_routing, ingest, etc.) and picks the right ones.

2

From your backend

When to use: your customer-facing product. Replaces the Brevo SDK + Resend SDK + your retry logic with one bearer token.

One HTTP call, no SDK install:

await fetch("https://plinth.tools/api/v1/call", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PLINTH_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "emails.send_template",
    arguments: {
      template: "welcome",
      to: "[email protected]",
      vars: { firstName: "Ada" },
    },
  }),
});

That's the entire integration. The provider gets picked by the app's routing strategy, the regulation buckets get checked, the brand gets substituted, the audit row gets written — none of which is your code's problem.

3

From an AI agent in the cloud

When to use: customer support agents, lifecycle bots, anything that drafts and sends mail on behalf of your team.

A support bot that does real work:

# Customer support agent (Python)
template = await call("emails.compose_template", {
    "goal": "Apologize for the outage on 2026-05-09 and explain credits.",
    "audience": "Pro tier customers in EU",
})
await call("emails.send_template", {
    "template": template["key"],
    "to": "[email protected]",
    "vars": { "credits_usd": 12 },
})

The AI decides what to draft and who to mail. Plinth makes sure it stays inside its sandbox: only the tools you allow, only within the regulation buckets you set, every action recorded in the audit chain.

4

From a script or GitHub Action

When to use: CI jobs, “email everyone whose trial expires today” cron, “announce on release tag” workflows.

Pure curl, no dependencies:

curl -X POST https://plinth.tools/api/v1/call \
  -H "Authorization: Bearer $PLINTH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"emails.send_template","arguments":{"template":"trial_expiring","to":"$RECIPIENT"}}'

A real example with four apps, all sending email

You connect Brevo and Resend once on /providers. Then you create four apps — each with its own routing strategy and brand identity:

App A — Transactional backend

Who: Your customer-facing product. Does: Sends welcome / receipt / password-reset emails. Strategy: round_robin across Brevo + Resend so reputation builds on both.

App B — Marketing newsletter cron

Who: A scheduled job. Does: Sends the weekly newsletter. Strategy: single (Brevo only — its higher daily quota fits the volume), separate brand identity from App A so unsubscribe links land on a different domain.

App C — Support agent in the cloud

Who: An AI that answers customer mail. Does: Composes apology / credit / clarification templates and sends them. Strategy: single (Resend) — separate provider so a hot bug here doesn't degrade transactional reputation.

App D — Cursor on my laptop

Who: You, the developer. Does: Composing templates, replaying yesterday's bounces, exploring the audit log. Strategy: single (Resend dev). Its allowedTools is locked down to emails.compose_template + emails.send_template + governance.audit_chain.

All four use the same Brevo and Resend keys. None of them sees the plaintext. Each gets the brand it should — App B's newsletter looks different from App A's receipts even though they live in one workspace.

A morning in the audit log might look like:

10:01  App A   emails.send_template     (welcome,  via resend)
10:01  App A   emails.send_template     (receipt,  via brevo)   ← round_robin
10:03  App C   emails.compose_template  (apology, en-US)
10:03  App C   emails.send_template     (apology,  via resend)
10:05  App B   emails.send_template     (newsletter, via brevo) ← single
10:05  App D   emails.compose_template  (welcome v2 draft)

You can revoke App D's key in one click without touching the others. App A keeps sending receipts. App B keeps shipping the newsletter. That's why you split apps even when they all do “send email” — different routing, different brand, different blast radius on revocation.

MCP vs REST — when to use which

You have two URLs. Same key works on both. Same things happen behind the scenes. Pick by what your client speaks:

https://plinth.tools/mcp

MCP

The protocol AI editors and agent frameworks understand.

Use from: Cursor, Claude Desktop, Cline, your custom agent loop, anywhere that asks for an “MCP server URL.”

POST https://plinth.tools/api/v1/call

REST

Plain HTTP and JSON. No protocol, no SDK.

Use from: your backend, scripts, GitHub Actions, edge functions, Cloudflare Workers, anything that can do fetch or curl.

They're literally the same dispatcher behind two envelopes. That's why one app key works on both — pick whichever your client speaks and don't overthink it.

Building an agent?

The full playbook — bootstrap JSON, Cursor/Claude config, self-registration steps, copy-paste curl, and FAQ — lives on /agents. Start with GET https://plinth.tools/api/v1/agent/bootstrap.

What's protected automatically

You don't configure any of this. Every send gets it for free:

Audited

Every send is written to a tamper-evident log you can export. Each row is hash-chained to the previous one — you'll spot any rewrite immediately.

Regulated

Token-bucket rate / quota gates per workspace × app × provider. TOS presets auto-applied on connect (Brevo 300/day, Resend 100/day + 10/s, …). Strictest rule wins.

Budget-checked

Set monthly caps per provider. Plinth blocks calls that would exceed them with a clear error before any money moves.

Scoped

Each app key only works in its workspace and only for the tools you allow. Defaults are sane; tighten any time.

Sealed credentials

Provider keys are encrypted with a per-workspace subkey (XChaCha20-Poly1305 + HKDF). The raw key leaves the system exactly once — at create time.

Idempotent

Retries on flaky networks won't double-send an email. Plinth dedupes by idempotency key, with provider-side message IDs surfaced back to you.

Frequently asked

Do my apps need to be deployed somewhere special?
No. Anywhere on the internet works — your laptop, Vercel, AWS, a home server, a GitHub Action. They just need to be able to make HTTPS requests.
Why route across multiple providers if I'm happy with one?
Honest answer: if your volume fits one provider's free tier and you've never been rate-limited, single-provider routing (the default) is fine. The pool starts paying off the first time you trip a daily quota at 4am, an outage on a launch day, or a reputation drop on one IP. You can also use the pool to A/B inboxing across rails — same template, same audience, two providers, look at the engagement difference.
What if my API key leaks?
Revoke it on /apps. The next call using it returns 401. Mint a fresh key for the same app and update your env var. The provider keys (Brevo, Resend, SMTP) are unaffected — they never left the vault.
Can two apps share an API key?
Provider routing (single, round-robin, random) and each rotation pool are stored per app — App A and App B never share one round-robin sequence. Plinth mints a distinct API key per app so every call resolves to exactly one AppConnection for audit and limits; that mapping is separate from how you label or store secrets in your own env files. Keeping separate apps is still the right split for allowedTools, brand, and revoking one integration without touching others.
Can I limit what one app can do?
Yes. Each app has an allowedTools list. Defaults to * (everything your workspace can reach). Set it to ["emails.send_template", "emails.compose_template"] if that's all the app should be able to do.
What if I want to call Resend (or Brevo) directly, not through Plinth?
You still can — Plinth doesn't replace the provider's API, it sits in front of it. But you lose the multi-provider routing, the regulation gates, the per-app brand resolution, the audit chain, and the single bearer token your agents already use. We recommend going through Plinth for anything you want governed.
When will warm-up / A/B subject testing / suppression sync ship?
They're on the queue, in that order, after we harden the launch surface (multi-provider pool + regulation + audit + brand + composer). We don't commit dates publicly because shipping a warm-up scheduler that doesn't accidentally torch a customer's reputation deserves to take however long it takes. Subscribe to updates on the home page if you want a note when each lands.
What providers can I connect today?
Email: Brevo and Resend, with in-house SMTP next. The other verticals (auth, payments, social, content, landing, hosting, governance) live in the codebase and have working adapters in dev — but we're not promoting them publicly until each surface is as well-baked as the email one is. See /providers for the live grid.
Still stuck? The full architecture lives in /docs and there's a per-vertical SKILL file under /skills.
konsole.oneHow Plinth works · Plinth