CodeOutbox docs
Email marketing that lives in your codebase: paste a form, write newsletters in Markdown, and send from the CLI, the API, or your coding agent. We run our own mail servers, so deliverability is handled.
Quickstart
Fastest: run npx codeoutbox in your project. No account needed: it writes a form component and creates a list that works right away. Claim it with the link it prints (or add --email you@example.com to get a claim link when your first subscriber confirms). Unclaimed lists hold up to 25 subscribers and are deleted after 14 days.
Or start from an account:
- Sign in at app.codeoutbox.com. It's passwordless. The dashboard walks you through your first list, form and email; create an API key for the CLI under Settings → API keys.
- Create a list (dashboard "+ Create list", or the API below).
- Embed the form on your site (dashboard "Copy embed").
- Send a Markdown newsletter with
co send.
The CLI talks to the API; configure it once:
export CO_URL=https://app.codeoutbox.com
export CO_TOKEN=co_live_... # your API key
Capture subscribers
Every list has a form endpoint. Drop this anywhere — it works with no JavaScript (native POST → double opt-in):
<form action="https://app.codeoutbox.com/f/<public_id>" method="POST">
<input type="email" name="email" required placeholder="you@example.com">
<button type="submit">Subscribe</button>
</form>
Or let the scaffolder detect your stack (React/Next, Vue/Nuxt, Svelte, Astro, plain HTML — Tailwind or not) and write a styled, accessible component into your project:
npx codeoutbox --id <public_id>
# ✅ react form (Tailwind) → src/components/CodeOutboxForm.tsx
Submissions get a confirmation email (double opt-in). Unsubscribes and one-click List-Unsubscribe are handled for you.
Write & send
A campaign is Markdown with frontmatter, kept in your repo:
---
subject: Launch day 🚀
group: newsletter
preheader: We shipped.
---
Hi there 👋
We just **launched**. [Read the post](https://example.com/post).
co send launch.md # dry run: recipients + spam-lint
co send launch.md --live # actually send
Add send_at: 2026-07-01T09:00:00Z to the frontmatter to schedule it, or draft: true to make it previewable but unsendable.
Merge tags & custom fields
Personalize the subject and body with any subscriber value. Add a fallback for people who don't have one:
---
subject: Hi {{first_name | "there"}}
group: newsletter
---
Here's what's new for **{{company}}** this month.
[Manage your account](https://example.com/account?email={{email}})
Built in: {{email}}, {{name}}, {{first_name}}, {{last_name}}. Anything else is a custom field. Extra inputs on your form become fields automatically (<input name="company">), and so do fields: {…} on the API and extra columns in an import. co send previews warn when recipients are missing a value that has no fallback. Values are escaped, so subscriber input can't inject HTML.
Import & export
Moving from Mailchimp, Kit, Substack or Buttondown? Import their CSV export as-is (step-by-step switching guides). Headers like Email Address and First Name are recognized, unsubscribed and bounced rows are left out, and other columns become custom fields:
In the dashboard: Lists → Import subscribers. From a terminal (with an API key):
CO_TOKEN=… npx codeoutbox import newsletter mailchimp.csv --confirmed # they opted in already
CO_TOKEN=… npx codeoutbox import newsletter leads.csv --send-optin # email each a confirm link
Your coding agent can do it too, with the MCP tool import_subscribers. To export: GET /v1/groups/<list>/subscribers/export?format=csv.
Your list is yours: export includes every subscriber, status and field.
Hosted signup page, archive & badge
Every list has a signup page in your brand at /l/<public_id>. Link to it from anywhere, no website needed. Turn on the archive to list past issues there too, each as its own web page:
co share newsletter # page URL + README badge
co archive on newsletter # past issues become public (archive: false keeps one out)
The badge shows your live count and links to the signup page, so a click becomes a subscribe:
[](https://app.codeoutbox.com/l/<public_id>)
GitHub Action — review in a PR, send on merge
Open a pull request that adds a campaign and CodeOutbox comments with the subject, the list, how many subscribers it will reach, spam-lint warnings, and a link to the rendered email. Merge it and it sends. Re-runs never double-send, and draft: true files are skipped.
# .github/workflows/newsletter.yml
name: Newsletter
on:
pull_request:
paths: ["campaigns/**"]
push:
branches: [main]
paths: ["campaigns/**"]
permissions:
contents: read
pull-requests: write
jobs:
codeoutbox:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Cellcote/CodeOutbox/action@main
with:
api-token: ${{ secrets.CODEOUTBOX_TOKEN }}
Create the token with co token create --name github and save it as the CODEOUTBOX_TOKEN repository secret. Inputs: paths (default campaigns/**/*.md), mode (auto | preview | send), url.
CLI reference
| Command | What it does |
|---|---|
co send <file> [--live] | Preview or send a campaign |
co sync [file] | Reconcile lists from codeoutbox.json |
co domains add|verify|list | Authenticate a sending domain |
co brand show|set | Per-tenant email branding |
co webhooks add|list|rm | Event webhooks |
co upgrade <plan> [--annual] | Stripe checkout |
co warmup | IP warmup status |
co token create | Mint an API key |
API
REST + Bearer auth (your API key). Base: https://app.codeoutbox.com.
# create a list
curl -X POST $CO_URL/v1/groups -H "Authorization: Bearer $CO_TOKEN" \
-H 'Content-Type: application/json' -d '{"slug":"news","name":"News"}'
# add a (consented) subscriber
curl -X POST $CO_URL/v1/groups/news/subscribers -H "Authorization: Bearer $CO_TOKEN" \
-H 'Content-Type: application/json' -d '{"email":"a@example.com","confirmed":true}'
# send
curl -X POST $CO_URL/v1/broadcasts -H "Authorization: Bearer $CO_TOKEN" \
-H 'Content-Type: application/json' -d '{"source":"","confirm":true}'
Other endpoints: /v1/usage, /v1/broadcasts (list + stats), /v1/domains, /v1/webhooks, /v1/account (brand), /v1/billing/checkout.
Domain authentication
By default you send from our shared, authenticated domain. To send from your own domain (and remove limits), authenticate it:
co domains add news.yourdomain.com # prints SPF / DKIM / DMARC records
# publish those TXT records in your DNS, then:
co domains verify news.yourdomain.com
Once verified, broadcasts send from noreply@news.yourdomain.com, DKIM-signed with your own key. You can also do this from the dashboard "Sending domains" card.
Event webhooks
Get subscriber events POSTed to your app:
co webhooks add https://your-app.com/hook --events subscriber.confirmed,subscriber.bounced
Events: subscriber.confirmed subscriber.unsubscribed subscriber.bounced subscriber.complained. Each request is signed:
X-CO-Event: subscriber.confirmed
X-CO-Signature: t=<unix>,v1=<hex>
# verify: v1 == HMAC-SHA256(secret, `${t}.${rawBody}`)
Branding
Tenant email wears your brand, not ours. It auto-derives from your verified domain; override anytime:
co brand set --name "Acme" --color "#2E7D32" --logo https://acme.com/logo.png
Scheduling
Add send_at (ISO 8601, UTC) to the campaign frontmatter and co send --live queues it; a durable worker delivers it at the scheduled time:
---
subject: Weekly digest
group: newsletter
send_at: 2026-07-01T09:00:00Z
---
Coding agent (MCP)
The MCP server lets Claude Code, Cursor or any MCP client set up and run your newsletter: create a list, add the form to your site, share the signup page, and preview and send campaigns. Sending always waits for your go-ahead.
# Claude Code (create the key in the dashboard: Settings → API keys)
claude mcp add codeoutbox -e CO_TOKEN=co_live_… -- npx -y codeoutbox mcp
Other clients take the same command in their MCP config:
{
"mcpServers": {
"codeoutbox": {
"command": "npx",
"args": ["-y", "codeoutbox", "mcp"],
"env": { "CO_TOKEN": "co_live_…" }
}
}
}
Then ask: "add a newsletter to this site". In Claude Code the add-newsletter prompt does the whole setup: list, form component, signup page, and a first email to preview. No key yet? The server still works without one: it creates a list you claim later.
Tools: create_group, create_provisional_list, list_groups, get_signup_page, subscriber_count, add_subscriber, preview_broadcast, send_test, send_broadcast, list_broadcasts, set_archive, add_domain, verify_domain, list_domains.