# JavaScript SDK

`@marleyfetch/sdk` wraps the API in a chainable builder. It has no dependencies, uses the
runtime's native `fetch`, and weighs about 1 kB gzipped, so it runs anywhere `fetch` does:
Node.js 18+, Cloudflare Workers, Vercel and Netlify functions, Deno and Bun.

```bash
npm install @marleyfetch/sdk
```

Source, issues and releases: [github.com/marleyfetch/sdk](https://github.com/marleyfetch/sdk).

## Send an email

```ts
import { MarleyFetch } from '@marleyfetch/sdk'

const mf = new MarleyFetch({ apiKey: process.env.MARLEYFETCH_API_KEY })

const { messageId } = await mf
  .from('you@yourcompany.com')
  .to('alice@example.com', 'bob@example.com')
  .cc('manager@example.com')
  .subject('Monthly report')
  .text_body('Numbers are up.')
  .html_body('<p>Numbers are up.</p>')
  .send()
```

Every setter is named after the API field it sets and returns the builder, so you can call them
in any order. Nothing is sent until the chain ends with:

| Method | Calls | Returns |
| --- | --- | --- |
| `.send()` | `POST /v1/send` | after delivery, with `messageId` |
| `.enqueue()` | `POST /v1/enqueue` (Pro) | as soon as the email is queued |
| `.build()` | nothing | the request body — handy in tests |

`to`, `cc` and `bcc` accept several arguments, arrays or comma-separated strings, and add up
across calls.

## Send a batch

```ts
const { batch_id } = await mf.batch()
  .name('Renewal reminder')
  .template_id(3)          // or .subject() / .text_body() / .html_body()
  .audience_id(12)         // or .recipients(['a@example.com', ...])
  .send()

const { batch } = await mf.getBatch(batch_id)
console.log(batch.status, `${batch.sent_count}/${batch.total_count}`)

await mf.deleteBatch(batch_id) // once it is completed or failed
```

See [Batch sending](https://docs.marleyfetch.com/guides/batches.md) for how batches pace themselves and personalise content.

## Handle errors

Any non-2xx response throws a `MarleyFetchError`:

```ts
import { MarleyFetch, MarleyFetchError } from '@marleyfetch/sdk'

try {
  await mf.to('a@example.com').subject('Hi').text_body('Hi').send()
} catch (error) {
  if (error instanceof MarleyFetchError) {
    console.error(error.status, error.code, error.message)
    if (error.code === 'RATE_LIMITED' && error.retryAfter) {
      // wait error.retryAfter seconds, then retry
    }
  }
  throw error
}
```

`error.code` is one of the codes in [Errors](https://docs.marleyfetch.com/reference/errors.md). Network failures throw whatever
the runtime's `fetch` throws. An incomplete email (no recipient, no subject, no body) throws a
plain `Error` from `.build()` before any request is made.

The SDK does not send an `Idempotency-Key` yet. If you need
[safe retries](https://docs.marleyfetch.com/guides/sending.md#safe-retries-with-idempotency-key) on `/send`, call the API with
`fetch` directly.

## Reading the API key

| Runtime | |
| --- | --- |
| Node.js, Vercel, Netlify | `process.env.MARLEYFETCH_API_KEY` |
| Cloudflare Workers | the `env` argument: `env.MARLEYFETCH_API_KEY` (set with `wrangler secret put`) |
| Deno | `Deno.env.get('MARLEYFETCH_API_KEY')`, and import from `npm:@marleyfetch/sdk` |
| Bun | `Bun.env.MARLEYFETCH_API_KEY` |

On Cloudflare Workers, create the client inside the handler, where `env` is available:

```ts
import { MarleyFetch } from '@marleyfetch/sdk'

export default {
  async fetch(request: Request, env: { MARLEYFETCH_API_KEY: string }) {
    const mf = new MarleyFetch({ apiKey: env.MARLEYFETCH_API_KEY })
    await mf.to('ops@yourcompany.com').subject('Worker alert').text_body('Something happened').send()
    return new Response('sent')
  },
}
```

## Smaller imports

`MarleyFetch` pulls in both builders. If you only send single emails, import the pieces
(about 800 bytes gzipped):

```ts
import { createTransport } from '@marleyfetch/sdk/core'
import { EmailBuilder } from '@marleyfetch/sdk/email'

const transport = createTransport({ apiKey: process.env.MARLEYFETCH_API_KEY })

await new EmailBuilder(transport).to('a@example.com').subject('Hello').text_body('Hi').send()
```

`createTransport` returns a plain `(method, path, body) => Promise` function, which is easy to
stub in tests. The package is ESM-only.
