Reference / Mail

Mail

Manage verified domains, IMAP mailboxes, MJML templates, and send transactional or bulk email — all through the same client.

Domains#

Verified sending domains for your company. Domain creation and DNS verification happen in the dashboard; the API surfaces a read-only view for use at send time.

List domains#

GET/api/mail/companies/{slug}/domains
const domains = await sentroy.domains.list()
// → Domain[]

Get domain#

GET/api/mail/companies/{slug}/domains/{id}
const domain = await sentroy.domains.get("domain-id")

Mailboxes#

IMAP mailbox accounts created against your verified domains. Used by the Inbox API to read messages and by Send to authenticate the from-address.

List mailboxes#

GET/api/mail/companies/{slug}/mailboxes
const mailboxes = await sentroy.mailboxes.list()

Templates#

Reusable MJML email templates with multilingual fields and variable placeholders.

List templates#

GET/api/mail/companies/{slug}/templates
const templates = await sentroy.templates.list()

Get template#

GET/api/mail/companies/{slug}/templates/{id}
const template = await sentroy.templates.get("template-id")

LocalizedString shape

Templates support multiple languages. A field can be a plain string (single-language) or an object keyed by language code:

{
  "id": "b3f1a2c4-...",
  "name": { "en": "Welcome Email", "tr": "Hosgeldin E-postasi" },
  "subject": { "en": "Welcome, {{name}}!", "tr": "Hosgeldin, {{name}}!" },
  "mjmlBody": { "en": "<mjml>...</mjml>", "tr": "<mjml>...</mjml>" },
  "variables": ["name", "company"],
  "domainId": "a1b2c3d4-...",
  "domainName": "example.com",
  "createdAt": "2026-01-15T10:30:00.000Z",
  "updatedAt": "2026-04-10T14:22:00.000Z"
}

Use the variables array to know which placeholders the template expects.

Create template#

Requires the templates.manage permission. name, subject and mjmlBody accept a plain string or a { tr, en } map; domainId ties the template to a verified sending domain. You do not send a variables array — the platform extracts it from the body and returns it on the created template.

POST/api/mail/companies/{slug}/templates
const template = await sentroy.templates.create({
  name: { en: "Welcome", tr: "Hos geldin" },
  subject: { en: "Welcome, {firstName}!", tr: "Hos geldin, {firstName}!" },
  mjmlBody: { en: "<mjml>...</mjml>", tr: "<mjml>...</mjml>" },
  domainId: "a1b2c3d4-...",
})

// template.variables -> ["firstName"]  (extracted from the body)

Update template#

Partial update — send only the fields you want to change. At least one of name, subject or mjmlBody is required. Editing the body re-extracts the variables list.

PATCH/api/mail/companies/{slug}/templates/{id}
const template = await sentroy.templates.update("template-id", {
  subject: { en: "Welcome aboard, {firstName}!" },
})

Delete template#

DELETE/api/mail/companies/{slug}/templates/{id}
await sentroy.templates.delete("template-id")

Create from the CLI#

The sentroy CLI creates templates from a file or piped stdin — handy in a CI job or build step. Localized fields accept a JSON object string. See the CLI reference for every flag.

# body from a file
sentroy mail templates create \
  --name=Welcome \
  --subject="Welcome, {firstName}!" \
  --domain=dom_123 \
  --mjml-file=welcome.mjml

# body piped on stdin, localized name + subject as JSON
cat welcome.mjml | sentroy mail templates create \
  --name='{"en":"Welcome","tr":"Hos geldin"}' \
  --subject='{"en":"Hi {firstName}","tr":"Merhaba {firstName}"}' \
  --domain=dom_123

sentroy mail templates update tpl_123 --subject="New subject"
sentroy mail templates delete tpl_123

Template variables#

Variables are placeholders you write directly into the subject and MJML body. Sentroy parses them automatically — there is no separate “declare variables” step — and returns the discovered names on the template’s variables field. At send time you supply the values in the variables object.

Three forms are supported (Mustache-like syntax):

NameTypeDescription
{name} / {{name}}scalarA single value, replaced with the matching key from the send-time variables object. Single and double braces both work.
{#items} … {/items}array sectionRepeats the enclosed block once per array element. Inside the block each item’s fields are in scope (e.g. {title}, {price}).
{^name} … {/^name}inverted sectionRenders the block only when name is missing, empty or false — the opposite of a truthy guard.

Variable names may contain letters, digits and underscores (\w+) — no dashes or dots — and are case-sensitive. There is no default-value syntax; an unmatched placeholder is left in the output verbatim so you can spot it. Nested sections are not supported.

<mjml>
  <mj-body>
    <mj-section><mj-column>
      <mj-text>Hi {firstName},</mj-text>

      {^hasItems}
        <mj-text>Your cart is empty.</mj-text>
      {/^hasItems}

      {#items}
        <mj-text>{title} — {price}</mj-text>
      {/items}
    </mj-column></mj-section>
  </mj-body>
</mjml>

Send it by passing scalars and arrays in variables:

await sentroy.send.email({
  to: "ada@example.com",
  from: "hello@example.com",
  domainId: "a1b2c3d4-...",
  templateId: "template-id",
  variables: {
    firstName: "Ada",
    hasItems: true,
    items: [
      { title: "Keyboard", price: "$80" },
      { title: "Mouse", price: "$25" },
    ],
  },
})

Inbox#

Read messages, list IMAP folders, group threads, and manage message state.

List messages#

GET/api/mail/companies/{slug}/inbox?mailbox=info@example.com&folder=INBOX&page=1&limit=20
const messages = await sentroy.inbox.list({
  mailbox: "info@example.com",
  folder: "INBOX",
  page: 1,
  limit: 20,
})

Get message#

GET/api/mail/companies/{slug}/inbox/{uid}?mailbox=info@example.com
const message = await sentroy.inbox.get(1234, {
  mailbox: "info@example.com",
})

List folders#

GET/api/mail/companies/{slug}/inbox/folders?mailbox=info@example.com
const folders = await sentroy.inbox.listFolders("info@example.com")

Get thread#

GET/api/mail/companies/{slug}/inbox/thread?subject=Re%3A%20Project%20update&mailbox=info@example.com
const thread = await sentroy.inbox.getThread(
  "Re: Project update",
  "info@example.com",
)

Mark as read / unread#

PATCH/api/mail/companies/{slug}/inbox/{uid}/read
await sentroy.inbox.markAsRead(1234, { mailbox: "info@example.com" })
await sentroy.inbox.markAsUnread(1234, { mailbox: "info@example.com" })

Move message#

POST/api/mail/companies/{slug}/inbox/{uid}/move
await sentroy.inbox.move(1234, "Trash", {
  from: "INBOX",
  mailbox: "info@example.com",
})

Delete message#

DELETE/api/mail/companies/{slug}/inbox/{uid}?mailbox=info@example.com
await sentroy.inbox.delete(1234, { mailbox: "info@example.com" })

Send email#

Single endpoint for transactional and bulk send. Pass either a templateId + variables, or raw html. Recipients can be a string or array of addresses.

Send with a template#

POST/api/mail/companies/{slug}/send
const result = await sentroy.send.email({
  to: "user@example.com",
  from: "info@example.com",
  subject: "Welcome!",
  domainId: "domain-id",
  templateId: "template-id",
  variables: {
    name: "John",
    company: "Acme",
  },
})

Send in a specific language#

For multilingual templates, pass langto pick which translation of subject and body to use. If omitted, the template's default language wins.

POST/api/mail/companies/{slug}/send
await sentroy.send.email({
  to: "user@example.com",
  from: "info@example.com",
  subject: "Hosgeldin!",
  domainId: "domain-id",
  templateId: "template-id",
  lang: "tr",
  variables: { name: "Ahmet" },
})

Send with raw HTML#

POST/api/mail/companies/{slug}/send
await sentroy.send.email({
  to: ["user1@example.com", "user2@example.com"],
  from: "info@example.com",
  subject: "Hello",
  domainId: "domain-id",
  html: "<h1>Hello World</h1>",
})

Send with attachments#

Attachments accept a base64 content string plus filename and MIME type.

POST/api/mail/companies/{slug}/send
await sentroy.send.email({
  to: "user@example.com",
  from: "info@example.com",
  subject: "Invoice",
  domainId: "domain-id",
  html: "<p>Please find your invoice attached.</p>",
  attachments: [
    {
      filename: "invoice.pdf",
      content: base64String,
      contentType: "application/pdf",
    },
  ],
})

Audience#

Manage contacts and audience lists. Build your own newsletter signup, sync customers from another system, or assemble segments for a campaign.

List contacts#

Paginated browsing with optional status and tag filters. Tags are comma-joined on the wire — pass them as an array to the SDK.

GET/api/mail/companies/{slug}/audience/contacts?page=1&limit=50&status=active&tags=customer,vip
const { contacts, total, page, limit } = await sentroy.audience.contacts.list({
  page: 1,
  limit: 50,
  status: "active",
  tags: ["customer", "vip"],
})

Create contact#

POST/api/mail/companies/{slug}/audience/contacts
const contact = await sentroy.audience.contacts.create({
  email: "user@example.com",
  name: "Jane Doe",
  tags: ["beta-tester"],
  metadata: { signupSource: "landing-2026-q2" },
})

Update contact#

Pass any subset of fields. Use status to mark unsubscribed/bounced.

PATCH/api/mail/companies/{slug}/audience/contacts/{id}
await sentroy.audience.contacts.update(contact.id, { tags: ["customer"] })

Delete contact#

Soft-delete — sets status: "unsubscribed". The record is preserved so historical mail-log foreign keys keep resolving and the email can't accidentally be re-added.

DELETE/api/mail/companies/{slug}/audience/contacts/{id}
await sentroy.audience.contacts.delete(contact.id)

Audience lists#

Lists are simple groupings; a single contact can belong to many. Use them as the target of a campaign or a form submission.

GET/api/mail/companies/{slug}/audience/lists
const lists = await sentroy.audience.lists.list()
POST/api/mail/companies/{slug}/audience/lists
const list = await sentroy.audience.lists.create({
  name: "Newsletter — May 2026",
  description: "Opt-ins from the homepage form",
})
DELETE/api/mail/companies/{slug}/audience/lists/{id}
await sentroy.audience.lists.delete(list.id)

List membership#

Membership operations are scoped via the SDK's members(listId) accessor — keeps the list id off every call.

POST/api/mail/companies/{slug}/audience/lists/{id}/members
const members = sentroy.audience.lists.members(list.id)
await members.add(contact.id)
GET/api/mail/companies/{slug}/audience/lists/{id}/members
const inList = await members.list()
DELETE/api/mail/companies/{slug}/audience/lists/{id}/members
await members.remove(contact.id)

Suppressions#

Suppressed addresses are skipped at send time. Bounces and complaints are added automatically by the mail server — the API is for honoring off-platform opt-outs or removing a stale entry.

List suppressions#

GET/api/mail/companies/{slug}/suppressions?page=1&limit=50&domainId=domain-id&reason=complaint
const suppressions = await sentroy.suppressions.list({
  domainId: "domain-id",
  reason: "complaint",
  page: 1,
  limit: 50,
})

Add suppression#

POST/api/mail/companies/{slug}/suppressions
const added = await sentroy.suppressions.add({
  email: "leaving@example.com",
  domainId: "domain-id",
  reason: "manual",
})

Remove suppression#

Removing a suppression makes the address eligible to receive mail again.

DELETE/api/mail/companies/{slug}/suppressions/{id}
await sentroy.suppressions.remove(added.id)

Webhooks#

Subscribe to delivery events on a per-domain basis. Each delivery is signed with the secret returned at create time — verify the HMAC on your endpoint before trusting the payload.

Create webhook#

POST/api/mail/companies/{slug}/webhooks
const webhook = await sentroy.webhooks.create({
  url: "https://example.com/webhooks/sentroy",
  events: ["sent", "bounced", "opened", "clicked", "unsubscribed"],
  domainId: "domain-id",
})

console.log(webhook.secret) // Returned ONCE — store it now

Event types#

NameTypeDescription
sentstringMail handed off to SMTP successfully
bouncedstringHard or soft bounce reported by remote MTA
failedstringSend pipeline failure (rendering, suppression, quota)
openedstringTracking pixel hit (requires trackOpens at send time)
clickedstringLink click recorded (requires trackClicks at send time)
unsubscribedstringRecipient clicked the {{unsubscribe_url}} link

List + scope#

GET/api/mail/companies/{slug}/webhooks
const all = await sentroy.webhooks.list()
const scoped = await sentroy.webhooks.list("domain-id")

Test fire#

Manual dispatch — POST a custom payload at the webhook's current URL. The result and a row in the delivery log are returned for inspection. The mail server's automated event delivery is unaffected; this is a debug tool, not a production retry path.

POST/api/mail/companies/{slug}/webhooks/{id}/test
const result = await sentroy.webhooks.test(webhook.id, {
  event: "sent",
  payload: {
    mailLogId: "ml_abc",
    to: "user@example.com",
    subject: "Welcome",
  },
})

console.log(result.responseStatus, result.durationMs)

List deliveries#

Paginated history of test/replay dispatches recorded for the webhook. Each row carries the full payload, response body, status, and round-trip duration.

GET/api/mail/companies/{slug}/webhooks/{id}/deliveries?page=1&limit=50&status=failed
const { items, total } = await sentroy.webhooks
  .deliveries(webhook.id)
  .list({ page: 1, limit: 50, status: "failed" })

Get delivery#

GET/api/mail/companies/{slug}/webhooks/{id}/deliveries/{deliveryId}
const delivery = await sentroy.webhooks
  .deliveries(webhook.id)
  .get(deliveryId)

Replay delivery#

Re-fire the recorded payload at the webhook's current URL. Useful for retesting after the receiver fixes a bug. The new row is linked to the original via replayOf.

POST/api/mail/companies/{slug}/webhooks/{id}/deliveries/{deliveryId}/replay
const result = await sentroy.webhooks
  .deliveries(webhook.id)
  .replay(deliveryId)

Update + delete#

PATCH/api/mail/companies/{slug}/webhooks/{id}
await sentroy.webhooks.update(webhook.id, { active: false })
DELETE/api/mail/companies/{slug}/webhooks/{id}
await sentroy.webhooks.delete(webhook.id)

Logs#

Query the mail log to debug delivery issues, surface per-message status in your own UI, or build a customer-facing activity timeline.

List logs#

GET/api/mail/companies/{slug}/logs?status=bounced&domainId=domain-id&from=2026-05-01T00:00:00Z&to=2026-05-31T23:59:59Z&page=1&limit=100
const logs = await sentroy.logs.list({
  status: "bounced",
  domainId: "domain-id",
  from: "2026-05-01T00:00:00Z",
  to: "2026-05-31T23:59:59Z",
  page: 1,
  limit: 100,
})

Get a single entry#

GET/api/mail/companies/{slug}/logs/{id}
const log = await sentroy.logs.get(logs[0].id)
console.log(log.openedAt, log.clickedAt) // tracking timestamps if enabled