[↑] CodeOutbox← home

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:

  1. 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.
  2. Create a list (dashboard "+ Create list", or the API below).
  3. Embed the form on your site (dashboard "Copy embed").
  4. 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:

[![subscribe](https://app.codeoutbox.com/badge/<public_id>?style=join)](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

CommandWhat it does
co send <file> [--live]Preview or send a campaign
co sync [file]Reconcile lists from codeoutbox.json
co domains add|verify|listAuthenticate a sending domain
co brand show|setPer-tenant email branding
co webhooks add|list|rmEvent webhooks
co upgrade <plan> [--annual]Stripe checkout
co warmupIP warmup status
co token createMint 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.