# SNDR documentation > SNDR is an email API for developers: verify a domain, send transactional email with one HTTPS request, and receive signed delivery webhooks. Base URL: https://api.sndr.sh/v1. Auth: `Authorization: Bearer `. --- # Introduction > SNDR is an email API for developers. Verify a domain, send with one request, and track every message. Source: https://sndr.sh/docs - [Quickstart](https://sndr.sh/docs/getting-started): Send your first email in a few minutes. - [JavaScript SDK](https://sndr.sh/docs/quickstart): Install `@rkiza/sndr` and send from any runtime. - [Send email](https://sndr.sh/docs/sending): Fields, limits, templates and attachments. - [API reference](https://sndr.sh/docs/api-reference): Every endpoint with request and response examples. ## Build with AI The docs are published as Markdown for ChatGPT, Claude, Cursor and other AI tools. Open them in a chat, or point your coding agent at `llms-full.txt`. - [Ask ChatGPT](https://chatgpt.com/?hints=search&q=Read%20https%3A%2F%2Fsndr.sh%2Fllms-full.txt%20so%20I%20can%20ask%20questions%20about%20the%20SNDR%20email%20API.): Open a chat with the full SNDR docs loaded. - [Ask Claude](https://claude.ai/new?q=Read%20https%3A%2F%2Fsndr.sh%2Fllms-full.txt%20so%20I%20can%20ask%20questions%20about%20the%20SNDR%20email%20API.): Open a chat with the full SNDR docs loaded. - [llms-full.txt](https://sndr.sh/llms-full.txt): All docs in one Markdown file for agents. - [Use with AI](https://sndr.sh/docs/ai): Markdown URLs and a ready-made agent prompt. ## More - [Domains](https://sndr.sh/docs/domains): Set up SPF, DKIM and DMARC. - [Webhooks](https://sndr.sh/docs/webhooks): Delivery and bounce events, signed. - [Errors](https://sndr.sh/docs/errors): Every error code and what to do. - [Partner API](https://sndr.sh/docs/cranl): Act for SNDR users with OAuth 2.0. --- # Quickstart > Send your first email with SNDR in a few minutes. Source: https://sndr.sh/docs/getting-started You need a SNDR account, a domain whose DNS you can edit, and an API key. ### 1. Create an account Sign up at [app.sndr.sh](https://app.sndr.sh/register). The free plan includes 3,000 emails a month, no card needed. ### 2. Add and verify a domain In **Domains**, add a subdomain such as `mail.acme.com` and copy the DNS records SNDR shows you into your DNS provider. See [Domains](https://sndr.sh/docs/domains). ### 3. Create an API key In **API keys**, create a key with the `send` scope. It is shown once, so keep it in an environment variable: ![The Create API key dialog with a name, the Live environment, and the send and read scopes selected.](https://sndr.sh/docs/images/create-api-key.webp) ```bash export SNDR_API_KEY="sndr_live_..." ``` ### 4. Send an email cURL: ```bash curl -X POST https://api.sndr.sh/v1/send \ -H "Authorization: Bearer $SNDR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": ["customer@example.com"], "subject": "Hello from SNDR", "html": "

It works.

" }' ``` Node.js: ```js import { Sndr } from '@rkiza/sndr'; const sndr = new Sndr(process.env.SNDR_API_KEY); const email = await sndr.emails.send({ from: 'Acme ', to: 'customer@example.com', subject: 'Hello from SNDR', html: '

It works.

', }); console.log(email.id); // eml_... ``` Python: ```python import os import requests res = requests.post( "https://api.sndr.sh/v1/send", headers={"Authorization": f"Bearer {os.environ['SNDR_API_KEY']}"}, json={ "from": "Acme ", "to": ["customer@example.com"], "subject": "Hello from SNDR", "html": "

It works.

", }, ) res.raise_for_status() print(res.json()["id"]) ``` Go: ```go body := strings.NewReader(`{ "from": "Acme ", "to": ["customer@example.com"], "subject": "Hello from SNDR", "html": "

It works.

" }`) req, _ := http.NewRequest("POST", "https://api.sndr.sh/v1/send", body) req.Header.Set("Authorization", "Bearer "+os.Getenv("SNDR_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) ``` ### 5. Check the result You get `202 Accepted` with the email `id` and `status: "queued"`. Follow it in **Emails** in the dashboard, or get notified with [webhooks](https://sndr.sh/docs/webhooks). ## Next steps - [Send email](https://sndr.sh/docs/sending): Every field, limit and option of a send. - [Webhooks](https://sndr.sh/docs/webhooks): Get delivery and bounce events. - [API reference](https://sndr.sh/docs/api-reference): All endpoints with examples. - [Use with AI](https://sndr.sh/docs/ai): Give ChatGPT, Claude or Cursor the docs. --- # JavaScript SDK > Send from Node.js, Bun, Deno or edge runtimes with the official SDK. Source: https://sndr.sh/docs/quickstart ## Install npm: ```bash npm install @rkiza/sndr ``` pnpm: ```bash pnpm add @rkiza/sndr ``` bun: ```bash bun add @rkiza/sndr ``` Works on Node 18+, Bun, Deno, Cloudflare Workers and Vercel Edge. It uses the platform `fetch`, with no native dependencies. ## Send an email send.ts: ```js import { Sndr } from '@rkiza/sndr'; const sndr = new Sndr(process.env.SNDR_API_KEY); const email = await sndr.emails.send({ from: 'Acme ', to: 'customer@example.com', subject: 'Hello from SNDR', html: '

It works.

', }); console.log(email.id); // eml_... ``` `to`, `cc` and `bcc` accept one address or an array. The SDK adds an `Idempotency-Key` to every send, so its automatic retries are safe. ## Options - `apiKey` (string, required): Your API key. Pass it as the only argument, or inside an options object. - `baseUrl` (string): API host. Defaults to `https://api.sndr.sh`. - `retries` (object | false): Retries on network errors, `429` and `5xx`. Default: 3 attempts. `false` turns them off. ## Handle errors ```js import { Sndr, SndrError } from '@rkiza/sndr'; try { await sndr.emails.send({ /* ... */ }); } catch (err) { if (err instanceof SndrError) { console.error(err.code, err.status, err.requestId); } } ``` `code` matches the [error codes](https://sndr.sh/docs/errors). Include `requestId` when you contact support. ## Other resources The client also exposes `sndr.domains`, `sndr.templates`, `sndr.webhooks`, `sndr.apiKeys` and `sndr.analytics`. To verify webhooks, use `verifyWebhookSignature` — see [Webhooks](https://sndr.sh/docs/webhooks). --- # Use with AI > Give ChatGPT, Claude, Cursor or any agent the SNDR docs. Source: https://sndr.sh/docs/ai Every docs page is also available as plain Markdown, so AI tools can read it without scraping HTML. | URL | What it is | | --- | --- | | [/llms.txt](https://sndr.sh/llms.txt) | Index of all pages with a one-line summary each. | | [/llms-full.txt](https://sndr.sh/llms-full.txt) | Every page in one Markdown file. Best for a full integration. | | `.md` | Any page as Markdown, e.g. [/docs/sending.md](https://sndr.sh/docs/sending.md). | | [/docs/cranl/openapi.json](https://sndr.sh/docs/cranl/openapi.json) | OpenAPI spec of the Partner API, for code generators. | ## On any page Use the **Copy page** menu at the top of a page to copy it as Markdown, or open it in ChatGPT, Claude or Perplexity with the page already loaded. ## Prompt for coding agents Paste this into Cursor, Claude Code, Copilot or ChatGPT: ```text Read https://sndr.sh/llms-full.txt, then add email sending to this project with the SNDR API. Use the @rkiza/sndr SDK for JavaScript, or plain HTTPS otherwise. Read the API key from the SNDR_API_KEY environment variable and never hard-code it. ``` > **Tip:** Never paste a real API key into a chat. Agents only need the variable name. --- # Send email > The fields, limits and options of a send request. Source: https://sndr.sh/docs/sending Send with `POST /v1/send`. The API answers `202 Accepted` straight away and delivers in the background. ![Your app posts to /v1/send and gets 202 queued. SNDR DKIM-signs and delivers the email; the result (delivered, bounced or failed) goes to your webhook endpoint.](https://sndr.sh/docs/images/send-lifecycle.svg) cURL: ```bash curl -X POST https://api.sndr.sh/v1/send \ -H "Authorization: Bearer $SNDR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": ["customer@example.com"], "subject": "Hello from SNDR", "html": "

It works.

" }' ``` Node.js: ```js import { Sndr } from '@rkiza/sndr'; const sndr = new Sndr(process.env.SNDR_API_KEY); const email = await sndr.emails.send({ from: 'Acme ', to: 'customer@example.com', subject: 'Hello from SNDR', html: '

It works.

', }); console.log(email.id); // eml_... ``` Python: ```python import os import requests res = requests.post( "https://api.sndr.sh/v1/send", headers={"Authorization": f"Bearer {os.environ['SNDR_API_KEY']}"}, json={ "from": "Acme ", "to": ["customer@example.com"], "subject": "Hello from SNDR", "html": "

It works.

", }, ) res.raise_for_status() print(res.json()["id"]) ``` Go: ```go body := strings.NewReader(`{ "from": "Acme ", "to": ["customer@example.com"], "subject": "Hello from SNDR", "html": "

It works.

" }`) req, _ := http.NewRequest("POST", "https://api.sndr.sh/v1/send", body) req.Header.Set("Authorization", "Bearer "+os.Getenv("SNDR_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) ``` ## Request fields - `from` (string, required): The sender: `hello@mail.acme.com` or `Acme `. Its domain must be verified. - `to` (string[], required): Recipients, 1 to 1,000 addresses. Always an array in the HTTP API. - `cc` (string[]): Copied recipients, up to 100. - `bcc` (string[]): Hidden recipients, up to 100. - `reply_to` (string): Where replies go, if not to `from`. - `subject` (string, required): One line, up to 998 characters. Optional when the template has a subject. - `html` (string): HTML body. Send `html`, `text` or both. - `text` (string): Plain-text body for clients that don’t show HTML. - `template_id` (string): A saved template. Its subject and bodies fill any field you leave empty. - `variables` (object): Values for `{{name}}` placeholders in the subject and bodies, e.g. `{"name": "Sara"}`. - `attachments` (object[]): Files: `{ "filename", "content", "content_type" }`, with `content` base64-encoded. - `headers` (object): Extra email headers, e.g. `{"X-Campaign": "welcome"}`. ## Limits | Limit | Value | | --- | --- | | Recipients (`to` + `cc` + `bcc`) | 1,000 per request | | Attachments | 10 files, 10 MiB total after decoding | | Request body | 16 MiB | | Subject | 998 characters, one line | ## Retry safely Add an `Idempotency-Key` header with a unique value, such as your order ID. A retry with the same key returns the first response instead of sending the email again. ```bash -H "Idempotency-Key: order-1234-receipt" ``` ## Suppressed recipients Addresses on your suppression list (hard bounces, complaints, unsubscribes) are removed before sending. If every `to` address is suppressed, the request fails with `suppressed`. ## Response 202 Accepted: ```json { "id": "eml_01HXKQ8B3N", "from": "Acme ", "to": ["customer@example.com"], "subject": "Hello from SNDR", "status": "queued", "created_at": "2026-01-15T10:30:00Z" } ``` `status` starts as `queued` and ends as `delivered`, `bounced` or `failed`. Read it with `GET /v1/emails/{id}`. --- # Domains > Verify the domains you send from. Source: https://sndr.sh/docs/domains You can only send from a verified domain. Verifying proves you own it and lets inbox providers trust your mail. ### 1. Add the domain In **Domains**, click **Add domain** and enter the domain you’ll send from. ### 2. Add the DNS records Copy each record’s **Name** into the host field and its **Value** into the content field at your DNS provider. ![The SPF, DKIM and DMARC TXT records SNDR shows for mail.acme.com, each with a Name and a Value to copy.](https://sndr.sh/docs/images/domain-dns-records.webp) The three required records for `mail.acme.com`. Your values come from the dashboard. ### 3. Verify Click **Verify**. DNS changes usually show up within minutes, but can take longer at some providers. > **Tip:** Use a subdomain such as `mail.acme.com`. It keeps your sending reputation separate from your main domain. ## What each record does | Record | Purpose | | --- | --- | | SPF | Lists the servers allowed to send for your domain. | | DKIM | A public key that lets inboxes check each email’s signature. | | DMARC | Tells inboxes what to do when SPF or DKIM checks fail. | > **Warning:** Keep your existing MX records (Google Workspace, Zoho…) — sending doesn’t need them changed. If the domain already has an SPF record, add SNDR’s include to it instead of creating a second one. Manage domains from code with the [Domains API](https://sndr.sh/docs/api/domains). --- # Webhooks > Get signed HTTP notifications when emails are delivered, bounce or fail. Source: https://sndr.sh/docs/webhooks Add an endpoint in **Webhooks** (or with `POST /v1/webhooks`) and choose its events. SNDR sends each event as a signed `POST` with a JSON body. ## Events | Event | Sent when | | --- | --- | | email.delivered | The recipient’s mail server accepted the email. | | email.bounced | The address doesn’t exist (permanent failure). It’s added to your suppression list. | | email.failed | The email couldn’t be sent, for example a rejection or signing problem. | | email.unsubscribed | A recipient used the unsubscribe link. The address is added to your suppression list. | Payload: ```json { "id": "evt_3f9a1c2b7d4e", "type": "email.delivered", "created_at": "2026-01-15T10:30:04Z", "data": { "email_id": "eml_01HXKQ8B3N", "to": ["customer@example.com"] } } ``` Deliveries are retried on failure, so the same event can arrive twice. Use `id` to process each event once. ## Verify the signature Each request has an `X-Sndr-Signature` header: `t=,v1=`. The HMAC is computed over `t`, a dot, and the raw body, with your endpoint’s secret. Always check it before trusting the payload. SDK: ```js import { verifyWebhookSignature } from '@rkiza/sndr'; // rawBody: the exact request body, before JSON.parse const valid = verifyWebhookSignature( rawBody, request.headers.get('x-sndr-signature') ?? '', process.env.SNDR_WEBHOOK_SECRET, ); if (!valid) return new Response('Invalid signature', { status: 401 }); ``` Node.js: ```js import { createHmac, timingSafeEqual } from 'node:crypto'; // header: "t=1736937004,v1=5f2b..." (v1s appears while a new secret is staged) function verify(rawBody, header, secret) { const parts = header.split(',').map((p) => p.trim().split('=')); const t = parts.find(([k]) => k === 't')?.[1]; if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest(); return parts.some(([k, sig]) => k.startsWith('v1') && /^[0-9a-f]{64}$/i.test(sig ?? '') && timingSafeEqual(expected, Buffer.from(sig, 'hex'))); } ``` > **Note:** Reject requests whose `t` is more than 5 minutes old to stop replays. ## The secret The full secret is shown once, when you create the endpoint (`secret` in the API response). Lists only show `secret_prefix`, which can’t be used to verify. ## Rotate the secret ### 1. Stage a new secret Open the endpoint’s **Signing keys** and choose **Generate replacement secret**. Requests are now signed with both secrets (`v1` and `v1s`). ### 2. Accept both Update your receiver to accept either secret. ### 3. Promote Choose **Make primary**. The old secret stops signing new deliveries. --- # API overview > Base URL, authentication, scopes and conventions of the SNDR API. Source: https://sndr.sh/docs/api-reference The SNDR API is plain HTTPS and JSON. Every request goes to the same base URL: ```text https://api.sndr.sh/v1 ``` ## Authentication Send your API key as a Bearer token. Keys look like `sndr_live_...` and are created in **API keys** in the dashboard. ```bash curl https://api.sndr.sh/v1/emails \ -H "Authorization: Bearer $SNDR_API_KEY" ``` > **Warning:** Keep keys on your server. Never put them in browser or mobile code. ## Scopes | Scope | Allows | | --- | --- | | `read` | All `GET` endpoints. | | `send` | Sending and every create, update or delete. | API keys can list keys but not create or delete them. That is done in the dashboard only. ## Pagination Paginated lists take `limit` (default 50, max 200) and `cursor`. Pass the `next_cursor` from one page to get the next, while `has_more` is `true`. ```json { "data": [ ... ], "next_cursor": "eyJ0cyI6...", "has_more": true, "count": 50, "limit": 50 } ``` ## Errors Failed requests return an HTTP error status and an `error` object. See [Errors](https://sndr.sh/docs/errors) for every code. ## Endpoints - [Emails](https://sndr.sh/docs/api/emails): Send and look up emails. - [Domains](https://sndr.sh/docs/api/domains): Add and verify sending domains. - [Templates](https://sndr.sh/docs/api/templates): Reusable subjects and bodies. - [Contacts](https://sndr.sh/docs/api/contacts): Your audience for broadcasts. - [Webhooks](https://sndr.sh/docs/api/webhooks): Event endpoints and signing keys. - [Analytics](https://sndr.sh/docs/api/analytics): Sends, deliveries, bounces and complaints. --- # Errors > What each error code means and how to fix it. Source: https://sndr.sh/docs/errors Errors use standard HTTP status codes and the same JSON shape: ```json { "error": { "code": "domain_not_verified", "message": "Domain mail.acme.com is not verified.", "request_id": "req_8a91lk2" } } ``` - `code` (string, required): Stable, machine-readable reason. Branch on this, not on `message`. - `message` (string, required): Human-readable explanation. - `request_id` (string): Include it when you contact support. - `field` (string): The request field that caused the error, when there is one. - `fields` (object): Per-field messages for `validation_failed`. - `retry_after_seconds` (integer): How long to wait before retrying a `429`. ## Codes | Code | HTTP | Meaning | | --- | --- | --- | | invalid_request | 400 | The body or a parameter is invalid. `message` says what to fix. | | suppressed | 400 | Every `to` address is on your suppression list. | | unauthenticated | 401 | The API key is missing, wrong or revoked. | | quota_exceeded | 402 | Your monthly email quota is used up. Upgrade or wait for the reset. | | plan_limit_reached | 402 | Your plan’s limit for this resource (domains, webhooks…) is reached. | | insufficient_scope | 403 | The API key lacks the scope this endpoint needs (`read` or `send`). | | domain_not_linked | 403 | The `from` domain hasn’t been added to this workspace. | | domain_not_verified | 403 | The `from` domain is added but not verified yet. | | quarantine_active | 403 | Sending is paused for this workspace. Contact support. | | forbidden | 403 | This key or workspace isn’t allowed to do that. | | not_found | 404 | The resource doesn’t exist in this workspace. | | conflict | 409 | The resource already exists or changed. | | payload_too_large | 413 | The request body is over 16 MiB. | | validation_failed | 422 | One or more fields are invalid. See `fields`. | | rate_limited | 429 | Too many requests. Retry after `retry_after_seconds`. | | send_velocity | 429 | You’re sending faster than allowed. Back off and retry. | | server_error | 500 | Something failed on our side. Retry with backoff. | | internal | 500 | Same as `server_error`. | | sends_frozen | 503 | Sending is paused for maintenance. Check the status page and retry. | > **Tip:** Retry `429` and `5xx` responses with exponential backoff. Don’t retry `4xx` errors until you’ve fixed the request. --- # Emails > Send emails and look up their status. Source: https://sndr.sh/docs/api/emails - [Send email](https://sndr.sh/docs/api/emails/send-email) `POST /v1/send`: Queue an email for delivery. - [List emails](https://sndr.sh/docs/api/emails/list-emails) `GET /v1/emails`: List emails sent from your workspace, newest first. - [Get email](https://sndr.sh/docs/api/emails/get-email) `GET /v1/emails/{id}`: Retrieve one email with its current status. --- # Send email > Queue an email for delivery. Source: https://sndr.sh/docs/api/emails/send-email `POST https://api.sndr.sh/v1/send` Queue an email for delivery. Returns 202 Accepted with the queued Email. Send an Idempotency-Key header to make retries safe: a repeat with the same key within 24 hours returns the first response. Scope: `send` **Body** (JSON) - `from` (string, required): Sender, as an address or "Name
". Its domain must be added and verified on your workspace. - `to` (string[], required): Recipient addresses, 1–1000, plain format (no display names). Must be an array, even for one recipient. - `cc` (string[]): CC addresses, max 100. - `bcc` (string[]): BCC addresses, max 100. to + cc + bcc together may not exceed 1000. - `subject` (string): Subject line, single line, max 998 bytes. Required unless the template provides one. - `html` (string): HTML body. Send html, text or both, unless template_id provides them. - `text` (string): Plain-text body. - `reply_to` (string): Reply-To address. - `template_id` (string): Saved template ID (tpl_...). Its subject, html and text fill any of those fields you leave empty. - `variables` (object): Values for {{ name }} placeholders in subject, html and text, e.g. {"first_name":"Alex"}. {{ user.name }} reads nested objects; unknown placeholders are left as is. - `headers` (object): Extra headers as string pairs, e.g. {"X-Entity-Ref-ID":"1234"}. No CR/LF allowed; standard headers such as From, To, Subject, Reply-To or List-Unsubscribe are ignored. - `tags` (object[]): Objects with string values, e.g. [{"name":"category","value":"welcome"}]. Accepted but not stored or returned yet. - `attachments` (object[]): Up to 10 files as {filename, content, content_type?}. content is standard base64; content_type defaults to application/octet-stream. Max 10 MiB decoded in total; the whole request max 16 MiB. **Request** ```bash curl -X POST 'https://api.sndr.sh/v1/send' \ -H "Authorization: Bearer $SNDR_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: order-1234-confirmation' \ -d '{ "from": "Acme ", "to": ["customer@example.com"], "reply_to": "support@acme.com", "subject": "Your order #{{ order_id }} is confirmed", "html": "

Thanks, {{ first_name }}!

Order #{{ order_id }} is being prepared.

", "text": "Thanks, {{ first_name }}! Order #{{ order_id }} is being prepared.", "variables": { "first_name": "Alex", "order_id": "1234" } }' ``` **Response** (202) ```json { "id": "em_3f9c1a2b-7d4", "organization_id": "org_8d41c7a2-3f6", "from": "\"Acme\" ", "to": ["customer@example.com"], "subject": "Your order #1234 is confirmed", "status": "queued", "domain_id": "dom_a7c2e910-4b1", "created_at": "2026-06-01T12:00:00Z" } ``` **Errors** - `unauthenticated` (401): Missing or invalid API key. - `invalid_request` (400): Body failed validation — e.g. to sent as a string, an invalid address, no subject or body, over 1000 recipients, CR/LF in a header field, unknown template_id, or a bad attachment. - `suppressed` (400): Every to address is on the suppression list. Suppressed cc/bcc addresses are dropped silently. - `quota_exceeded` (402): The monthly email quota is used up. It resets on the 1st of the month (UTC). - `insufficient_scope` (403): The API key is missing the send scope. - `domain_not_linked` (403): The from domain has not been added to your workspace. - `domain_not_verified` (403): The from domain is added but its DNS records are not verified yet. - `quarantine_active` (403): Sending is paused for this account. Contact SNDR support. - `forbidden` (403): A recipient is on a known spam-trap list. Remove it and retry. - `payload_too_large` (413): The request, or the rendered html + text, is over 16 MiB. - `send_velocity` (429): Sending too fast for this account. Back off and retry. - `sends_frozen` (503): Sending is temporarily paused platform-wide. Retry later. --- # List emails > List emails sent from your workspace, newest first. Source: https://sndr.sh/docs/api/emails/list-emails `GET https://api.sndr.sh/v1/emails` List emails sent from your workspace, newest first. Cursor-paginated: pass next_cursor back as cursor while has_more is true. Scope: `read` **Query parameters** - `limit` (integer): Page size. Default 50, max 200. - `cursor` (string): The next_cursor value from the previous page. Omit for the first page. **Request** ```bash curl 'https://api.sndr.sh/v1/emails?limit=1' \ -H "Authorization: Bearer $SNDR_API_KEY" ``` **Response** (200) ```json { "data": [ { "id": "em_3f9c1a2b-7d4", "organization_id": "org_8d41c7a2-3f6", "from": "\"Acme\" ", "to": ["customer@example.com"], "subject": "Your order #1234 is confirmed", "status": "delivered", "domain_id": "dom_a7c2e910-4b1", "created_at": "2026-06-01T12:00:00Z", "delivered_at": "2026-06-01T12:00:04Z" } ], "next_cursor": "eyJ0cyI6IjIwMjYtMDYtMDFUMTI6MDA6MDBaIiwiaWQiOiJlbV8zZjljMWEyYi03ZDQifQ", "has_more": true, "count": 1, "limit": 1 } ``` **Errors** - `unauthenticated` (401): Missing or invalid API key. - `insufficient_scope` (403): The API key is missing the read scope. - `validation_failed` (422): limit is above 200, or cursor is not a value returned as next_cursor. --- # Get email > Retrieve one email with its current status. Source: https://sndr.sh/docs/api/emails/get-email `GET https://api.sndr.sh/v1/emails/{id}` Retrieve one email with its current status. error explains a failed or bounced email; message bodies are not returned. Scope: `read` **Path parameters** - `id` (string, required): Email ID (em_...), as returned by POST /send. **Request** ```bash curl 'https://api.sndr.sh/v1/emails/em_3f9c1a2b-7d4' \ -H "Authorization: Bearer $SNDR_API_KEY" ``` **Response** (200) ```json { "id": "em_3f9c1a2b-7d4", "organization_id": "org_8d41c7a2-3f6", "from": "\"Acme\" ", "to": ["customer@example.com"], "cc": ["accounts@example.com"], "subject": "Your order #1234 is confirmed", "status": "delivered", "domain_id": "dom_a7c2e910-4b1", "created_at": "2026-06-01T12:00:00Z", "delivered_at": "2026-06-01T12:00:04Z" } ``` **Errors** - `unauthenticated` (401): Missing or invalid API key. - `insufficient_scope` (403): The API key is missing the read scope. - `not_found` (404): Resource does not exist or belongs to a different account. --- # Domains > Add, verify and remove the domains you send from. Source: https://sndr.sh/docs/api/domains - [List domains](https://sndr.sh/docs/api/domains/list-domains) `GET /v1/domains`: List all sending domains on your workspace with their status and DNS records. - [Add domain](https://sndr.sh/docs/api/domains/add-domain) `POST /v1/domains`: Add a sending domain. - [Verify domain](https://sndr.sh/docs/api/domains/verify-domain) `POST /v1/domains/{id}/verify`: Run the DNS checks now and return the updated domain. - [Delete domain](https://sndr.sh/docs/api/domains/delete-domain) `DELETE /v1/domains/{id}`: Remove a sending domain. --- # List domains > List all sending domains on your workspace with their status and DNS records. Source: https://sndr.sh/docs/api/domains/list-domains `GET https://api.sndr.sh/v1/domains` List all sending domains on your workspace with their status and DNS records. Not paginated. Scope: `read` **Request** ```bash curl 'https://api.sndr.sh/v1/domains' \ -H "Authorization: Bearer $SNDR_API_KEY" ``` **Response** (200) ```json { "data": [ { "id": "dom_a7c2e910-4b1", "organization_id": "org_8d41c7a2-3f6", "name": "mail.acme.com", "status": "verified", "region": "us-east-1", "spf_status": "verified", "dkim_status": "verified", "dmarc_status": "verified", "mx_status": "verified", "tls_rpt_status": "failed", "mta_sts_status": "failed", "bimi_status": "failed", "records": [ { "type": "TXT", "name": "mail.acme.com", "value": "v=spf1 include:_spf.sndr.sh ~all", "status": "verified", "hint": "Add include:_spf.sndr.sh to your existing SPF record, or use this value if no SPF record exists. Keep one SPF record and retain your other mail providers.", "required": true, "label": "SPF" } ], "created_at": "2026-01-20T09:00:00Z" } ] } ``` **Errors** - `unauthenticated` (401): Missing or invalid API key. - `insufficient_scope` (403): The API key is missing the read scope. --- # Add domain > Add a sending domain. Source: https://sndr.sh/docs/api/domains/add-domain `POST https://api.sndr.sh/v1/domains` Add a sending domain. Returns 201 with the domain (status pending) and the DNS records to publish; required ones have required: true. Scope: `send` **Body** (JSON) - `name` (string, required): Domain or subdomain to send from, e.g. mail.acme.com. - `region` (string): Region label stored on the domain. Defaults to us-east-1. **Request** ```bash curl -X POST 'https://api.sndr.sh/v1/domains' \ -H "Authorization: Bearer $SNDR_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"name": "mail.acme.com"}' ``` **Response** (201) ```json { "id": "dom_a7c2e910-4b1", "organization_id": "org_8d41c7a2-3f6", "name": "mail.acme.com", "status": "pending", "region": "us-east-1", "spf_status": "unknown", "dkim_status": "unknown", "dmarc_status": "unknown", "mx_status": "unknown", "tls_rpt_status": "unknown", "mta_sts_status": "unknown", "bimi_status": "unknown", "records": [ { "type": "TXT", "name": "mail.acme.com", "value": "v=spf1 include:_spf.sndr.sh ~all", "status": "unknown", "hint": "Add include:_spf.sndr.sh to your existing SPF record, or use this value if no SPF record exists. Keep one SPF record and retain your other mail providers.", "required": true, "label": "SPF" }, { "type": "TXT", "name": "sndr._domainkey.mail.acme.com", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...", "status": "unknown", "hint": "DKIM signing key — every outgoing message is signed with it.", "required": true, "label": "DKIM" }, { "type": "TXT", "name": "_dmarc.mail.acme.com", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@mail.acme.com; ruf=mailto:dmarc@mail.acme.com; pct=100; adkim=r; aspf=r", "status": "unknown", "hint": "Policy that ties SPF + DKIM together. Tells receivers what to do with messages that fail.", "required": true, "label": "DMARC" } ], "created_at": "2026-06-01T12:00:00Z" } ``` **Errors** - `unauthenticated` (401): Missing or invalid API key. - `invalid_request` (400): Body is not valid JSON, or name is missing or not a valid domain name. - `plan_limit_reached` (402): The plan’s domain limit is reached. - `insufficient_scope` (403): The API key is missing the send scope. - `conflict` (409): This domain is already added to your workspace. --- # Verify domain > Run the DNS checks now and return the updated domain. Source: https://sndr.sh/docs/api/domains/verify-domain `POST https://api.sndr.sh/v1/domains/{id}/verify` Run the DNS checks now and return the updated domain. status becomes verified when SPF, DKIM and DMARC all pass, otherwise it stays pending; each check is verified or failed. Scope: `send` **Path parameters** - `id` (string, required): Domain ID (dom_...). **Request** ```bash curl -X POST 'https://api.sndr.sh/v1/domains/dom_a7c2e910-4b1/verify' \ -H "Authorization: Bearer $SNDR_API_KEY" ``` **Response** (200) ```json { "id": "dom_a7c2e910-4b1", "organization_id": "org_8d41c7a2-3f6", "name": "mail.acme.com", "status": "pending", "region": "us-east-1", "spf_status": "verified", "dkim_status": "verified", "dmarc_status": "failed", "mx_status": "verified", "tls_rpt_status": "failed", "mta_sts_status": "failed", "bimi_status": "failed", "records": [ { "type": "TXT", "name": "_dmarc.mail.acme.com", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@mail.acme.com; ruf=mailto:dmarc@mail.acme.com; pct=100; adkim=r; aspf=r", "status": "failed", "hint": "Policy that ties SPF + DKIM together. Tells receivers what to do with messages that fail.", "required": true, "label": "DMARC" } ], "created_at": "2026-06-01T12:00:00Z" } ``` **Errors** - `unauthenticated` (401): Missing or invalid API key. - `insufficient_scope` (403): The API key is missing the send scope. - `not_found` (404): Resource does not exist or belongs to a different account. --- # Delete domain > Remove a sending domain. Source: https://sndr.sh/docs/api/domains/delete-domain `DELETE https://api.sndr.sh/v1/domains/{id}` Remove a sending domain. Emails already sent are unaffected, but the domain can no longer be used as a from address. Scope: `send` **Path parameters** - `id` (string, required): Domain ID (dom_...). **Request** ```bash curl -X DELETE 'https://api.sndr.sh/v1/domains/dom_a7c2e910-4b1' \ -H "Authorization: Bearer $SNDR_API_KEY" ``` **Response** (204) No content. **Errors** - `unauthenticated` (401): Missing or invalid API key. - `insufficient_scope` (403): The API key is missing the send scope. - `not_found` (404): Resource does not exist or belongs to a different account. --- # Templates > Subjects and bodies you reuse across sends. Source: https://sndr.sh/docs/api/templates - [List templates](https://sndr.sh/docs/api/templates/list-templates) `GET /v1/templates`: List all templates on your workspace, most recently updated first. - [Create template](https://sndr.sh/docs/api/templates/create-template) `POST /v1/templates`: Create a template. - [Get template](https://sndr.sh/docs/api/templates/get-template) `GET /v1/templates/{id}`: Retrieve one template, including its html and text body. - [Update template](https://sndr.sh/docs/api/templates/update-template) `PATCH /v1/templates/{id}`: Update the fields you send; omitted fields keep their value. - [Delete template](https://sndr.sh/docs/api/templates/delete-template) `DELETE /v1/templates/{id}`: Permanently delete a template. --- # List templates > List all templates on your workspace, most recently updated first. Source: https://sndr.sh/docs/api/templates/list-templates `GET https://api.sndr.sh/v1/templates` List all templates on your workspace, most recently updated first. Not paginated. Scope: `read` **Request** ```bash curl 'https://api.sndr.sh/v1/templates' \ -H "Authorization: Bearer $SNDR_API_KEY" ``` **Response** (200) ```json { "data": [ { "id": "tpl_c19e5d34-82a", "organization_id": "org_8d41c7a2-3f6", "name": "Order confirmation", "subject": "Your order #{{ order_id }} is confirmed", "html": "

Hi {{ first_name }},

Order #{{ order_id }} confirmed.

", "text": "Hi {{ first_name }}, order #{{ order_id }} confirmed.", "version": 3, "created_at": "2026-02-10T14:00:00Z", "updated_at": "2026-05-01T09:00:00Z" } ] } ``` **Errors** - `unauthenticated` (401): Missing or invalid API key. - `insufficient_scope` (403): The API key is missing the read scope. --- # Create template > Create a template. Source: https://sndr.sh/docs/api/templates/create-template `POST https://api.sndr.sh/v1/templates` Create a template. Returns 201 with the template. HTML is sanitized on save: scripts,