Emails
Send transactional and marketing emails via the REST API.
Base URL
https://api.sendry.online/v1
All endpoints require Authorization: Bearer <api_key>.
Send email
POST /v1/emails
Requires sending_access scope.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Sender address. Format: Name <email@domain.com> or email@domain.com. Domain must be verified. |
to | string | string[] | Yes | Recipient(s). Up to 50 addresses. |
subject | string | Yes | Email subject line. |
html | string | No* | HTML body. Max 5 MB. At least one of html or text is required (unless using template_id). |
text | string | No* | Plain-text body. Max 1 MB. |
cc | string | string[] | No | CC recipients. |
bcc | string | string[] | No | BCC recipients. |
reply_to | string | No | Reply-to address. |
attachments | Attachment[] | No | Base64-encoded file attachments. |
tags | Tag[] | No | Custom key/value tags for filtering in analytics. |
headers | Record<string, string> | No | Custom email headers (e.g., X-* headers). |
scheduled_at | string (ISO 8601) | No | Schedule delivery for a future time. Must be in the future, max 1 year ahead. |
template_id | string | No | Use a saved template. Overrides html/text. |
variables | Record<string, any> | No | Template substitution variables. |
tracking | boolean | object | No | Open/click tracking for this message. Default true. |
tracking is per message and takes precedence over the sending domain's
track_opens / track_clicks. Omit it to follow the domain. A boolean sets
both kinds at once; an object sets one without touching the other:
{ "tracking": false } // no pixel, links delivered exactly as written
{ "tracking": { "clicks": false } } // links left alone, opens still recorded
{ "tracking": { "opens": false } } // no pixel, links still rewritten
Turn clicks off for any message whose links carry a one-time credential, such as
a password reset or a signing link: a rewritten link sends that credential
through the tracking domain first. Leaving opens alone means you still see
whether the message was read.
GET /v1/emails/:id returns the tracking that actually applied, as
tracking: { opens, clicks }, so a message with no open events can be told
apart from one that was never tracked.
Attachment object:
{
filename: string; // e.g., "invoice.pdf"
content: string; // Base64-encoded file content
content_type?: string; // MIME type, default "application/octet-stream"
}
Tag object:
{
name: string; // e.g., "category"
value: string; // e.g., "welcome"
}
Example request
curl -X POST https://api.sendry.online/v1/emails \
-H "Authorization: Bearer $SENDRY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": "user@example.com",
"subject": "Welcome to Acme!",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
"tags": [{"name": "category", "value": "welcome"}]
}'
Response
{
"id": "email_abc123",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme!",
"status": "queued",
"created_at": "2025-03-12T09:00:00Z"
}
Send marketing email
POST /v1/emails/marketing
Requires sending_access scope. Marketing emails automatically include List-Unsubscribe and List-Unsubscribe-Post headers for CAN-SPAM compliance.
Request body
Same as Send email, plus:
| Field | Type | Required | Description |
|---|---|---|---|
unsubscribe_url | string | Yes | URL added to List-Unsubscribe header. Required for CAN-SPAM. |
list_id | string | No | Mailing list identifier used for list-level unsubscribe tracking. |
Send batch
POST /v1/emails/batch
Requires sending_access scope. Send up to 100 emails in a single request. All emails are queued atomically.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
emails | EmailItem[] | Yes | Array of 1–100 email objects. |
from | string | No | Default sender address for all emails (can be overridden per-email). |
subject | string | No | Default subject for all emails (can be overridden per-email). |
template_id | string | No | Default template for all emails. |
Each item in emails accepts all send fields except from, subject, and template_id at the top level can serve as defaults.
Example request
curl -X POST https://api.sendry.online/v1/emails/batch \
-H "Authorization: Bearer $SENDRY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"subject": "Your order update",
"emails": [
{"to": "alice@example.com", "html": "<p>Alice, your order shipped!</p>"},
{"to": "bob@example.com", "html": "<p>Bob, your order shipped!</p>"}
]
}'
Response
{
"data": [
{"id": "email_abc123", "status": "queued"},
{"id": "email_abc124", "status": "queued"}
]
}
Get email
GET /v1/emails/:id
Requires read_only scope.
Response
{
"id": "email_abc123",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme!",
"status": "delivered",
"created_at": "2025-03-12T09:00:00Z",
"sent_at": "2025-03-12T09:00:01Z",
"last_event": "opened",
"region": "us-east-1",
"attachments": []
}
Status values: queued, sent, delivered, bounced, complained
Update a scheduled email
PATCH /v1/emails/:id
Requires full_access scope. Updates a queued email that has not been sent yet.
Request body
All fields are optional; include only the fields you want to change.
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | No | New subject line. |
html | string | No | New HTML body. Max 5 MB. |
text | string | No | New plain-text body. Max 1 MB. |
scheduled_at | string | No | New ISO 8601 scheduled send time. |
Example request
{
"subject": "Updated: your order has shipped",
"scheduled_at": "2025-03-16T09:00:00Z"
}
Response
Returns the updated email object, same shape as Get email.
List emails
GET /v1/emails
Requires read_only scope.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Results per page (1–100). |
cursor | string | Pagination cursor from previous response's next_cursor. | |
status | string | Filter by status: queued, sent, delivered, bounced, complained. |
Response
{
"data": [
{
"id": "email_abc123",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme!",
"status": "delivered",
"created_at": "2025-03-12T09:00:00Z",
"sent_at": "2025-03-12T09:00:01Z"
}
],
"has_more": false,
"next_cursor": null
}
Pagination is cursor-based. Pass next_cursor as the cursor parameter to get the next page.
List events for an email
GET /v1/emails/:id/events
Requires read_only scope. Lists every delivery event for one email (opens, clicks, bounces, complaints, and so on). Every event carries the individual recipient it belongs to; a message addressed to more than one recipient is delivered as one copy per recipient, each with its own tracking pixel and tracked links. Events recorded before per-recipient delivery shipped may carry the recipient *, meaning the message as a whole.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Results per page (1–100). |
cursor | string | Pagination cursor from the previous response's next_cursor. | |
type | string | Filter by event type: sent, delivered, opened, clicked, bounced, complained, unsubscribed. |
Response
{
"data": [
{
"id": "evt_abc123",
"email_id": "email_abc123",
"type": "delivered",
"recipient": "user@example.com",
"metadata": null,
"created_at": "2025-03-12T09:00:05Z"
}
],
"has_more": false,
"next_cursor": null
}
Pagination is cursor-based, matching List emails.
Get deliverability insights
GET /v1/emails/:id/insights
Requires read_only scope. Runs a set of pre-send checks on the message and returns a deliverability score with the individual checks.
Response
{
"score": 86,
"checks": [
{
"id": "spf",
"label": "SPF record",
"status": "pass",
"message": "The sending domain publishes an SPF record that authorizes Sendry.",
"action": null
},
{
"id": "subject",
"label": "Subject length",
"status": "warn",
"message": "The subject line is longer than 60 characters and may be truncated.",
"action": "Shorten the subject to 60 characters or fewer."
}
],
"generated_at": "2025-03-12T09:00:00Z"
}
Each check has a status of pass, warn, fail, or info.
Generate a shareable preview link
GET /v1/emails/:id/share
Requires read_only scope. Returns a preview URL for the email that expires after 48 hours.
Response
{
"url": "/preview/emails/V1StGXR8Z5jdHi6BmyT0fKc9QwErTy",
"expires_at": "2025-03-14T09:00:00Z"
}
The returned url is relative. Prefix it with your dashboard origin to build the full link.
Cancel email
POST /v1/emails/:id/cancel
Requires sending_access scope. Cancels a scheduled or queued email. Returns an error if the email has already been sent.
Response
{
"id": "email_abc123",
"status": "cancelled"
}