npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openemail/sdk

v0.0.8

Published

The official TypeScript SDK for the OpenEmail API. Send email and broadcasts, work with threads, drafts, labels, templates, rules, webhooks, tracking, contacts, audiences, sign-up forms, domains, suppressions, files, API keys, roles, members, mailbox impo

Readme

Intro to the Npm Package

The official TypeScript client for the OpenEmail API. A method for every one of the 385 documented operations, 485 in all once the paging and upload helpers are counted, typed end to end, with zero dependencies. It runs on Node 20+, Bun, Deno and Cloudflare Workers, and ships as ESM and CommonJS. Python, Ruby and PHP have clients of their own with the same methods: pip install openemail, gem install openemail and composer require openemail/sdk.

It carries a workspace API key or an OAuth access token, so it belongs on a server or in a tool that runs on your own machine. The one exception is disposable inboxes, which need no credential and work in a browser.

Installing

npm i @openemail/sdk@latest

Using

import { OpenEmail } from '@openemail/sdk'

const openemail = new OpenEmail('oe_live_...')

const sent = await openemail.emails.send({
    from: 'Acme Billing <[email protected]>',
    to: '[email protected]',
    subject: 'Your September invoice',
    html: '<p>Your invoice is attached.</p>',
    attachments: [{ filename: 'invoice.pdf', content: pdfBytes, contentType: 'application/pdf' }]
})

console.log(sent.id, sent.status)

Create a key in OpenEmail under Settings, API keys. It is shown once, and it belongs in an environment variable rather than in code.

Build the client once, in a module of its own, and import it everywhere else:

import { OpenEmail } from '@openemail/sdk'

export const openemail = new OpenEmail(process.env.OPENEMAIL_API_KEY!)

Or skip even that. The package ships a ready made openemail that reads OPENEMAIL_API_KEY the first time it is touched:

import { openemail } from '@openemail/sdk'

await openemail.emails.send({ from, to, subject, text })

Every send carries an idempotency key, generated once per call and reused by its retries, so a retried request replays the original message rather than sending a second one. Pass your own with { idempotencyKey } to make that hold across processes and restarts.

Reading the mailbox

const page = await openemail.threads.list({ folder: 'inbox', limit: 25 })

for await (const thread of openemail.threads.iterate({ folder: 'inbox', query: 'invoice' })) {
    const full = await openemail.threads.get(thread.id)

    console.log(full.messageCount, full.hasUnread)
}

Every paginated resource has list for one page, listAll for every page at once and iterate to stream items and stop whenever you like. listAll resolves to one array, apart from addresses.listAll, which resolves to the whole address book.

Errors

import { OpenEmailApiError, openemail } from '@openemail/sdk'

try {
    await openemail.templates.send('order-shipped', {
        from: '[email protected]',
        to: '[email protected]',
        props: { orderId: 'AC-4192' }
    })
}

catch (error) {
    if (error instanceof OpenEmailApiError && error.isValidation) {
        console.error(error.code, error.param, error.requestId)
    }

    throw error
}

An API refusal is one class, OpenEmailApiError, with status, type, code, param and requestId, plus isValidation, isNotFound, isRateLimited and friends to branch on. No response at all is OpenEmailNetworkError, with isTimeout when the deadline passed.

Webhooks

import { verifyWebhookSignature } from '@openemail/sdk'

export default async (request: Request) => {
    const event = await verifyWebhookSignature({
        payload: await request.text(),
        headers: request.headers,
        secret: process.env.OPENEMAIL_WEBHOOK_SECRET!
    })

    console.log(event.type, event.data)

    return new Response(null, { status: 204 })
}

It checks the HMAC in constant time and rejects a delivery more than five minutes old, then returns the parsed event. Pass the raw body: re-serialising it changes the bytes and the signature will not match.

Disposable inboxes

import { createTempMail } from '@openemail/sdk'

const temp = createTempMail()

const inbox = await temp.create({ ttlMinutes: 60 })

const { items } = await temp.listMessages(inbox.id, { inboxToken: inbox.token })

create needs no credential and is the only call that returns the inbox token, so keep it.

OAuth access tokens

An app a person connected to OpenEmail with OAuth, such as a command line tool or an agent, holds an access token rather than an API key. Pass it as accessToken:

import { OpenEmail } from '@openemail/sdk'

export const openemail = new OpenEmail({
    accessToken: async () => session.freshAccessToken()
})

accessToken takes the token itself, or a function that returns it. The function runs before every request, so renew the token there when it is close to expiring and the client never has to be rebuilt. Pass apiKey or accessToken, not both. createOpenEmail() reads OPENEMAIL_ACCESS_TOKEN when you pass neither and OPENEMAIL_API_KEY is not set. me.get() answers object: 'oauth_token' for a token, with the connected app's clientId and expiresAt, when the person's approval of the app runs out.

A token acts for a person, so before a sensitive change, such as deleting a domain or changing a webhook, it is asked for the same verification code the web app asks for. The request fails with isStepUpRequired. Ask for a code, check it, then replay the request:

import { OpenEmailApiError } from '@openemail/sdk'

try {
    await openemail.domains.delete(domain.id)
}

catch (error) {
    if (!(error instanceof OpenEmailApiError) || !error.isStepUpRequired) throw error

    const challenge = await openemail.security.beginStepUp()

    const code = await ask(challenge.method === 'email'
        ? `Enter the code we emailed to ${challenge.sentTo}`
        : 'Enter the code from your authenticator app, or a backup code')

    await openemail.security.verifyStepUp({ code })
    await openemail.domains.delete(domain.id)
}

An emailed code works for 10 minutes, and beginStepUp({ resend: true }) sends a fresh one. The email shows the name the app registered with, its client ID and the change it asked to make, so the person can check who is asking before they hand the code over. Once a code is verified the app is not asked again for 60 minutes, for any of the 40 changes that ask for one, through the REST API or through the MCP tools that make the same changes. verifyStepUp lists them. security.stepUpStatus() says whether it is verified right now. An app that cannot ask for a code, such as an MCP connector, can instead be allowed by the person for 60 minutes with Allow changes for 60 minutes in Account settings, Connected apps, on the website. API keys are never asked for a code.

Each app has its own budget of codes, so another app never uses it up, and neither does the web app, which keeps its own 10 an hour. An app can ask for 5 codes an hour and 20 in 24 hours, then gets 429 step_up_throttled. A code allows 5 tries, and after the fifth wrong one a plain beginStepUp() starts a fresh challenge. Ten wrong codes in 24 hours pause verification for the app, and 20 in 24 hours from all of a person's apps together pause it for every app they connected. Either way both calls answer 429 step_up_locked, with the time the pause ends in the message. Codes entered in the web app count toward neither pause, and the person can still verify there.

Configuring

Pass an options object instead of the bare key when the defaults are not right:

import { OpenEmail } from '@openemail/sdk'

export const openemail = new OpenEmail({
    apiKey: process.env.OPENEMAIL_API_KEY!,
    baseUrl: 'https://api.openemail.uk',
    timeoutMs: 30_000,
    maxRetries: 2,
    fetch: myFetch,
    headers: { 'X-Team': 'billing' }
})

createOpenEmail(options) is the same constructor with one difference: anything you leave out is read from the environment. The shipped openemail takes the same options through init(options), called once at startup.

baseUrl also comes from OPENEMAIL_BASE_URL. Use an https: origin: the client refuses to send an API key, an access token or an inbox token over plain http:, and throws before the request leaves, unless the server is on this machine at localhost, a 127.x.x.x address or ::1. A baseUrl on 0.0.0.0 throws when the client is built, since that is the address a server listens on: use 127.0.0.1 with the same port. Reads are retried on 408 and 5xx with backoff. A 429 is retried only when it carries a Retry-After, and any wait longer than a minute throws instead of sleeping. Writes that cannot safely repeat are not retried. Every method outside tempMail takes { signal, apiKey } as its last argument, so one process can serve several workspaces with one client. The tempMail methods take { signal, inboxToken } instead.

An endpoint no method wraps yet is one raw.request() away, with the client's credential, base URL, timeout and retry policy applied:

const result = await openemail.raw.request('/something-new', {
    method: 'POST',
    body: { name: 'Invoices' }
})

The path must begin with a single /. Anything else, such as //host/x or @host/x, throws before a request is sent, and so does a path whose finished URL leaves the base URL's origin, so the credential it carries never reaches another host.

When a newer version is on npm the client says so once on a TTY. OPENEMAIL_DISABLE_UPDATE_NOTICE=1 or { disableUpdateNotice: true } turns that off.

Everything else, templates, rules, tracking, calendar, contacts, audiences, broadcasts (openemail.broadcasts, which sends one personalised message to everybody in an audience), roles, members, settings, mailbox imports (openemail.imports, which brings a Google Takeout archive or an mbox across) and provider imports (openemail.providerImports, which moves a Resend account over), plus the full reference for every method, lives in the documentation. What changed in each release is in the changelog.