Node.js / Bun

Written in TypeScript, runs on Node 18+ and Bun 1.0+. Ships with full type definitions.

Install

npm
npm install sevk
pnpm
pnpm add sevk
bun
bun add sevk

Initialize

Pass your API key to the constructor. The key is required; the SDK never reads it from the environment.

import { Sevk } from 'sevk'
const sevk = new Sevk('sevk_full_xxxxxxxx')

Send Email

HTML body

import { Sevk } from 'sevk'
const sevk = new Sevk('sevk_full_xxxxxxxx')
const { id } = await sevk.emails.send({
subject: 'Welcome',
html: '<h1>Welcome</h1><p>Thanks for signing up.</p>'
})
console.log(id)

sevk Markup body

Pass markup instead of html and the SDK renders it into email-safe HTML locally before sending, with no API round-trip.

await sevk.emails.send({
subject: 'Welcome',
markup: `
<section padding="32px">
<heading level="1">Welcome</heading>
<paragraph>Thanks for signing up.</paragraph>
<button href="https://app.example.com">Open the app</button>
</section>
`
})

Attachments

Up to 10 attachments per email, with a combined decoded size of 10 MiB (10,485,760 bytes). Pass content as base64. The final MIME message has a separate 40 MiB limit, checked by the worker before sending; an accepted email can still become FAILED if it exceeds that limit.

import { readFileSync } from 'node:fs'
await sevk.emails.send({
subject: 'Your receipt',
html: '<p>Thanks for your purchase.</p>',
attachments: [
{
filename: 'receipt.pdf',
content: readFileSync('./receipt.pdf').toString('base64'),
contentType: 'application/pdf'
}
]
})

Bulk

Send up to 100 emails in one request. Schema validation applies to the entire batch; only per-entry processing failures are returned in errors. Attachments have the same per-email limits as a single send. Bulk sends cannot be scheduled. See bulk validation and delivery.

const result = await sevk.emails.sendBulk({
emails: [
{ from: '[email protected]', to: '[email protected]', subject: 'Hi', text: 'Hi A' },
{
subject: 'Your invoice',
html: '<p>See attached.</p>',
attachments: [
{ filename: 'invoice.pdf', content: '<base64>', contentType: 'application/pdf' }
]
}
]
})
console.log(result.success, result.failed, result.ids)

Scheduled Email

Set scheduledAt to an RFC 3339 timestamp with a timezone, in the future and no more than 30 days ahead. Scheduled emails cannot include attachments. Quota and balance are reserved when accepted, not when delivery starts. The returned id identifies the email record, not a completed delivery.

Schedule
const scheduledAt = new Date(Date.now() + 60 * 60 * 1000).toISOString()
const { id } = await sevk.emails.send({
subject: 'Appointment reminder',
text: 'Your appointment is tomorrow.',
scheduledAt
}, { idempotencyKey: 'appointment-reminder/apt_123' })
if (!id) throw new Error('Expected a single email id')
const email = await sevk.emails.get(id)
console.log(email.status, email.scheduledAt)

Persist the chosen timestamp, payload, and idempotency key with the appointment. A retry must reuse all three; do not recalculate scheduledAt on each attempt.

Reschedule or cancel

Reschedule
await sevk.emails.update(id, {
scheduledAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString()
})
Cancel
await sevk.emails.cancel(id)

Rescheduling and cancellation are allowed only before provider delivery starts. Cancellation releases the reserved quota and balance; repeating it after CANCELLED succeeds. A 409 means the current state no longer allows the operation. See reschedule and cancel.

Contacts

const contact = await sevk.contacts.create({
data: { firstName: 'John', plan: 'pro' }
})
const one = await sevk.contacts.get(contact.id)
await sevk.contacts.update(contact.id, {
data: { plan: 'enterprise' }
})
const page = await sevk.contacts.list({ limit: 50 })
for (const c of page.items) console.log(c.email)
await sevk.contacts.delete(contact.id)

Bulk import contacts

const result = await sevk.contacts.bulkImport({
audienceId: '9e7a3b5c-1234-4567-89ab-cdef01234567',
contacts: [
{ email: '[email protected]', subscribed: true, data: { firstName: 'A' } },
{ email: '[email protected]', subscribed: false, data: { plan: 'free' } }
]
})
console.log(result.created, result.errors)

Audiences

const audience = await sevk.audiences.create({
name: 'Newsletter',
description: 'Main newsletter subscribers'
})
await sevk.audiences.addContacts(audience.id, {
contactIds: ['b2c3d4e5-f678-4901-abcd-ef1234567890', 'b2c3d4e5-f678-4901-abcd-ef1234567891']
})
const contacts = await sevk.audiences.listContacts(audience.id, { limit: 50 })
for (const c of contacts.items) console.log(c.email)

Broadcasts

const broadcast = await sevk.broadcasts.create({
name: 'December Newsletter',
subject: 'December Newsletter',
body: '<section><heading level="1">Updates</heading></section>',
style: 'MARKUP',
senderName: 'sevk Team',
domainId: '6f2c8e1a-9b4d-4a7e-8f3c-1a2b3c4d5e6f',
targetType: 'AUDIENCE',
audienceId: '9e7a3b5c-1234-4567-89ab-cdef01234567'
})
await sevk.broadcasts.test(broadcast.id, { emails: ['[email protected]'] })
const estimate = await sevk.broadcasts.estimateCost(broadcast.id)
console.log(estimate.contactCount, estimate.estimatedCost)
await sevk.broadcasts.send(broadcast.id)
const analytics = await sevk.broadcasts.analytics(broadcast.id)
console.log(analytics.sentEmails, analytics.openedEmails, analytics.clickedEmails)

Render Markup

The render export turns sevk Markup into email HTML locally, with no API round-trip. Useful when you want to store the rendered HTML or preview it in another tool.

import { render } from 'sevk'
const html = render(`
<section padding="32px" background-color="#f8f9fa">
<heading level="1">Hello</heading>
<paragraph>Rendered locally.</paragraph>
</section>
`)

Error Handling

Every API failure throws a subclass of SevkError, which carries statusCode, code, fields, details, and responseBody. JSON bodies are decoded; non-JSON bodies remain strings. RateLimitError also has retryAfter in seconds. The full set: BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, ValidationError, RateLimitError, InternalServerError.

import { Sevk, SevkError, ValidationError, RateLimitError } from 'sevk'
const sevk = new Sevk('sevk_xxxxxxxxx')
try {
await sevk.emails.send({
to: 'not-an-email',
subject: 'Test',
text: 'Hello'
})
} catch (err) {
if (err instanceof ValidationError) {
console.error(err.fields) // { to: ['Invalid email address'] }
} else if (err instanceof RateLimitError) {
console.error('Retry after', err.retryAfter, 'seconds')
} else if (err instanceof SevkError) {
console.error(err.statusCode, err.code, err.message)
} else {
throw err
}
}