Platform
Hosting & deploysPush to a branch, get a live linkDatabaseReal Postgres, one per appSign-inEmail and Google, users includedFile storageUploads, photos and documentsAI gatewayEvery model, with a hard spend cap
Resources
DocsGuides written for people who are newFor AI agentsClaude Code, Cursor, MCP, JSON CLICompareCamplax beside the stack it replacesPricingFrom $25/month plus prepaid creditsSupportA person answers, not a queue botPoliciesTerms, privacy and how we handle data Get started
Open in ChatGPT Open in Claude Last updated 2026-11-07

Email

Inboxes your app, its agents or a person send and receive from, plus the mail your app sends — templates, delivery logs and suppressions. All on the injected project token.

Every project has an Email page in the console: what it sent, its inboxes, the templates it sends from, its own mail domains, stats and a suppression list. Mail goes out through Amazon SES, each project's mail under its own SES tenant (a stub that records without sending stands in for local runs).

Your app sends mail with the project token Camplax already injects — no key, no SMTP password, no webhook secret.

Status (Oct 15 2026): mail is live both ways. Sending goes through Amazon SES, which has granted Camplax production access, so mail reaches any address, not only verified ones. inbound.camplax.app and your own domains receive through Camplax's mail server, mx1.camplax.app, which has been proven end to end — delivery to an inbox, +tag threads, locked inboxes refusing strangers, and unknown addresses refused. The console shows Domains and Inboxes once GET /email/domains says the deployment can deliver.

Inboxes

An inbox is an address that sends and receives — for your app, an agent inside it, or a person. Make one under Email → Inboxes → New inbox (a name and an address), or from your app with email.inbox() below. Give out up to 1,000 per project, and support+anything@ reaches support, so one inbox can hand every user, agent or task an address of its own. Each +tag is its own conversation — one person writing to agents+ada@ and agents+lin@ is two threads — and answers go out from the tagged address the mail came to, so the person's next reply reaches the same agent.

Each inbox decides where its mail goes — either, both, or neither (then it is kept in the console only):

  • Your app. Each message is posted to a path on your app, signed with Camplax's key — verify it like a cron call, with no secret. This is how an agent gets mail.
  • A person. Each message is sent on to someone on the project, at their account email, as "Cara via Support" — replying answers the original sender directly. Only people with a seat on the project and a verified email can be picked — never a typed address. Up to 100 a day per inbox. Out-of-office replies and other automatic mail stay in the inbox and are not sent on, and a message whose sender could not be verified says so at the top.

And it decides who may write to it: anyone, or only your team and senders you allow (new inboxes on inbound.camplax.app start that way; on your own domain they start open).

To send as the inbox from a mail app — Apple Mail, Outlook, Gmail's "Send mail as" — make it a login in the inbox's sheet: server mx1.camplax.app, port 465 (SSL), the address as the username, and a password shown once. A login sends only as its own inbox: the message's From (and Sender, if it has one) must be that address — any display name is fine — or it is refused with a 550. The same caps and suppressions apply. Logins and an inbox's webhook are for owners and members; deleting an inbox, which deletes all its mail, is for owners.

An inbox can also post each message to any https URL through the API (webhookUrl on POST/PATCH /email/inboxes). Those posts are project webhooks with the email.received event, signed with Camplax's key like every other — no secret is handed out. A receiver outside Camplax that wants an HMAC as well asks for one with POST /email/inboxes/:id/rotate-secret (shown once); replacing it later keeps the old one signing for 24 hours.

The console shows each inbox's recent mail, read-only. It is not a mail client: your app, Lee, or the person's own mail app replies.

Try it: the Mail starter (New project → Mail) is a Gmail-style app built on all of this — sign in, make mailboxes, send and receive, and switch any mailbox to an AI agent that answers by itself. Its source is the quickest reference for wiring email into your own app.

Email from your app

Two things, both called with CAMPLAX_PROJECT_TOKEN (scopes email:send and email:receive, on production app tokens only — previews never carry them, and camplax dev gets them only with --use-production-services):

  • Send. POST /v1/email/send with { to, subject, text and/or html, from?, threadId? }. from is one of the project's inboxes — its local part (support) or its full address (support@mail.example.com); leave it out and the first inbox sends — a project with none gets <slug>@inbound.camplax.app made for it. A bare local part picks the inbound.camplax.app inbox first when two domains share it. Suppressions apply. Limits: 10 emails a second per project; 500 a day for a project's first 7 days; 100 a day per inbox on the shared inbound.camplax.app (none on your own verified domain); and the plan's monthly allowance — 10,000 / 25,000 / 75,000 sent and 25,000 / 75,000 / 200,000 received on Indie / Pro / Scale, pooled across your apps. The account owner gets one email a month when your apps have sent 80% of it. Past the allowance, more mail is paid from your account's prepaid credits: $0.40 per 1,000 sent and $0.05 per 1,000 received, taken 1,000 sent or 10,000 received at a time before it is used. When the credits run out, sending stops with an error that says so, and you get an email; add credits on the Billing page and it starts again at once. Up to 5× the allowance a month in all, then it stops until the 1st (UTC) — write to support to raise that. The same limits cover mail sent from a mail app over SMTP; a burst over 10 a second is told to retry, and the mail app sends it a moment later. Fails today — see the note above.
  • Receive. PUT /v1/email/inboxes/<local-part> with { forwardTo: "/api/email/inbound", locked?: false, domain? } claims the inbox (or updates it — safe on every boot). domain puts it on one of your verified domains instead of inbound.camplax.app. When inbound delivery goes live, every message the inbox accepts is posted to that path on your own app, signed with Camplax's key — verify it like a cron call, with no secret. GET /v1/email/inboxes lists them. Ten failed deliveries in a row pause forwarding (mail is kept, not lost); sending the path again resumes it.
  • Release. DELETE /v1/email/inboxes/<local-part> (add ?domain=mail.example.com for one on your own domain) deletes the inbox and all its mail, and the address is free to claim again. An inbox this project does not have answers 404, so a retry is safe. An inbox with a mail-app login or a copy to a person answers 409 — an Owner deletes that one under Email → Inboxes. Use it when the user who owned an address deletes it or their account.

In Plax:

import { createPlaxEmail } from "@camplax/plax";
const email = createPlaxEmail();

await email.inbox("bot", { forwardTo: "/api/email/inbound", locked: false });
await email.send({ to: "ada@example.com", subject: "Hi", text: "Hello", from: "bot" });

// On your own domain:
await email.inbox("support", { domain: "mail.example.com", forwardTo: "/api/email/inbound" });
await email.send({ to: "ada@example.com", subject: "Hi", text: "Hello", from: "support@mail.example.com" });

// POST /api/email/inbound
const got = await email.receive(req, await readRawBody(req));
if (got.ok) {
  const said = got.data.message.visibleText; // what they typed, without the quoted thread
  await email.reply(got.data, { text: `You said: ${said}` }); // same conversation, from the same inbox
}

// When the address is no longer needed — its mail is deleted with it:
await email.releaseInbox("bot"); // { released: true }, or { released: false } if it was already gone

For an agent, that is the whole loop: read message.visibleText, answer with email.reply. An app with many agents gives each one an address — its own inbox (email.inbox("ada")), or a tag on one inbox (agents+ada@) — and routes on message.to or message.tag. To start a conversation, send from: "agents+ada@mail.example.com".

An address is not a password. Anyone who knows an agent's address can write to it, and the sender picks the +tag. If an agent can see or do anything private, give it an address nobody can guess (agents+k7f2q9@, not agents+kim@), and check message.from (with message.senderAuthenticated) against the person the agent belongs to before it acts — or set the inbox to only accept your team and allowed senders. Each delivery also carries message.tag (the order-42 in bot+order-42@…, or null), message.replyTo (where the sender asked for answers — nothing vouches for it, so email.reply answers message.from and an app that follows Reply-To does so on purpose), the full text and html, attachment names and sizes, and thread.id.

Each delivery carries message.senderAuthenticated — true only when the sender's domain signed the mail (DKIM). Trust from for anything that matters only when it is. Mail is untrusted input: anyone who can write to the inbox can write instructions to your agent. Keep an agent's inbox to known senders, or don't let what it reads trigger anything you would not let a stranger do. The AI chatbot starter wires all of this in — it mails a chat and takes mail by reply once app mail sends and inbound delivers.

Your own domain

Send and receive as support@mail.example.com instead of the shared address. In the console: Email → Domains → Add domain, type the name, and publish the records it shows at your DNS host:

TypeNameValueWhy
CNAME ×3<token>._domainkey.mail.example.com<token>.dkim.amazonses.comSigns your mail (DKIM)
MXmail.example.com10 mx1.camplax.appReceives your mail
TXT_camplax.mail.example.comcamplax-verify=…Proves the name is yours
TXT_dmarc.mail.example.comv=DMARC1; p=quarantine; adkim=r; aspf=rRecommended — stops others sending as you

Camplax checks every five minutes, or press Check now. When the three required kinds are found, the domain reads Ready, and it is checked again every day — remove a record and it goes back to waiting. Then make an inbox on it under Inboxes, or from your app with domain as above. Nothing to paste into your app: the same project token sends and receives.

  • Use a subdomain like mail.example.com when example.com already gets mail (Google Workspace, Microsoft 365). The MX record decides where a name's mail goes, so pointing example.com at Camplax moves all of it here. The console warns when the name already has an MX somewhere else.
  • On your own domain any name is yours — support@, billing@, admin@ all work. The shared inbound.camplax.app keeps those names reserved so no project can pose as Camplax.
  • New addresses on your domain accept mail from anyone; on the shared domain they start locked to owners and the allowlist. Mail from another Camplax inbox — sent by the API or from a mail app — meets the same lock and allowlist as mail from anywhere else: to write to a locked inbox, add the sending inbox's address to its allowlist.
  • A flood stops at 10,000 received per inbox per day — the sender's mail server is told to try again later, so nothing is lost. Past the monthly received allowance, mail is paid from your account's prepaid credits; with none left, new mail is turned away the same way (you are emailed). An account past 5× its monthly received allowance turns away mail from senders Camplax cannot verify until the 1st; mail from verified senders keeps arriving until it alone reaches 5×, so a spam run cannot lock out your real mail.
  • Up to 25 domains a project. Adding, checking and removing one is for owners, as with web domains. Removing one needs its inboxes deleted first.

Templates

A template is a subject and HTML body with {{merge_tags}} filled in at send time — the console editor shows a live preview. A Password reset system template is seeded the first time you open the page: it is the mail sent when you reset a user's password from the Users page, and its on/off switch and wording are real. You can make more — a saved template can be test-sent or deleted; only templates a flow actually uses are seeded.

POST /v1/projects/<slug>/email/preview renders a template — or a draft subject and HTML — against sample data so you can check the merge tags before saving.

Test sends

Send test mails one saved template to your own signed-in address. That is deliberate, and the API enforces it: POST /v1/projects/<slug>/email/send takes a templateKey or templateId and merge-tag data, and nothing else — a request that passes subject, html or from is rejected, and the recipient is always the signed-in user, never an address from the request. A test send exercises the real provider, so it lands in your real inbox and your real log.

Delivery

Every send is a row in the log (GET /v1/projects/<slug>/email/logs, filterable by status) — queued, sent, delivered, opened, deferred, bounced, complained, failed or suppressed, with the provider id and any error attached. GET /email/stats rolls that history into sent, delivered, open and bounce numbers, plus failed — sends that never left Camplax (failed or suppressed), which are never counted as sent. The log keeps your plan's history window — 7 days on Indie, 14 on Pro, 30 on Scale — and older sends are removed by a sweep that runs every minute; moving to a smaller plan removes what is older than its window on the next pass. A hard bounce or complaint that arrives after its send was removed still puts the address on the suppression list.

SES reports delivery events back to POST /v1/webhooks/ses, and they update the matching send row. A hard bounce or a spam complaint does more than mark the row — it puts the address on the suppression list, and later sends to it are refused before they reach the provider. Soft bounces only update the log. You can also add or check addresses yourself: GET/POST /email/suppressions and GET /email/suppressions/check?address=….

Every project shares Camplax's sending reputation, so a few rules protect it. A message with a javascript: or data: link, or a link that hides its real destination (https://bank.com@other.site), is refused with unsafe_link. Mail that can never arrive is refused: a name reserved for examples, tests or private networks (example.com, example.net, example.org, home.arpa, or anything ending in .test, .example, .invalid, .localhost, .local, .internal or .onion), an IP address or one-word host instead of a domain, or a console test user's @test.camplax.app address. It is refused with undeliverable_recipient and never sent (SMTP refuses it too, with a 550). To try sending without mailing a person, send to Amazon's simulator, success@simulator.amazonses.com. Once a project has sent at least 100 messages in 7 days, passing 4% hard bounces or 0.08% spam complaints over those 7 days (a single complaint never does) pauses its sending with sending_paused until they are back under the line — remove the addresses that bounce and it resumes by itself. Mail to the simulator does not count.

Inboxes appears once mail can actually arrive: when inbound.camplax.app points its MX at Camplax's mail server, or once the project has a verified domain of its own. It lists only inboxes on those domains. Rejected mail and per-inbox allow/block lists have routes (/email/inboxes/:id/rejections, /email/inboxes/:id/lists) but no console view yet.

The endpoints

All of it is console-session routes under /v1/projects/<slug>/email:

MethodPathWhat it does
GET/POST/email/templatesList or create templates
PATCH/DELETE/email/templates/:idEdit subject, body or the enabled switch — or remove the template
POST/email/previewRender merge tags against sample data
POST/email/sendTest-send a saved template to yourself
GET/email/logsDelivery log, newest first
GET/email/statsTotals and rates over your plan's history window
GET/POST/email/suppressionsList or add suppressed addresses
DELETE/email/suppressions/:idTake an address off the list
GET/email/suppressions/checkIs this address suppressed?
GET/POST/email/inboxesList inboxes (with the people mail can go to), or make one
PATCH/DELETE/email/inboxes/:idChange where its mail goes (forwardTo, forwardUserId), who may write (locked), on/off — or delete it
POST/DELETE/email/inboxes/:id/smtpMake or remove its mail-app login (/smtp/rotate for a new password)
GET/email/inboxes/:id/threadsIts recent conversations
GET/POST/email/domainsList your mail domains (plus whether this deployment can verify one), or add one
GET/DELETE/email/domains/:idOne domain and its records, or remove it
POST/email/domains/:id/verifyCheck its DNS now

Under the hood: Amazon SES (each send under a per-project tenant). A Cloudflare Email Sending binding is wired as a fallback but not bound, and a stub records without sending in local runs. Inbound has two doors built — the Cloudflare Email Routing rule on the zone, and the Stalwart mail box on mx1.camplax.app posting to the same hook — feeding one store: D1 rows for inboxes/threads/messages, the raw .eml in R2. The MX points at mx1, so the Stalwart door is the one in use. Your own domain is an SES identity (Easy DKIM) associated with the project's SES tenant, plus a domain on the Stalwart box.

Camplax

Hosting, database, sign-in and storage in one project. One login, one bill.

Platform
More
Legal
Company
© 2026 Camplax From $25 / month · credits stop at zero