Emails
Send transactional email and list recent sends. Migrating from Resend? See docs/migration.md.
Send email
POST /emails
Send a transactional email from a verified domain.
Base URL: https://api.ranla.ai
Authentication
Authorization: Bearer rnl_…API keys are created in the dashboard (API Keys). Keys start with rnl_.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from |
string or { email, name? } |
Yes | Must be on a verified domain |
to |
string or string[] |
Yes | Recipient(s) |
subject |
string |
Yes | Subject line |
html |
string |
No* | HTML body (htmlBody / html_body aliases accepted) |
text |
string |
No* | Plain-text body (textBody, text_body, and plain_body aliases accepted) |
reply_to |
string or string[] |
No | Reply-to address(es) (replyTo alias accepted) |
cc |
string or string[] |
No | CC recipient(s) |
bcc |
string or string[] |
No | BCC recipient(s) |
tags |
{ name, value }[] or object |
No | Up to 10 key/value tags stored with the email |
headers |
object |
No | Custom headers; dangerous transport headers are rejected |
attachments |
array |
No | Up to 10 files, 10MB decoded each; executable/script extensions are blocked. Attachment aliases filename, contentType, content, and contentId are also accepted. |
scheduled_at |
string |
No | ISO 8601 timestamp. Creates a scheduled email instead of sending immediately (scheduledAt alias accepted) |
unsubscribe |
boolean |
No | Opt in to managed unsubscribe (see below). Off by default for transactional mail. |
category |
string |
No | Optional override: transactional, product, or newsletter. Inherits from template when sending with a template. |
* At least one of html or text is required.
Managed unsubscribe (opt-in)
Pure transactional mail (password resets, receipts, security alerts) should leave unsubscribe off.
When unsubscribe: true, Ranla:
- Injects RFC 8058
List-UnsubscribeandList-Unsubscribe-Postheaders (one-click unsubscribe) - Replaces
{{unsubscribe_url}}insubject,html, andtextwith a signed confirmation link - Binds the link to the first
toaddress for that message
When sending with a published template, the template's unsubscribe_enabled flag is used unless you pass unsubscribe explicitly on the send request. Product and newsletter templates auto-enable managed unsubscribe and support category-scoped opt-out — recipients can unsubscribe from that category without blocking transactional mail.
Template categories
Templates have a category that controls preference management:
| Category | Unsubscribe behavior |
|---|---|
transactional |
Default. No category opt-out. Global suppression only. |
product |
Category-scoped opt-out (onboarding tips, upgrade nudges) |
newsletter |
Same mechanism as product, for broadcast-style mail |
Sends to recipients who opted out of a category return 422:
{
"error": {
"message": "One or more recipients opted out of this email category",
"code": "validation_error",
"details": {
"category_unsubscribed": ["[email protected]"],
"category": "product",
"email_id": "msg_…"
}
}
}Global suppressions return the same status with details.suppressed instead.
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Product updates",
"html": "<p>Hi there.</p><p><a href=\"{{unsubscribe_url}}\">Unsubscribe</a></p>",
"unsubscribe": true
}Example
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Hello from Ranla",
"html": "<p>It works.</p>"
}With display name:
{
"from": { "email": "[email protected]", "name": "Your App" },
"to": ["[email protected]", "[email protected]"],
"subject": "Hello",
"html": "<p>Hi</p>",
"text": "Hi",
"reply_to": ["[email protected]"],
"tags": [{ "name": "order_id", "value": "ord_123" }],
"headers": { "X-Entity-ID": "ord_123" }
}Sandbox self-test
Before your own domain is verified, you can self-test with the shared sandbox domain:
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Sandbox check",
"html": "<p>Sandbox works.</p>"
}The sandbox sender is limited to the account email attached to the API key owner.
After your domain is verified, you can also self-test from your verified domain on the free plan — still limited to the account email:
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Verified domain check",
"html": "<p>My domain works.</p>"
}Add a payment method to unlock Free production (send to other recipients on your verified domain — no charge until you upgrade). Upgrade to Pro or Scale for more volume.
Responses
200 — Accepted
{
"id": "msg_abc123…",
"status": "sent"
}Scheduled sends return:
{
"id": "msg_abc123…",
"status": "scheduled"
}id is a stable public message ID (msg_…). Use it to correlate webhook events and dashboard activity.
Idempotency
Optional header on POST /emails:
Idempotency-Key: order-123Keys are scoped per account, retained for 24 hours. Replays return the original response. Reusing a key with a different body returns 409 idempotency_conflict.
await client.emails.send({ …, idempotencyKey: 'order-123' })Retrieve email
GET /emails/{id}
Returns one send by public id (msg_…).
const { email } = await client.emails.get('msg_abc123…')Resend
POST /emails/{id}/resend
Creates a new send from a previously stored email (same from/to/subject/body, tags, and headers). Returns a new msg_… id. Attachment bytes are not stored, so attachments are omitted.
Dashboard: Emails → Resend on any row.
SDK:
await client.emails.resend('msg_abc123…')CLI:
supersendtx emails resend --id msg_abc123…Schedule, reschedule, and cancel
Add scheduled_at to POST /emails to store the message and enqueue it for later delivery:
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Tomorrow",
"html": "<p>See you tomorrow.</p>",
"scheduled_at": "2026-08-01T12:00:00.000Z"
}Scheduled emails can be updated only while their status is scheduled:
PATCH /emails/msg_abc123…{ "scheduled_at": "2026-08-01T13:00:00.000Z" }Cancel a scheduled email:
{ "cancel": true }SDK:
await client.emails.update('msg_abc123…', { scheduledAt: '2026-08-01T13:00:00.000Z' })
await client.emails.cancel('msg_abc123…')CLI:
supersendtx emails cancel --id msg_abc123…Scheduled sends store message bodies so the worker can send them later. Attachments are not supported on scheduled emails in v1 because raw attachment data is not stored.
Batch send
POST /emails/batch
Send up to 100 emails in one request:
{
"emails": [
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Hello",
"html": "<p>Hi</p>"
}
]
}The response is index-aligned:
{
"data": [
{ "index": 0, "id": "msg_abc123…", "status": "sent" }
]
}Per-email validation or delivery failures are returned in that item’s error. Attachments are rejected for batch sends in v1.
SDK:
const result = await client.emails.batch([{ from, to, subject, html }])List emails
GET /emails?limit=25&cursor=…
Returns recent sends for the authenticated API key’s account. limit is 1–100 (default 25). Use opaque cursor / next_cursor for pagination.
Responses include rate-limit headers: ratelimit-limit, ratelimit-remaining, ratelimit-reset.
200
{
"emails": [
{
"id": "msg_abc123…",
"from": "[email protected]",
"to": ["[email protected]"],
"cc": [],
"bcc": [],
"reply_to": [],
"subject": "Hello",
"status": "delivered",
"last_event": "delivered",
"bounce_reason": null,
"tags": [],
"scheduled_at": null,
"cancelled_at": null,
"created_at": "2026-07-25T12:00:00.000Z",
"sent_at": "2026-07-25T12:00:01.000Z",
"delivered_at": "2026-07-25T12:00:05.000Z",
"bounced_at": null
}
],
"has_more": false,
"next_cursor": null
}Status values: queued, scheduled, sent, delivered, bounced, failed, cancelled.
SDK
const { emails, next_cursor } = await client.emails.list({ limit: 10 })Error format
All errors return:
{
"error": {
"message": "Human-readable message",
"code": "validation_error",
"details": {}
}
}| Status | When |
|---|---|
| 400 | Invalid JSON, missing fields, invalid from |
| 401 | Missing/invalid Bearer token or API key |
| 403 | from domain not verified, sandbox/verified-domain sender used for recipients outside the account email, or send to other recipients without Free production unlocked (payment method on file) |
| 409 | Idempotency key conflict |
| 429 | Free plan send limit (plan_limit) |
| 502 | Upstream mail delivery error |
| 503 | Mail delivery temporarily unavailable |
Attachment limits
- Maximum 10 attachments per non-batch, non-scheduled send
- Maximum 10MB decoded size per attachment
- Blocked filename extensions:
.exe,.bat,.cmd,.com,.scr,.js,.vbs,.dll
Examples
curl
curl -X POST https://api.ranla.ai/emails \
-H "Authorization: Bearer $RANLA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"[email protected]","to":"[email protected]","subject":"Hello","html":"<p>Hi</p>"}'Node.js
import { Ranla } from '@supersend/ranla'
const client = new Ranla(process.env.RANLA_API_KEY!)
await client.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Hello',
html: '<p>Hi</p>',
})Throws SuperSendTXError on failure (error.status, error.message, error.details).
CLI
supersendtx emails send \
--from [email protected] \
--to [email protected] \
--subject "Hello" \
--html "<p>Hi</p>" \
--tag order_id=ord_123 \
--schedule 2026-08-01T12:00:00.000ZRequires RANLA_API_KEY or --api-key.
OpenAPI
Machine-readable spec: openapi/supersendtx.yaml