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-Unsubscribe and List-Unsubscribe-Post headers (one-click unsubscribe)
  • Replaces {{unsubscribe_url}} in subject, html, and text with a signed confirmation link
  • Binds the link to the first to address 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-123

Keys 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.000Z

Requires RANLA_API_KEY or --api-key.


OpenAPI

Machine-readable spec: openapi/supersendtx.yaml