# Send email from Express

Five steps: install the SDK, create a test key, send to the sandbox from a POST route, read the log, then verify a domain so you can send to anyone.

## 1. Install

The SDK needs Node 20 or later and has no runtime dependencies. The scripts below use top-level `await`, so set `"type": "module"` in your package.json if it is not already.

**shell**

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

## 2. Create an API key

[Sign in](/login), open **API keys** in the dashboard and create a **test** key. It starts with `av_test_`. Export it so the server can read it:

**shell**

```bash
export 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 POST route that calls the SDK and passes the API's error envelope through when a send is rejected. Other errors go to `next` so Express handles them as usual.

```ts
// server.ts
import express from "express";
import { Avelto, AveltoError } from "@avelto/sdk";

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

app.post("/send", async (_req, res, next) => {
  try {
    const { id } = await avelto.emails.send({
      from: "you@sandbox.avelto.dev",
      to: "delivered@sandbox.avelto.dev",
      subject: "Hello from Avelto",
      text: "It works.",
    });
    res.status(201).json({ id });
  } catch (err) {
    if (err instanceof AveltoError) {
      res
        .status(err.status || 502)
        .json({ error: { code: err.code, message: err.message } });
    } else {
      next(err);
    }
  }
});

app.listen(3000);
```

Start the server with `npx tsx server.ts` and call the route:

**shell**

```bash
curl -X POST http://localhost:3000/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

Add a GET route that 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.get("/emails/:id", async (req, res, next) => {
  try {
    const email = await avelto.emails.get(req.params.id);
    res.json({
      status: email.status, // "queued", then "sent", then "delivered"
      events: email.events.map((e) => e.type),
    });
  } catch (err) {
    next(err);
  }
});
```

**shell**

```bash
curl http://localhost:3000/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. Add the DNS records it prints (three DKIM CNAMEs, an SPF TXT and a DMARC TXT) at your DNS provider; it polls `domains.get` until `status` is `verified`. Use a subdomain such as `mail.acme.com`.

```ts
// 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 verify-domain.ts
```

Once the domain is verified, switch `AVELTO_API_KEY` to a live key (`av_live_`) and change `from` in the route 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, with an Express example.
- [Test mode](/docs/test-mode): test keys, the sandbox sender and the simulator addresses.

---

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