# Send email from Node.js

Five steps: install the SDK, create a test key, send to the sandbox, 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
```

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

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

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

try {
  const { id } = await avelto.emails.send({
    from: "you@sandbox.avelto.dev",
    to: "delivered@sandbox.avelto.dev",
    subject: "Hello from Avelto",
    text: "It works.",
  });
  console.log(id); // "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10"
} catch (err) {
  if (err instanceof AveltoError) {
    console.error(err.status, err.code, err.message);
  } else {
    throw err;
  }
}
```

**shell**

```bash
npx tsx send.ts
```

The API answers `201 Created` with the email id. 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

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

```ts
const email = await avelto.emails.get(id);

console.log(email.status); // "queued", then "sent", then "delivered"
for (const e of email.events) {
  console.log(e.type, e.occurred_at);
}
```

```json
{
  "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10",
  "mode": "test",
  "from": "you@sandbox.avelto.dev",
  "to": ["delivered@sandbox.avelto.dev"],
  "subject": "Hello from Avelto",
  "status": "delivered",
  "events": [
    {
      "id": "e1f0c3a4-8b2d-4c6e-9a1f-5d7b3e2c8a90",
      "type": "email.queued",
      "payload": {},
      "occurred_at": "2026-09-17T10:12:04.000Z"
    },
    {
      "id": "a7c2e9d1-3f4b-4a8e-b6c0-2d9e1f7b5c34",
      "type": "email.sent",
      "payload": { "test": true, "ses_message_id": "test-9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" },
      "occurred_at": "2026-09-17T10:12:06.000Z"
    },
    {
      "id": "c4b8d2f6-7e1a-4d3c-8f9b-6a2e0c5d1b78",
      "type": "email.delivered",
      "payload": { "test": true, "recipients": ["delivered@sandbox.avelto.dev"] },
      "occurred_at": "2026-09-17T10:12:06.000Z"
    }
  ]
}
```

## 5. Verify a domain

Add a domain, publish the DNS records it returns (three DKIM CNAMEs, an SPF TXT and a DMARC TXT), then poll `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"
```

Once the domain is verified, switch `AVELTO_API_KEY` to a live key (`av_live_`) and change `from` 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://avelto.dev/docs/quickstart/node
