> ## Documentation Index
> Fetch the complete documentation index at: https://developers.nateq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js SDK

> Send and read email from Node.js, with TypeScript types and no runtime dependencies.

The official Node.js client for the Nateq API. TypeScript-first, ships both ESM
and CommonJS, and has zero runtime dependencies.

<CardGroup cols={2}>
  <Card title="npm" icon="package" href="https://www.npmjs.com/package/@nateq/sdk">
    @nateq/sdk
  </Card>

  <Card title="GitHub" icon="git-branch" href="https://github.com/nateq-ai/sdk-node">
    Source and issues
  </Card>
</CardGroup>

## Requirements

Node.js 18 or newer.

## Install

```bash theme={null}
npm install @nateq/sdk
```

## Quickstart

Create an API key with the `emails:send` scope, then put it in your environment:

```bash theme={null}
NATEQ_API_KEY=tg_live_your_key_here
```

```ts theme={null}
import { Nateq } from "@nateq/sdk";

const nateq = new Nateq(); // reads NATEQ_API_KEY

const result = await nateq.emails.send({
  toEmails: ["customer@example.com"],
  subject: "Welcome aboard",
  htmlBody: "<p>Thanks for signing up.</p>",
});

console.log(result.id);
```

## Authentication

The SDK reads `NATEQ_API_KEY` by default. Pass it explicitly to load it from a
secret manager, or to talk to more than one organization:

```ts theme={null}
const nateq = new Nateq({ apiKey: await secrets.get("nateq_api_key") });
```

The key is validated locally before any request, so a mistyped key fails
immediately rather than going out over the network.

<Warning>
  The client holds your API key, so never render it. `console.log`, `JSON.stringify`,
  and `util.inspect` all redact it — but don't defeat that by logging the key
  directly.
</Warning>

| Scope         | Needed for                         |
| ------------- | ---------------------------------- |
| `emails:send` | `emails.send()`                    |
| `emails:read` | `emails.get()` and `emails.list()` |

## Sending email

`send()` resolves once the API accepts the message. Delivery happens afterwards —
a resolved promise means Nateq took responsibility for it, not that it landed.

```ts theme={null}
const result = await nateq.emails.send({
  toEmails: ["a@example.com", "b@example.com"],
  subject: "Quarterly update",
  htmlBody: "<p>Hello</p>",
  plainTextBody: "Hello",
  fromEmail: "hello@yourdomain.com", // must be a verified address
  fromName: "Acme Support",
  ccEmails: ["c@example.com"],
  bccEmails: ["d@example.com"],
  replyTo: "support@yourdomain.com",
  ticketId: "…",
  attachmentIds: ["…"],
  headers: { "X-Campaign": "q3" },
});
```

Provide `htmlBody`, `plainTextBody`, or both. Omit `fromEmail` and
`emailAddressId` to use your organization's default verified address.

## Reading email

```ts theme={null}
const email = await nateq.emails.get(result.id);

email.status; // "delivered"
email.openCount;
email.bounceReason;
```

List sends, newest first:

```ts theme={null}
const page = await nateq.emails.list({
  status: "bounced",
  toEmail: "customer@example.com",
  limit: 25,
});

for (const email of page.emails) {
  console.log(email.subject);
}

page.total;
```

`limit` defaults to 50 and is capped at 100.

## Errors

Every error extends `NateqError`, so one `catch` covers the surface:

```ts theme={null}
import {
  NateqError,
  NateqPermissionError,
  NateqRateLimitError,
  NateqValidationError,
} from "@nateq/sdk";

try {
  await nateq.emails.send({ /* … */ });
} catch (err) {
  if (err instanceof NateqValidationError) {
    // 400/422 — the request was wrong. Don't retry it unchanged.
  } else if (err instanceof NateqPermissionError) {
    // 403 — e.g. the key lacks emails:send
    err.code; // "INSUFFICIENT_SCOPE"
  } else if (err instanceof NateqRateLimitError) {
    err.retryAfter; // seconds
  } else if (err instanceof NateqError) {
    err.status;
    err.requestId; // quote this to support
  }
}
```

| Error                      | When                                                |
| -------------------------- | --------------------------------------------------- |
| `NateqValidationError`     | 400/422, or bad arguments caught before any request |
| `NateqAuthenticationError` | 401 — key missing, malformed, revoked               |
| `NateqPermissionError`     | 403 — key valid, but not allowed                    |
| `NateqNotFoundError`       | 404                                                 |
| `NateqRateLimitError`      | 429 — carries `retryAfter`                          |
| `NateqServerError`         | 5xx                                                 |
| `NateqTimeoutError`        | request exceeded the timeout                        |
| `NateqConnectionError`     | DNS, TLS, socket, offline                           |
| `NateqConfigurationError`  | bad SDK setup — thrown before any request           |

See [Errors](/errors) for the underlying API codes.

## Retries and duplicate sends

Reads (`get`, `list`) retry automatically on timeouts, 5xx, and connection
failures, with exponential backoff that honours `Retry-After`.

**`send()` does not.** The API has no idempotency key, so a send that fails
*after* reaching the server may already have gone out. Replaying it could mail
your customer twice, and the SDK won't make that call for you.

The exception is a **429**, which the API raises before anything is sent — so it
is retried, because a duplicate is impossible.

If a `send()` throws `NateqTimeoutError` or `NateqServerError`, the outcome is
genuinely unknown. Look it up before retrying:

```ts theme={null}
try {
  await nateq.emails.send({ toEmails: [user.email], subject, htmlBody });
} catch (err) {
  if (err instanceof NateqTimeoutError || err instanceof NateqServerError) {
    const recent = await nateq.emails.list({ toEmail: user.email, limit: 5 });
    // decide based on what's there
  }
}
```

## Configuration

```ts theme={null}
const nateq = new Nateq({
  apiKey: "tg_live_…",
  baseUrl: "https://api.nateq.io/api/v1",
  timeout: 30_000, // ms
  maxRetries: 2, // 0 disables
});
```

`baseUrl` must be `https`, unless it points at localhost for local development.

## Framework usage

<CodeGroup>
  ```ts Next.js theme={null}
  // app/api/welcome/route.ts
  import { Nateq } from "@nateq/sdk";

  const nateq = new Nateq();

  export async function POST(request: Request) {
    const { email } = await request.json();

    await nateq.emails.send({
      toEmails: [email],
      subject: "Welcome aboard",
      htmlBody: "<p>Thanks for signing up.</p>",
    });

    return Response.json({ ok: true });
  }
  ```

  ```ts NestJS theme={null}
  import { Module, Injectable } from "@nestjs/common";
  import { Nateq } from "@nateq/sdk";

  @Module({
    providers: [{ provide: Nateq, useFactory: () => new Nateq() }],
    exports: [Nateq],
  })
  export class NateqModule {}

  @Injectable()
  export class WelcomeMailer {
    constructor(private readonly nateq: Nateq) {}

    async send(email: string) {
      await this.nateq.emails.send({
        toEmails: [email],
        subject: "Welcome aboard",
        htmlBody: "<p>Thanks for signing up.</p>",
      });
    }
  }
  ```

  ```ts Express theme={null}
  import express from "express";
  import { Nateq } from "@nateq/sdk";

  const nateq = new Nateq();
  const app = express();

  app.post("/welcome", express.json(), async (req, res, next) => {
    try {
      await nateq.emails.send({
        toEmails: [req.body.email],
        subject: "Welcome aboard",
        htmlBody: "<p>Thanks for signing up.</p>",
      });
      res.json({ ok: true });
    } catch (err) {
      next(err);
    }
  });
  ```
</CodeGroup>

<Note>
  Construct the client once and reuse it. It's stateless and safe to share.
</Note>

<Check>
  Sending inside a request ties your response time to ours. For anything
  non-critical, send from a background job or queue instead.
</Check>
