Get Started

Quickstart

Send your first email in under five minutes.

This guide walks you from a fresh account to a delivered email in four steps. You'll need a verified domain on AWS SES (or a subdomain you can add DNS records for).

Heads up

If you haven't connected AWS yet, read the AWS SES Setup guide first - it takes about 15 minutes.

1. Create an API key

In the dashboard, go to Settings → API Keys and click Create key. Pick a permission level:

  • Full Access - every endpoint (read, send, manage)
  • Sending Only - restricted to POST /v1/emails

Your key is shown once, prefixed with efsa_. Copy it into your secrets manager immediately.

2. Verify your domain

Add a domain under Domains → Add domain. You'll receive three DNS records to add to your registrar:

TYPE    NAME                            VALUE
CNAME   abcdef._domainkey.yourdomain.com  abcdef.dkim.amazonses.com
CNAME   ghijkl._domainkey.yourdomain.com  ghijkl.dkim.amazonses.com
CNAME   mnopqr._domainkey.yourdomain.com  mnopqr.dkim.amazonses.com

Click Verify. DNS propagation is usually instant on Cloudflare, up to a few hours elsewhere.

3. Send your first email

Fire a request to the send endpoint:

curl -X POST https://api.yourdomain.com/v1/emails \
  -H "Authorization: Bearer efsa_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to":   "ada@lovelace.io",
    "subject": "You own this now.",
    "html":  "<p>Welcome aboard.</p>"
  }'

You'll get back an email ID. The status transitions through queued → sent → delivered as SNS events arrive.

4. Handle webhooks

Subscribe to delivery events under Settings → Webhooks. Webhooks fire for email.delivered, email.bounced, and email.complained. Each request is signed with HMAC-SHA256 over ${timestamp}.${body}:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(
  rawBody: string,
  headerSignature: string | null, // X-Webhook-Signature, e.g. "v1=abc..."
  headerTimestamp: string | null, // X-Webhook-Timestamp
  secret: string,
) {
  if (!headerSignature || !headerTimestamp) return false;
  const signed = `${headerTimestamp}.${rawBody}`;
  const expected = createHmac("sha256", secret).update(signed).digest("hex");
  const received = headerSignature.replace(/^v1=/, "");
  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex"),
  );
}

That's it, you're wired up. Next, explore the full API: