Skip to content
View as Markdown

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

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:

MethodCallsReturns
.send()POST /v1/sendafter delivery, with messageId
.enqueue()POST /v1/enqueue (Pro)as soon as the email is queued
.build()nothingthe 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 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. 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 on /send, call the API with fetch directly.

Reading the API key ​

Runtime
Node.js, Vercel, Netlifyprocess.env.MARLEYFETCH_API_KEY
Cloudflare Workersthe env argument: env.MARLEYFETCH_API_KEY (set with wrangler secret put)
DenoDeno.env.get('MARLEYFETCH_API_KEY'), and import from npm:marleyfetch-sdk
BunBun.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.