#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
- Open your project at
https://app.coduck.ai/project/<projectId>. - Switch to the Cloud panel and open the Email tab.
- Click Add domain and enter your domain (e.g.
example.com). - CoDuck shows the DKIM CNAME records (typically 3) to set on your registrar. Add them.
- Once DNS has propagated, click Verify.
Sending becomes available immediately after verification passes.
#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), 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_KEYto 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.
// 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 });
}// 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 });
}#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:
const from = `noreply@${process.env.NEXT_PUBLIC_CODUCK_PROJECT_SUBDOMAIN}.coduck.app`;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.
#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.
#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.