Node.js / Bun
Written in TypeScript, runs on Node 18+ and Bun 1.0+. Ships with full type definitions.
Install
npm install sevk
pnpm add sevk
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: [{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.
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
await sevk.emails.update(id, {scheduledAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString()})
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: []})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'})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}}