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

@sentroy-co/sdk

v1.0.18

Published

TypeScript SDK for Sentroy Mail Server API

Readme

@sentroy-co/sdk

TypeScript SDK for the Sentroy Mail Server API. Zero dependencies, native fetch, full type safety.

Install

npm install @sentroy-co/sdk

Quick Start

import { SentroyClient } from '@sentroy-co/sdk';

const sentroy = new SentroyClient({
  baseUrl: 'https://mail-api.example.com',
  apiKey: 'sk_...',
});

Domains

// Add a domain
const { data: domain } = await sentroy.domains.create({ domain: 'example.com' });

// Get DNS records to configure
const { data: dns } = await sentroy.domains.getDnsRecords(domain.id);
// Returns SPF, DKIM, DMARC, MX, A records ready to copy-paste

// Verify DNS configuration
const { data: result } = await sentroy.domains.verify(domain.id);
// result.verification.spf / .dkim / .dmarc → true/false

// List all domains
const { data: domains, meta } = await sentroy.domains.list({ page: 1, limit: 20 });

// Delete
await sentroy.domains.delete(domain.id);

Templates

// Create an MJML template
const { data: template } = await sentroy.templates.create({
  name: 'Welcome Email',
  subject: 'Welcome {{name}}!',
  mjmlBody: `<mjml>
    <mj-body>
      <mj-section>
        <mj-column>
          <mj-text>Hello {{name}}, welcome to {{company}}!</mj-text>
        </mj-column>
      </mj-section>
    </mj-body>
  </mjml>`,
  domainId: domain.id,
});
// template.variables → ["name", "company"]
// template.htmlBody → compiled HTML (cached)

// Preview with variables filled in
const { data: preview } = await sentroy.templates.preview(template.id, {
  name: 'John',
  company: 'Acme',
});
// preview.html → rendered HTML
// preview.subject → "Welcome John!"

// Update
await sentroy.templates.update(template.id, {
  subject: 'Hey {{name}}, welcome aboard!',
});

// List (filter by domain)
const { data: templates } = await sentroy.templates.list({ domainId: domain.id });

Sending Emails

// Send a single email
const { data: result } = await sentroy.send.single({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Hello!',
  templateId: template.id,
  variables: { name: 'John', company: 'Acme' },
  domainId: domain.id,
});
// result.jobId, result.mailLogId, result.status → "queued"

// Send with raw HTML
await sentroy.send.single({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Quick update',
  html: '<h1>Hello!</h1><p>This is a quick update.</p>',
  domainId: domain.id,
});

// Send with attachments
await sentroy.send.single({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Your invoice',
  html: '<p>Please find your invoice attached.</p>',
  domainId: domain.id,
  attachments: [
    {
      filename: 'invoice.pdf',
      content: base64EncodedPdf, // base64 string
      contentType: 'application/pdf',
    },
  ],
});

// Reply to a message (threading)
await sentroy.send.single({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Re: Project update',
  html: '<p>Thanks for the update!</p>',
  domainId: domain.id,
  inReplyTo: '<[email protected]>',
  references: [
    '<[email protected]>',
    '<[email protected]>',
  ],
});
// Sets RFC 5322 In-Reply-To and References headers
// All mail clients (Gmail, Outlook, Apple Mail) will display this as a thread

// Schedule for later
await sentroy.send.single({
  to: '[email protected]',
  from: '[email protected]',
  subject: 'Scheduled email',
  html: '<p>This was scheduled!</p>',
  domainId: domain.id,
  scheduledAt: '2025-01-15T09:00:00Z',
});

// Batch send (max 500 recipients)
const { data: batch } = await sentroy.send.batch({
  recipients: [
    { to: '[email protected]', variables: { name: 'Alice' } },
    { to: '[email protected]', variables: { name: 'Bob' } },
  ],
  from: '[email protected]',
  subject: 'Hello {{name}}',
  templateId: template.id,
  domainId: domain.id,
});
// batch.totalQueued → 2, batch.totalSuppressed → 0

// Check job status
const { data: status } = await sentroy.send.getJobStatus(result.jobId);
// status.state → "completed" | "failed" | "delayed" | ...

// Cancel a scheduled send
await sentroy.send.cancel(result.jobId);

Inbox (IMAP)

Inbox methods accept two optional scope parameters:

  • mailbox — The email account address (e.g. [email protected]). Identifies which mailbox account to operate on.
  • folder — The IMAP folder name (e.g. INBOX, Sent, Trash, Drafts). Defaults to INBOX when omitted.
// List messages from INBOX
const { data: messages, meta } = await sentroy.inbox.list({
  mailbox: '[email protected]',
  page: 1,
  limit: 20,
  unread: true,
});
// Each message includes threading fields:
// messages[0].messageId → "<[email protected]>"
// messages[0].inReplyTo → "<parent-id@...>" (null if not a reply)

// List messages from Sent folder
const { data: sent } = await sentroy.inbox.list({
  mailbox: '[email protected]',
  folder: 'Sent',
});

// Read a message (full body, headers, attachments, threading info)
const { data: message } = await sentroy.inbox.get(messages[0].uid);
// message.textBody, message.htmlBody, message.attachments, message.headers
// message.messageId → "<[email protected]>" (for threading)
// message.inReplyTo → "<[email protected]>"
// message.references → ["<root-id@...>", "<parent-id@...>"]

// Mark as read / unread
await sentroy.inbox.markAsRead(123);
await sentroy.inbox.markAsUnread(123);

// Move to folder
await sentroy.inbox.move(123, 'Trash');

// Delete
await sentroy.inbox.delete(123);

// Search within a specific folder
const { data: results } = await sentroy.inbox.search({
  mailbox: '[email protected]',
  folder: 'INBOX',
  from: '[email protected]',
  subject: 'invoice',
  since: '2025-01-01',
});

// List IMAP folders for the account
const { data: mailboxes } = await sentroy.inbox.listMailboxes();
// [{ name: "INBOX", path: "INBOX", totalMessages: 42, unreadMessages: 5 }, ...]

// Toggle flagged / starred
await sentroy.inbox.toggleFlag(123, '[email protected]');

// Get full thread (cross-folder: INBOX + Sent merged)
const { data: thread } = await sentroy.inbox.getThread(
  'Project update',           // subject (Re:/Fwd: stripped automatically)
  '[email protected]',
);
// Returns all messages in the conversation, chronologically sorted
// Each message includes a `folder` field ("INBOX" or "Sent")
// thread[0].from, thread[0].htmlBody, thread[0].folder → "INBOX"
// thread[1].from, thread[1].htmlBody, thread[1].folder → "Sent"

// Download attachment
const buffer = await sentroy.inbox.downloadAttachment(123, '1.2');

Mail Logs

// List logs (filter by status, domain, sender, recipient)
const { data: logs, meta } = await sentroy.logs.list({
  status: 'bounced',
  domainId: domain.id,
  from: '[email protected]',
  to: '[email protected]',
  page: 1,
  limit: 50,
});
// logs[0].status → "bounced"
// logs[0].messageId → "<[email protected]>"
// logs[0].sentAt, .bouncedAt, .openedAt, .clickedAt

// Get a single log entry
const { data: log } = await sentroy.logs.get(logs[0].id);

Webhooks

// Create a webhook
const { data: webhook } = await sentroy.webhooks.create({
  url: 'https://myapp.com/webhooks/mail',
  events: ['sent', 'bounced', 'opened', 'clicked', 'unsubscribed'],
  domainId: domain.id,
});
// webhook.secret → HMAC signing secret (shown only once)

// List webhooks
const { data: webhooks } = await sentroy.webhooks.list(domain.id);

// Update
await sentroy.webhooks.update(webhook.id, { active: false });

// Delete
await sentroy.webhooks.delete(webhook.id);

Webhook payload sent to your endpoint:

{
  "event": "bounced",
  "timestamp": "2025-01-15T09:00:00.000Z",
  "data": {
    "mailLogId": "...",
    "to": "[email protected]",
    "from": "[email protected]",
    "subject": "Hello!",
    "domainId": "...",
    "messageId": "<[email protected]>"
  }
}

Verify with the X-Sentroy-Signature header (HMAC-SHA256 of the raw body using your webhook secret).

Suppression List

Emails that bounced or unsubscribed are automatically added. Suppressed emails are blocked at send time.

// Check if an email is suppressed
const { data } = await sentroy.suppressions.check('[email protected]', domainId);
// data.suppressed → true/false, data.reason → "bounce" | "unsubscribe" | ...

// List suppressions
const { data: list } = await sentroy.suppressions.list({
  domainId: domain.id,
  reason: 'bounce',
});

// Manually add
await sentroy.suppressions.add({
  email: '[email protected]',
  reason: 'manual',
  domainId: domain.id,
});

// Remove (re-enable sending)
await sentroy.suppressions.remove(suppressionId);

Statistics

// Overview (optionally filter by domain and date range)
const { data: stats } = await sentroy.statistics.overview({
  domainId: domain.id,
  from: '2025-01-01',
  to: '2025-01-31',
});
// stats.rates.delivery → 98.5
// stats.rates.bounce → 1.2
// stats.rates.open → 45.3
// stats.rates.click → 12.1

// Daily breakdown
const { data: daily } = await sentroy.statistics.daily({
  domainId: domain.id,
  days: 30,
});
// [{ date: "2025-01-15", sent: 150, bounced: 2, opened: 68, clicked: 15 }, ...]

// Per-domain summary
const { data: domains } = await sentroy.statistics.domains();

Email Validation

// Validate a single email
const { data: result } = await sentroy.validate.email('[email protected]');
// result.valid → false
// result.checks.syntax → true
// result.checks.mxExists → false
// result.checks.disposable → false
// result.suggestion → "[email protected]"

// Batch validate (max 100)
const { data: results, meta } = await sentroy.validate.batch([
  '[email protected]',
  '[email protected]',
  '[email protected]',
]);
// meta.valid → 1, meta.invalid → 2

API Keys

// Create (plain key is shown only once)
const { data: key } = await sentroy.apiKeys.create({
  name: 'Production',
  scopes: ['send', 'read'],
  domainId: domain.id, // optional — null = all domains
});
// key.key → "sk_..." (save this!)

// List
const { data: keys } = await sentroy.apiKeys.list();

// Revoke
await sentroy.apiKeys.revoke(key.id);

Mailbox Users

// Create a mailbox (Dovecot user)
await sentroy.mailboxes.create({
  email: '[email protected]',
  password: 'secure-password',
  domainId: domain.id,
});

// List
const { data: users } = await sentroy.mailboxes.list(domain.id);

// Update password
await sentroy.mailboxes.updatePassword('[email protected]', 'new-password');

// Delete
await sentroy.mailboxes.delete('[email protected]');

Health

const { data: health } = await sentroy.health.check();
// health.status → "healthy" | "degraded" | "unhealthy"
// health.services → { postgres: "ok", redis: "ok", postfix: "ok", ... }

const { data: queue } = await sentroy.health.queue();
// queue.counts → { waiting: 0, active: 2, completed: 1500, failed: 3, delayed: 10 }

Error Handling

import { SentroyClient, SentroyHttpError } from '@sentroy-co/sdk';

try {
  await sentroy.send.single({ /* ... */ });
} catch (err) {
  if (err instanceof SentroyHttpError) {
    console.error(err.statusCode); // 400, 401, 403, 422, 429...
    console.error(err.body.error);  // "Email is in the suppression list"
    console.error(err.body.details); // [{ field: "to", message: "Invalid email" }]
  }
}

Configuration

const sentroy = new SentroyClient({
  baseUrl: 'https://mail-api.example.com', // API server URL
  apiKey: 'sk_...',                         // Bearer token
  timeout: 30000,                           // Request timeout in ms (default: 30s)
});

License

MIT