Send emails with Phoenix
Transactional and marketing email from a Phoenix context or Oban job. Req makes one call, and retries never send twice.
✓Free to start. Unlimited contacts on every tier.
One request, complete trace
Request
POST /api/v1/emails
Authorization: Bearer aem_…
Idempotency-Key: welcome-8f21
{ "to": "[email protected]", "template": "welcome" }
202 Accepted
{ "id": "msg_7ca31", "status": "queued" }
[ 01 / 05 ]Quickstart
How do I send email from Phoenix?
Create a key, keep it on the server, and call one endpoint from a context or Oban job.
Get an API key
Open Settings → API & MCP and create a key with the Send + manage permission (the default). It starts with aem_ and is shown once.
Store it as a server-side environment variable
Set AEM_KEY in your release environment. System.fetch_env! fails loudly if it's missing, which is what you want.
EnvironmentAEM_KEY=aem_…Send the email
Req posts to the email API. Return the error tuple from your Oban job and Oban retries it; the same Idempotency-Key keeps it to one email.
lib/app/email.exresponse = Req.post!("https://sendandretain.com/api/v1/emails", headers: [ authorization: "Bearer #{System.fetch_env!("AEM_KEY")}", idempotency_key: "welcome-#{user.id}" ], json: %{to: user.email, template: "welcome"} ) case response do %{status: status, body: body} when status in 200..299 -> {:ok, body["id"]} %{status: status, body: body} -> {:error, {status, body}} endCheck the response
A 202 means queued, not delivered. Keep the id, then follow delivery in the message log or through a signed webhook.
202 Accepted{ "id": "msg_7ca31", "status": "queued" }
[ 02 / 05 ]Where it goes
Where should email code live in your Phoenix app?
Contexts and Oban jobs
Oban owns local retries. A stable idempotency key ties every retry to one email.
Events from contexts
Send product events from contexts after the data change succeeds, not from controllers.
A raw body for webhooks
A Plug with a custom body reader caches the raw body for the signature check.
[ 03 / 05 ]Marketing email
Can I send marketing emails from Phoenix too?
Yes. Send an event from the same context when a user signs up, starts a trial, or goes quiet, and an automation sends the right email. Automations start paused, and every email goes to one person: no newsletter blasts.
- Transactional and marketing email share one contact record and one unsubscribe list.
- Unsubscribes, bounces, and complaints are suppressed for you before every send.
- Templates are react.email components; published versions never change under you.
[ 04 / 05 ]Example prompts
What can you ask your coding agent to do in Phoenix?
Claude Code, Cursor, and Codex can do these over the Send & Retain MCP server, with your repo open. Automations they build stay paused until you turn them on.
Welcome email
“Add a welcome email to our Phoenix registration using Send & Retain and Req, with an idempotency key.”
Oban worker
“Move our receipt email into an Oban worker that calls Send & Retain, keyed so retries never send twice.”
Webhook
“Add a Phoenix endpoint that verifies Send & Retain delivery webhooks with a raw-body Plug and marks bounced users.”
Trial automation
“Send a trial_started event from our Phoenix app and build a paused trial-conversion automation for it.”
[ 05 / 05 ]Related
Keep building with Phoenix
Questions people ask us
Send your first email from Phoenix today.
Create a key, paste the code into a context or Oban job, and watch the email land in the message log.