CoDuck Docs

#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.

#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_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 });
}
// 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:

ts
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:

StatuserrorMeaning
402email_requires_paid_planProject owner is on Free — email is Pro/Studio only. Don't retry.
402quota_exceededMonthly quota reached.
403from_not_allowedfrom isn't a verified domain or your coduck.app subdomain.
413body_too_largeHTML + text exceed 256 KB combined.
422recipient_suppressedRecipient previously bounced or complained. Don't auto-retry.
429rate_limited / new_account_cooldownBack off and retry; body includes retryAfterSec where applicable.
503project_pausedSending 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

PlanIncluded / monthOverage
Freenot available
Pro ($20/mo)5,000not available yet
Studio ($200/mo)50,000not 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

LimitValue
Rate50/min, 500/hour per project
Recipients per send1
Body size256 KB combined HTML + text
Attachmentsnot supported yet
Use casetransactional 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.

#Next