# Send email from Next.js

Five steps: install the SDK, create a test key, send to the sandbox from a route handler, read the log, then verify a domain so you can send to anyone. The SDK runs on the server only; your API key never reaches the browser.

## 1. Install

The SDK needs Node 20 or later and has no runtime dependencies.

**shell**

```bash
npm install @avelto/sdk
```

## 2. Create an API key

[Sign in](/login), open **API keys** in the dashboard and create a **test** key. It starts with `av_test_`. Put it in `.env.local`, which Next.js loads on the server and never ships to the client:

**.env.local**

```bash
# .env.local
AVELTO_API_KEY=av_test_...
```

> **Sandbox rules.** Test keys never deliver anything; they run the pipeline and record events. The sandbox sender `you@sandbox.avelto.dev` only delivers to your account's verified owner email and to the simulator addresses `delivered@`, `bounced@` and `complained@sandbox.avelto.dev`. Anything else is refused with `403 sandbox_recipient_not_allowed`. To send to anyone, verify a domain (step 5).

## 3. Send your first email

A route handler in the App Router. It calls the SDK server-side and passes the API's error envelope through when a send is rejected.

```ts
// app/api/send/route.ts
import { Avelto, AveltoError } from "@avelto/sdk";
import { NextResponse } from "next/server";

const avelto = new Avelto(process.env.AVELTO_API_KEY!);

export async function POST() {
  try {
    const { id } = await avelto.emails.send({
      from: "you@sandbox.avelto.dev",
      to: "delivered@sandbox.avelto.dev",
      subject: "Hello from Avelto",
      text: "It works.",
    });
    return NextResponse.json({ id }, { status: 201 });
  } catch (err) {
    if (err instanceof AveltoError) {
      return NextResponse.json(
        { error: { code: err.code, message: err.message } },
        { status: err.status || 502 },
      );
    }
    throw err;
  }
}
```

Start `npm run dev` and call the route:

**shell**

```bash
curl -X POST http://localhost:3000/api/send
```

The API answers `201 Created` with the email id, and the route returns the same. Anything else is an `AveltoError`, and the route passes its `status`, `code` and `message` back as the same error envelope.

```http
HTTP/1.1 201 Created
Content-Type: application/json

{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }
```

> **Retries are built in.** The SDK retries a request up to three times with exponential backoff and jitter: always on `429`, `502` and `503`, and on network errors and `504` too when repeating is safe (reads, deletes and sends, which carry an idempotency key). Every `emails.send` carries an `Idempotency-Key` (a random UUID unless you pass `idempotencyKey`), so a retried send never produces a second email. See [Retries](/docs/sdk) to tune or disable it.

## 4. Check the log

A second handler fetches the email by id. `status` moves from `queued` to `sent` to `delivered`, and `events` records each step: `email.queued`, `email.sent`, `email.delivered`.

```ts
// app/api/emails/[id]/route.ts
import { Avelto } from "@avelto/sdk";
import { NextResponse } from "next/server";

const avelto = new Avelto(process.env.AVELTO_API_KEY!);

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const email = await avelto.emails.get(id);
  return NextResponse.json({
    status: email.status, // "queued", then "sent", then "delivered"
    events: email.events.map((e) => e.type),
  });
}
```

**shell**

```bash
curl http://localhost:3000/api/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10
```

## 5. Verify a domain

Adding a domain is a one-off task, so run it as a script rather than a route. It publishes nothing itself: add the DNS records it prints (three DKIM CNAMEs, an SPF TXT and a DMARC TXT) at your DNS provider, and it polls `domains.get` until `status` is `verified`. Use a subdomain such as `mail.acme.com`.

```ts
// scripts/verify-domain.ts
import { Avelto } from "@avelto/sdk";

const avelto = new Avelto(process.env.AVELTO_API_KEY!);

const domain = await avelto.domains.create({ name: "mail.acme.com" });

for (const r of domain.dns_records) {
  console.log(`${r.type}\t${r.name}\t${r.value}\t(${r.purpose})`);
}

// Publish the records, then poll. domains.get re-checks DNS on every call.
let status = domain.status;
while (status === "pending") {
  await new Promise((r) => setTimeout(r, 30_000));
  status = (await avelto.domains.get(domain.id)).status;
}
console.log(status); // "verified" or "failed"
```

**shell**

```bash
npx tsx --env-file=.env.local scripts/verify-domain.ts
```

Once the domain is verified, switch `AVELTO_API_KEY` to a live key (`av_live_`) and change `from` in the route handler to an address on it, such as `hello@mail.acme.com`. Nothing else changes.

## Next

- [Send email](/docs/send-email): every field, attachments, tags, scheduling and idempotency.
- [Webhooks](/docs/webhooks): get events pushed to your app.
- [Test mode](/docs/test-mode): test keys, the sandbox sender and the simulator addresses.

---

Rendered page: https://staging.avelto.dev/docs/quickstart/nextjs
