FlaresendDocs

HTTP client

Reference for the Flaresend class. Every namespace and method, with retries and idempotency explained.

import { Flaresend } from '@flaresend/client';

const flaresend = new Flaresend({
  apiKey: process.env.FLARESEND_API_KEY!,
  baseUrl: 'https://mailer.example.com',
});

Options

Prop

Type

Every request sends Authorization: Bearer <apiKey> and Accept: application/json, and JSON bodies with Content-Type: application/json.

Retries

RequestRetried on 429, 5xx and network errors?
Every GETYes
emails.send, emails.sendBatchYes. Each call carries an idempotency key, so a retry never sends twice.
Every other POST, PATCH, DELETENo. Tried once.
  • The wait between tries is exponential backoff with jitter: about 0.5 s, 1 s, 2 s and so on, capped at 8 s.
  • If the response has a Retry-After header, the client waits that long instead. If Retry-After is more than 60 seconds, it doesn't wait and throws the error.
  • 429 daily_limit_exceeded is never retried: waiting a few seconds won't help.
  • Any other 4xx is thrown at once.

Idempotency

Every emails.send call gets an idempotency key, sent as the Idempotency-Key header. The key is, in order:

  1. opts.idempotencyKey, the second argument.
  2. input.idempotencyKey, in the email itself.
  3. A random auto_<uuid> made for this call.

The same key is reused for that call's retries, so a timeout followed by a retry sends one email, not two. The random key does not protect you from your own code calling send twice. For that, pass a key that is stable for the thing you're sending:

await flaresend.emails.send(input, { idempotencyKey: `welcome-${user.id}` });

A second call with the same key and the same content returns the first email with idempotent: true. The same key with different content throws 409 idempotency_payload_mismatch. Keys don't expire. See Idempotency.

emails.sendBatch works the same way with one key for the whole batch; the mailer stores it per item as <key>:<index>.

Methods

Paginated methods return { data, nextCursor }. Pass nextCursor back as cursor for the next page; it is null on the last page. Other list methods return a plain array.

me()

me(): Promise<MeRecord>

GET /v1/me. Returns { project: { id, slug, name }, key: { id, name, mode } }. A cheap way to check the key and URL work.

emails

MethodEndpointReturns
send(input, opts?)POST /v1/emails{ id, status, idempotent? }
sendBatch(inputs, opts?)POST /v1/emails/batch{ data }, or { dryRun: true, data } with opts.dryRun
get(id)GET /v1/emails/:idEmailRecord with recipients and events
list(query?)GET /v1/emailsListResponse<EmailRecord>
content(id)GET /v1/emails/:id/contentEmailContent
cancel(id)DELETE /v1/emails/:id{ id, status: 'canceled' }
reschedule(id, scheduledAt)PATCH /v1/emails/:idEmailRecord

opts for send is { idempotencyKey?: string }. For sendBatch it is { idempotencyKey?: string; dryRun?: boolean }.

list takes { limit, cursor, status, to, from, q, tag, since, until }. to matches any of to, cc and bcc. q searches the subject. tag is key:value.

const page = await flaresend.emails.list({ status: 'bounced', since: '2026-09-01T00:00:00Z', limit: 50 });

events

MethodEndpointReturns
list(query?)GET /v1/eventsListResponse<EventRecord>

query is { limit, cursor, type, emailId, since }.

domains

MethodEndpointReturns
list()GET /v1/domainsDomainRecord[]

apiKeys

MethodEndpointReturns
list()GET /v1/api-keysApiKeyRecord[]

webhooks

MethodEndpointReturns
list()GET /v1/webhooksWebhookRecord[]
create(input)POST /v1/webhooksWebhookRecord including secret
get(id)GET /v1/webhooks/:idWebhookRecord
update(id, input)PATCH /v1/webhooks/:idWebhookRecord
remove(id)DELETE /v1/webhooks/:id{ id, deleted: true }
test(id)POST /v1/webhooks/:id/test{ deliveryId }
rotateSecret(id)POST /v1/webhooks/:id/rotate-secretWebhookRecord including the new secret
deliveries(id, query?)GET /v1/webhooks/:id/deliveriesListResponse<WebhookDeliveryRecord>

templates

MethodEndpointReturns
list()GET /v1/templatesTemplateRecord[], stored and built-in
get(name)GET /v1/templates/:nameTemplateRecord
create(input)POST /v1/templatesTemplateRecord
update(name, input)PATCH /v1/templates/:nameTemplateRecord
remove(name)DELETE /v1/templates/:name{ name, deleted: true }
versions(name)GET /v1/templates/:name/versionsTemplateVersionRecord[]
restore(name, version)POST /v1/templates/:name/restoreTemplateRecord
render(name, data?)POST /v1/templates/:name/render{ subject, html, text }

analytics

MethodEndpointReturns
get(query?)GET /v1/analyticsAnalyticsResult

query is { range: '7d' | '30d' | '90d', interval: 'day' | 'hour' }, defaults 7d and day.

contacts

MethodEndpointReturns
list(query?)GET /v1/contactsListResponse<ContactRecord>. query is { limit, cursor, q }
create(input)POST /v1/contactsContactRecord. Upserts by email
get(id)GET /v1/contacts/:idContactRecord
update(id, input)PATCH /v1/contacts/:idContactRecord
remove(id)DELETE /v1/contacts/:id{ id, deleted: true }
import(contacts)POST /v1/contacts/import{ created, updated }. Up to 5,000

audiences

MethodEndpointReturns
list()GET /v1/audiencesAudienceRecord[]
create({ name })POST /v1/audiencesAudienceRecord
get(id)GET /v1/audiences/:idAudienceRecord
update(id, { name })PATCH /v1/audiences/:idAudienceRecord
remove(id)DELETE /v1/audiences/:id{ id, deleted: true }
contacts(id, query?)GET /v1/audiences/:id/contactsListResponse<ContactRecord>
addContacts(id, contactIds)POST /v1/audiences/:id/contacts{ added }
removeContacts(id, contactIds)DELETE /v1/audiences/:id/contacts{ removed }

broadcasts

MethodEndpointReturns
list()GET /v1/broadcastsBroadcastRecord[]
create(input)POST /v1/broadcastsBroadcastRecord, a draft
get(id)GET /v1/broadcasts/:idBroadcastRecord with counts
update(id, input)PATCH /v1/broadcasts/:idBroadcastRecord
remove(id)DELETE /v1/broadcasts/:id{ id, deleted: true }
send(id, { scheduledAt? })POST /v1/broadcasts/:id/sendBroadcastRecord
cancel(id)POST /v1/broadcasts/:id/cancelBroadcastRecord

Helpers

attachmentFromBytes(filename, bytes, type?)

Builds an attachment from an ArrayBuffer or Uint8Array. The API needs attachment content as base64; this does the encoding without Buffer, so it works in Workers and browsers too.

import { attachmentFromBytes } from '@flaresend/client';

const pdf = new Uint8Array(await file.arrayBuffer());
await flaresend.emails.send({
  from: 'Acme <billing@acme.com>',
  to: 'ada@example.com',
  subject: 'Your invoice',
  text: 'Your invoice is attached.',
  attachments: [attachmentFromBytes('invoice.pdf', pdf, 'application/pdf')],
});

bytesToBase64(bytes) is exported too, if you only need the encoding.

Typed templates

Pass a map of template name to data type as the type argument of send, and data is checked against the template you name:

type Templates = {
  welcome: { name: string; appName: string; loginUrl: string };
  'magic-link': { loginUrl: string; expiresInMinutes?: number };
};

await flaresend.emails.send<Templates>({
  from: 'Acme <hello@acme.com>',
  to: 'ada@example.com',
  template: 'welcome',
  data: { name: 'Ada', appName: 'Acme', loginUrl: 'https://acme.com/login' },
});

TypedSend<Templates> is the input type on its own. See Typed templates.

Exported types

The package re-exports the request and response types from @flaresend/types, including SendEmailInput, SendEmailResult, EmailRecord, EmailContent, EventRecord, ListResponse, BatchResult, BatchDryRunResult, WebhookRecord, WebhookPayload, TemplateRecord, ContactRecord, AudienceRecord, BroadcastRecord and AnalyticsResult.

On this page