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

# Node.js SDK

> Official @reallyquickemails/sdk for Node.js, Bun, Deno, and edge runtimes.

Official ReallyQuickEmails SDK for Node.js / TypeScript. It covers all 5 modules: `emails`, `leads`, `automations`, `events`, and `senders`.

* **Repo:** [github.com/DropoutCapital/reallyquickemails-sdk](https://github.com/DropoutCapital/reallyquickemails-sdk)
* **npm:** [@reallyquickemails/sdk](https://www.npmjs.com/package/@reallyquickemails/sdk)
* **License:** MIT
* **Size:** \~13 KB CJS / \~12 KB ESM, zero runtime deps (uses native `fetch`)

<Info>
  Works in any runtime with native `fetch`: Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge.
  **It does NOT run in the browser**: the constructor throws an error if it detects `window`, to avoid exposing the API key in the client bundle.
</Info>

## Installation

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install @reallyquickemails/sdk
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add @reallyquickemails/sdk
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add @reallyquickemails/sdk
    ```
  </Tab>

  <Tab title="bun">
    ```bash theme={null}
    bun add @reallyquickemails/sdk
    ```
  </Tab>
</Tabs>

## Quickstart

<Steps>
  <Step title="Get an API key">
    Generate a key in `Settings → Integrations → API Keys`. To get started, use a **Test key** (`sk_test_*`): it sends real emails without polluting your production metrics. See [API Keys](/en/guides/api-keys).

    ```bash theme={null}
    export RQE_API_KEY="sk_test_your_test_key"
    ```
  </Step>

  <Step title="Initialize the client">
    ```ts theme={null}
    import { RQE } from '@reallyquickemails/sdk';

    const rqe = new RQE({ apiKey: process.env.RQE_API_KEY });
    ```

    If you omit `apiKey`, the SDK reads `process.env.RQE_API_KEY`. To point to another backend (staging, self-hosted), pass `baseUrl` or set `RQE_BASE_URL`:

    ```ts theme={null}
    const rqe = new RQE({
      apiKey: process.env.RQE_API_KEY,
      baseUrl: 'https://api-staging.example.com',
    });
    ```
  </Step>

  <Step title="Send your first email">
    ```ts theme={null}
    const { data, error } = await rqe.emails.send({
      sender: 'support@yourdomain.com',     // verified sender
      recipient: 'user@example.com',
      subject: 'Welcome',
      html: '<p>Hello from RQE</p>',
    });

    if (error) {
      console.error('Send failed:', error.message);
    } else {
      console.log('Email queued, id:', data.email_id);
    }
    ```

    The sender must be verified in your project. See [domains](/en/api-reference/domains).
  </Step>
</Steps>

## Error handling

Each method returns `{ data, error, headers }`. **Always check `error` before using `data`** — `data` is `null` when there's an error.

```ts theme={null}
const { data, error, headers } = await rqe.emails.send({ /* ... */ });

if (error) {
  switch (error.statusCode) {
    case 401: /* Invalid API key */ break;
    case 429: /* Rate limit — wait and retry */ break;
    default:  console.error(error);
  }
  return;
}

console.log(data.email_id);
console.log('Idempotency replayed:', headers['idempotency-replayed']);
```

| `error.name`        | When                                                   |
| ------------------- | ------------------------------------------------------ |
| `unauthorized`      | Invalid or missing API key                             |
| `validation_error`  | Invalid body (missing required fields, bad format)     |
| `not_found`         | Resource does not exist (lead, automation, event)      |
| `network_error`     | Network failure — no response reached from the backend |
| `parse_error`       | Backend returned a non-JSON response                   |
| `application_error` | Generic backend error (5xx)                            |

## Modules

### `emails` — transactional + broadcast

```ts theme={null}
// Single email
await rqe.emails.send({
  sender: 'support@brand.com',
  recipient: 'user@example.com',
  subject: 'Confirmation',
  html: '<p>Your order was received.</p>',
  attachments: [{ filename: 'invoice.pdf', content: base64Pdf }],
});

// Broadcast — one template to up to 10,000 recipients
await rqe.emails.sendBatch({
  sender: 'noreply@brand.com',
  subject: 'Hello {{name}}',
  html: '<p>Hello {{name}}, your {{plan}} plan renews soon.</p>',
  recipients: [
    { email: 'a@x.com', data: { name: 'Alice', plan: 'Pro' } },
    { email: 'b@x.com', data: { name: 'Bob', plan: 'Free' } },
  ],
});
```

### `leads` — contact management

```ts theme={null}
// Upsert by email
await rqe.leads.upsert({
  email: 'user@example.com',
  data: { plan: 'pro', signup_date: '2026-04-30' },
  segment_ids: ['seg-uuid-1'],
});

// Bulk up to 1000
await rqe.leads.upsertMany({
  leads: [
    { email: 'a@x.com', data: { plan: 'pro' } },
    { email: 'b@x.com' },
  ],
});

// Tags by email
await rqe.leads.addTags('user@example.com', ['vip', 'beta']);
const { data } = await rqe.leads.getTags('user@example.com');

// Custom attributes (raw body, NOT wrapped)
await rqe.leads.setAttributes('user@example.com', {
  total_orders: 42,
  last_order_date: '2026-04-30',
});

// List with pagination
const page = await rqe.leads.list({ page: 1, per_page: 50, search: 'pro' });

// Get / update / delete by UUID
const { data: { lead } } = await rqe.leads.get('lead-uuid');
await rqe.leads.update('lead-uuid', { data: { plan: 'enterprise' } });
await rqe.leads.delete('lead-uuid');
```

### `automations` — trigger flows

```ts theme={null}
// Idempotent enroll — pass trigger_reference_id so that multiple calls
// with the same external_id do NOT create duplicate runs, but instead update the existing run
await rqe.automations.enroll('automation-uuid', {
  email: 'user@example.com',
  data: { first_name: 'Adriano', plan: 'pro' },
  trigger_reference_id: 'order-12345',
});

// Manual enroll (UI-style bulk)
await rqe.automations.manualEnroll('automation-uuid', {
  emails: ['a@x.com', 'b@x.com'],
});

// Cancel by recipient
await rqe.automations.cancel('automation-uuid', {
  email: 'user@example.com',
  reason: 'payment_succeeded',
});

// Cancel by external_id (when you don't have the email)
await rqe.automations.cancelByExternalId('automation-uuid', {
  external_id: 'order-12345',
});
```

### `events` — custom tracking

```ts theme={null}
// Single event
await rqe.events.track({
  email: 'user@example.com',
  event: 'offer_accepted',
  properties: { offer_id: 'xyz', amount_usd: 5000 },
});

// Bulk up to 1000
await rqe.events.bulk([
  { email: 'a@b.com', event: 'page_view', properties: { url: '/pricing' } },
  { email: 'c@d.com', event: 'page_view', properties: { url: '/features' } },
]);

// List with filters + offset pagination
const { data } = await rqe.events.list({
  email: 'user@example.com',
  event: 'offer_accepted',
  since: '2026-04-01T00:00:00Z',
  per_page: 50,
});
```

### `senders` — list / verify senders (v0.1.1+)

Discover which senders are verified, verify new ones via magic link, and check a sender's status.

<Warning>
  This module requires `projectId` in the constructor (or the `RQE_PROJECT_ID` env). Find it in the dashboard URL: `app.reallyquickemails.com/projects/<projectId>/...`. The other modules (emails, leads, automations, events) don't need this parameter.
</Warning>

```ts theme={null}
const rqe = new RQE({
  apiKey: process.env.RQE_API_KEY,
  projectId: process.env.RQE_PROJECT_ID,
});

// List all senders
const { data } = await rqe.senders.list();
// data.senders = [{ email, sender_name, status, type, ... }, ...]

// Filter only verified ones
const { data: verified } = await rqe.senders.list({ status: 'verified' });

// Choose a sender at runtime based on context
const transactionalSender = verified.senders.find(s => s.email.startsWith('noreply@'));
await rqe.emails.send({
  sender: transactionalSender.email,
  recipient: 'user@example.com',
  subject: 'Confirmation',
  html: '<p>...</p>',
});

// Verify a new sender (a magic link is sent to the email)
await rqe.senders.verifyEmail({ email: 'support@brand.com' });

// Check the status of a specific sender
const { data: st } = await rqe.senders.status('support@brand.com');
// st.status: 'Success' | 'Pending' | 'Failed' | 'NotStarted'
```

Typical use cases:

* **Multi-tenant** — list the active project's senders and choose based on the client
* **Failover** — if a sender fails DKIM/DMARC, automatically fall back to another verified one
* **Onboarding wizard** — UI shows available senders for the client to choose from
* **Health check** — periodic polling of status to detect failed senders

## Idempotency

For safe retries (network timeouts, Lambda retries), pass `idempotencyKey` on write operations. Currently supported on `emails.send` and `emails.sendBatch`.

```ts theme={null}
await rqe.emails.send(
  {
    sender: 'support@brand.com',
    recipient: 'user@example.com',
    subject: 'Order #123',
    html: '<p>Confirmation</p>',
  },
  { idempotencyKey: 'order-123-confirmation' },
);
```

The backend caches the response for 24h for the `(project_id, idempotencyKey)` pair. A second call with the same key returns the cached response **without re-processing** and adds the `Idempotency-Replayed: true` header.

```ts theme={null}
const { data, headers } = await rqe.emails.send(
  { /* ... */ },
  { idempotencyKey: 'unique-id' },
);
if (headers['idempotency-replayed'] === 'true') {
  console.log('Replay — the backend had already processed this request');
}
```

<Info>
  More on the backend's behavior in [API Keys → Idempotency](/en/guides/api-keys).
</Info>

## Runtime compatibility

| Runtime            | Supported | Notes                                                         |
| ------------------ | --------- | ------------------------------------------------------------- |
| Node.js 18+        | ✅         | Native `fetch`                                                |
| Node.js 16         | ❌         | No native `fetch`                                             |
| Bun                | ✅         |                                                               |
| Deno               | ✅         |                                                               |
| Cloudflare Workers | ✅         |                                                               |
| Vercel Edge        | ✅         |                                                               |
| Browser            | ❌         | The SDK throws if it detects `window` to protect your API key |

## Configuration

| Option      | Type     | Default                                                           | Description                                                                                             |
| ----------- | -------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `apiKey`    | `string` | `process.env.RQE_API_KEY`                                         | Your project's API key                                                                                  |
| `baseUrl`   | `string` | `process.env.RQE_BASE_URL` or `https://api.reallyquickemails.com` | Override for staging or self-hosted                                                                     |
| `projectId` | `string` | `process.env.RQE_PROJECT_ID`                                      | Project UUID. **Required only by the `senders` module** — the other modules derive it from the API key. |

## Support

* **Bugs and feature requests:** [GitHub Issues](https://github.com/DropoutCapital/reallyquickemails-sdk/issues)
* **Email:** [harold@dropout.cl](mailto:harold@dropout.cl)
