---
title: Sending email
description: Verified-domain transactional email built into every project.
category: email
order: 1
agent: "Send transactional email from app code with a server-side POST to ${CODUCK_API_URL}/email/send using 'Authorization: Bearer ${CODUCK_API_KEY}' (both env vars are injected at deploy). From must be a verified domain or noreply@${NEXT_PUBLIC_CODUCK_PROJECT_SUBDOMAIN}.coduck.app. Also available from the terminal via 'coduck email send' once a domain is verified with 'coduck email domains add / verify'. SES-backed; includes suppression list and pause/resume. There is no @coduckai/sdk email helper yet."
---

# Sending email

CoDuck includes transactional email — verify a domain you own and send mail from it. CoDuck handles delivery, bounce processing, and a suppression list automatically. Backed by Amazon SES; you don't need an SES account. **Available on Pro and Studio plans only.**

## Why verified domains

Modern email providers reject mail without proper SPF and DKIM — you can't just send from `from@yourdomain.com` without proving you own `yourdomain.com`. CoDuck generates the DKIM records via SES; you set them on your registrar; verification confirms it worked.

## Add a sending domain

1. Open your project at `https://app.coduck.ai/project/<projectId>`.
2. Switch to the **Cloud** panel and open the **Email** tab.
3. Click **Add domain** and enter your domain (e.g. `example.com`).
4. CoDuck shows the DKIM CNAME records (typically 3) to set on your registrar. Add them.
5. Once DNS has propagated, click **Verify**.

Sending becomes available immediately after verification passes.

> [!NOTE]
> This is separate from the custom-domain verification used to serve your project's website. Verifying a domain for email only authorizes it for sending mail.

## Send a message

Three ways to send: the dashboard, the CLI, or your app's own code.

**From the dashboard.** Open the **Email** tab and use the **Send test** composer. Set the From (must be a verified domain or the fallback `noreply@<your-subdomain>.coduck.app` — see [The coduck.app fallback address](#the-coduckapp-fallback-address)), To, subject, and body.

**From your app's code.** Send with a server-side HTTP request to the email endpoint. `CODUCK_API_KEY` and `CODUCK_API_URL` are both injected into your project's environment at deploy time, so no configuration is needed. This is exactly what CoDuck's AI builder generates for you.

> [!IMPORTANT]
> Sending is **server-only**. Never expose `CODUCK_API_KEY` to the browser — call the endpoint from a route handler, server action, or API route, never from a client component. It also only works once the project is deployed: the key is not present in the in-editor preview.

```ts
// e.g. app/api/invite/route.ts (Next.js App Router)
export async function POST(req: Request) {
  const { to } = await req.json();

  const res = await fetch(`${process.env.CODUCK_API_URL}/email/send`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.CODUCK_API_KEY}`,
    },
    body: JSON.stringify({
      from: `noreply@${process.env.NEXT_PUBLIC_CODUCK_PROJECT_SUBDOMAIN}.coduck.app`,
      to,                                  // single recipient
      subject: "Welcome",                  // 1–998 chars
      html: "<p>Thanks for signing up.</p>",
      text: "Thanks for signing up.",      // html and/or text — at least one
      replyTo: "support@example.com",      // optional
    }),
  });

  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    // err.error is a stable code (see Error responses); err.message is human-readable.
    return Response.json({ error: err.error ?? "send_failed" }, { status: res.status });
  }

  const { messageId } = await res.json(); // 202 → { ok: true, messageId }
  return Response.json({ messageId });
}
```

> [!NOTE]
> There is no `@coduckai/sdk` email helper today — send via the HTTP endpoint above. A typed SDK wrapper may land later, but the endpoint is the supported, stable interface and won't change underneath you.

### The coduck.app fallback address

If you haven't verified a custom sending domain yet, paid projects can send from `noreply@<your-subdomain>.coduck.app`. `<your-subdomain>` is your project's **deployment subdomain** — the exact value in your project's URL (`https://<your-subdomain>.coduck.app`). It is injected into your app as `NEXT_PUBLIC_CODUCK_PROJECT_SUBDOMAIN`, so build the address from that rather than hardcoding it:

```ts
const from = `noreply@${process.env.NEXT_PUBLIC_CODUCK_PROJECT_SUBDOMAIN}.coduck.app`;
```

The local part (`noreply`, `hello`, …) is yours to choose; only the domain is validated, and it must match your project's own subdomain exactly — you cannot send from another project's subdomain. Once you verify your own domain, prefer sending from it for better deliverability and branding.

### Error responses

Non-2xx responses carry a stable `error` code plus a human-readable `message`. The ones to handle:

| Status | `error` | Meaning |
|---|---|---|
| 402 | `email_requires_paid_plan` | Project owner is on Free — email is Pro/Studio only. Don't retry. |
| 402 | `quota_exceeded` | Monthly quota reached. |
| 403 | `from_not_allowed` | `from` isn't a verified domain or your `coduck.app` subdomain. |
| 413 | `body_too_large` | HTML + text exceed 256 KB combined. |
| 422 | `recipient_suppressed` | Recipient previously bounced or complained. Don't auto-retry. |
| 429 | `rate_limited` / `new_account_cooldown` | Back off and retry; body includes `retryAfterSec` where applicable. |
| 503 | `project_paused` | Sending is paused (SES policy). Resume from the **Email** tab. |

**From the CLI.** For terminal or CI use, see [`coduck email send`](/docs/cli/commands).

## Suppressions

Hard bounces, spam complaints, and unsubscribes are automatically added to your project's per-project suppression list. Subsequent sends to those addresses fail with a 422 without ever hitting SES — this protects your sender reputation.

Manage the list from **Cloud panel → Email tab → Suppressions**.

> [!NOTE]
> Removing a suppressed address and re-sending to them is the fastest way to wreck your sender reputation — only do it when you've confirmed the suppression was a mistake.

## Pausing

High bounce or complaint rates auto-pause your project's sending. Resume from the **Email** tab once you've fixed the underlying issue (bad list, broken signup form, abuse from a compromised account).

## Quotas

| Plan | Included / month | Overage |
|---|---|---|
| Free | not available | — |
| Pro ($20/mo) | 5,000 | not available yet |
| Studio ($200/mo) | 50,000 | not available yet |

Quotas reset on the 1st of each month UTC. Sends past the monthly quota return a 402 error (`quota_exceeded`). Sends from a free-tier project return a 402 (`email_requires_paid_plan`).

## Limits

| Limit | Value |
|---|---|
| Rate | 50/min, 500/hour per project |
| Recipients per send | 1 |
| Body size | 256 KB combined HTML + text |
| Attachments | not supported yet |
| Use case | transactional only — not for newsletters or marketing |

For bulk marketing or newsletters, use a dedicated provider. CoDuck email is for receipts, password resets, alerts, and similar transactional traffic.

## Common failures

- **Verification fails after setting records** — DNS hasn't propagated. Wait 5-10 minutes and retry.
- **"From address not allowed"** — the From domain isn't verified yet. Check status in the **Email** tab.
- **Mail lands in spam** — also add a DMARC record to your domain. CoDuck only handles SPF and DKIM; DMARC is on you.

## For developers

A command-line interface is available for sending mail and managing suppressions from a terminal or CI environment. See [/docs/cli/commands](/docs/cli/commands).

## Next

- [Deploy your project](/docs/projects/deploy)
- [Custom domains](/docs/domains/custom-domains)
