# Sentroy Mail
> Transactional email API — templates, domains, mailboxes, inbox, suppressions, logs, webhooks, SMTP. Drop-in replacement for **Resend**, **Postmark**, **Mailgun**, and **SendGrid**. Same `stk_` token covers Mail + Storage + CDN.
## Quickstart
```bash
npm install @sentroy-co/client-sdk
```
```ts
import { Sentroy } from "@sentroy-co/client-sdk";
const sentroy = new Sentroy({
baseUrl: "https://sentroy.com",
companySlug: "acme",
accessToken: process.env.SENTROY_API_KEY!, // stk_...
});
await sentroy.send.execute({
from: "hello@acme.com",
to: ["user@example.com"],
subject: "Welcome",
html: "
Hi
",
text: "Hi",
});
```
## Authentication
- **Header:** `Authorization: Bearer stk_<48-hex>`
- **Required permission:** `send.execute` for sending; `domains.manage`, `mailboxes.manage`, `templates.manage`, `inbox.view`, `logs.view`, `suppressions.manage`, `webhooks.manage` for the corresponding admin surface.
- Tokens are company-scoped. Create in Dashboard → company → Settings → Access Tokens. Plaintext shown **once on create**.
## Endpoints
Base: `https://sentroy.com/api/mail/...` (SDK rewrites `/api/mail/*` automatically).
| Verb | Path | Purpose | Permission |
|---|---|---|---|
| `POST` | `/send` | Send transactional message (HTML, text, template, attachments) | `send.execute` |
| `GET` / `POST` | `/templates` | List + create templates with `{tr, en}` LocalizedString bodies | `templates.manage` |
| `GET` / `POST` | `/domains` | List + add sending domains (returns DNS records to copy) | `domains.manage` |
| `POST` | `/domains/:id/verify` | Re-check DKIM / SPF / DMARC | `domains.manage` |
| `GET` / `POST` | `/mailboxes` | List + provision IMAP mailboxes | `mailboxes.manage` |
| `GET` | `/inbox` | Read mailbox messages | `inbox.view` |
| `GET` | `/logs` | Delivery + event log | `logs.view` |
| `GET` / `POST` | `/suppressions` | Bounce / complaint suppressions | `suppressions.manage` |
| `GET` / `POST` | `/webhooks` | Outbound delivery webhooks | `webhooks.manage` |
## Recipes
### Templated send with variables
```ts
await sentroy.send.execute({
from: "noreply@acme.com",
to: ["user@example.com"],
templateId: "tpl_welcome",
variables: { firstName: "Ada", verifyUrl: "https://acme.com/v/abc" },
locale: "en", // picks the LocalizedString slot
});
```
### Creating templates
```ts
const tpl = await sentroy.templates.create({
name: { en: "Welcome", tr: "Hos geldin" }, // LocalizedString: string OR {tr, en, ...}
subject: "Welcome {firstName}",
mjmlBody: "...{firstName}...",
domainId: "dom_123", // REQUIRED — id of a verified sending domain
});
// tpl.variables -> string[] (auto-extracted from subject + body)
await sentroy.templates.update(tpl.id, { subject: "Hi {firstName}" }); // partial, >=1 field
await sentroy.templates.delete(tpl.id);
```
- No `variables` input — the platform **auto-extracts** the variable list from the body and returns it as `template.variables: string[]`. There is no declare-variables step.
- REST: `POST` / `PATCH /:id` / `DELETE /:id` on `/api/mail/companies/{slug}/templates` (`templates.manage`).
- CLI: `sentroy mail templates create --name= --subject= --domain= (--mjml-file= | --mjml='' | stdin)`; `update `; `delete `. Localized `--name` / `--subject` accept a plain string or a JSON object string (e.g. `--name='{"en":"Welcome","tr":"Hos geldin"}'`).
- Other-language SDKs (Go, Python, PHP) are templates **read-only** (`list`/`get`); create/update/delete is TS SDK, CLI, or REST.
### Template variables
Mustache-like, regex-based, single-level:
- **Scalar:** `{name}` or `{{name}}` (both brace forms work).
- **Array section:** `{#items} ... {/items}` — repeats per array element; item fields are in scope inside (e.g. `{title}`, `{price}`).
- **Inverted section:** `{^name} ... {/^name}` — renders only when `name` is missing / empty / false.
- Names match `\w+` (letters, digits, underscore), case-sensitive — no dashes/dots. **No default-value syntax.** Nested sections are not supported.
- At send, pass values in `variables` (scalars + arrays for sections), e.g. `variables: { firstName: "Ada", hasItems: true, items: [{ title: "Keyboard", price: "$80" }] }`.
- A template referencing a variable the send call does not provide is rejected with **HTTP 422** listing the missing names.
### Raw send (HTML + text)
```ts
await sentroy.send.execute({
from: "alerts@acme.com",
to: ["ops@example.com"],
subject: "Disk full",
html: "db-1 at 92%
",
text: "db-1 at 92%",
replyTo: "noreply@acme.com",
});
```
### Add + verify a sending domain
```ts
const { data: domain } = await sentroy.domains.create({ domain: "mail.acme.com" });
// domain.dns -> [{ type: "TXT", host: "...", value: "..." }, ...]
// publish DNS, then:
await sentroy.domains.verify(domain.id);
```
### Provision an IMAP mailbox
```ts
await sentroy.mailboxes.create({
domain: "mail.acme.com",
localPart: "support",
quotaMb: 5_000,
});
```
## DKIM / SPF / DMARC checklist
After `domains.create` Sentroy returns the DNS records you must publish on your authoritative zone before sending succeeds:
1. **TXT** at `._domainkey.` — DKIM public key
2. **TXT** at `` — SPF (`v=spf1 include:_spf.sentroy.com ~all`)
3. **TXT** at `_dmarc.` — DMARC (`v=DMARC1; p=quarantine; rua=mailto:...`)
4. **MX** at `` — only if you also want to **receive** mail through Sentroy mailboxes
5. Call `POST /api/mail/domains/:id/verify` until status flips to `verified`
## Gotchas
- **LocalizedString fields.** Template subjects and bodies use `{tr, en}` shape; pick a `locale` on send or pass a flat string for single-locale projects.
- **From-address must be on a verified domain.** Unverified domains 422 on send.
- **413 file-too-big.** Attachments capped at 25 MB per message; prefer Storage links for larger payloads.
- **Suppressions are sticky.** Once a recipient lands on the suppression list (bounce / complaint), subsequent sends silently skip them — remove via `DELETE /suppressions/:id`.
- **Webhook signing.** Outbound webhook payloads carry an HMAC header; verify before trusting.
## Error envelope
Every endpoint returns the same shape:
```json
{ "data": { "id": "..." }, "error": null }
```
On failure:
```json
{ "data": null, "error": "Domain not verified" }
```
HTTP status mirrors REST conventions (`401` missing/invalid token, `403` permission denied, `404` resource not found, `422` validation, `429` rate-limited, `5xx` upstream).
## Rate limits
- Default: **100 req / 10s** per `stk_` token across Mail endpoints.
- Send throughput shaped per-domain reputation; cold domains warm over 7 days.
- `429` includes `Retry-After` header — back off and retry.
## Competitors
Sentroy Mail is a direct alternative to **Resend**, **Postmark**, **Mailgun**, and **SendGrid**. Migrations covered in `/compare/resend` and `/compare/mailgun`; same primitives (domains, templates, send, webhooks, suppressions) with a single bearer token that also unlocks Storage, CDN, and Auth.
For full reference: https://docs.sentroy.com/mail