# Ranla — full documentation bundle Generated for AI agents. Canonical index: https://docs.ranla.ai/llms.txt OpenAPI: https://docs.ranla.ai/openapi.yaml --- # What is Ranla Ranla is the AI growth engineer. It runs the marketing across six jobs from what people do in your product, and ships the product events and conversion tracking it needs as pull requests you review. Transactional email runs on the same stack. You talk to Ranla in the app, in Slack, or from Cursor and Claude over [MCP](./ai/mcp.md). Each job has a desk in the app where you see what Ranla found, what it drafted, and what is waiting on you. ## The six jobs | Job | What Ranla does | What you connect | How-to | |-----|-----------------|------------------|--------| | **Engineering** | Writes the product events and conversion tracking the other jobs need, as a pull request you review. It does not push to your default branch. | GitHub, or send events with the SDK | [Instrument your product](./instrument-quickstart.md) | | **Lifecycle email** | Campaigns and automations for activation, trial endings, and win-backs, with a holdout when you want proof of lift. Transactional email and SMS run on the same stack. | A verified sending domain. SMS uses your own Twilio account. | [Automations](./api/automations.md) | | **Paid advertising** | Drafts ads and landing angles, watches spend against the goals you named, and recommends pausing what does not earn. Google Ads today. | Google Ads. Google bills ad spend to your own card. | [Google Ads](./guides/google-ads.md) | | **Search** | Finds the queries your buyers actually run, drafts pages that answer them, and tracks whether they rank. | Google Search Console. GitHub to deliver pages as pull requests. | [Search](./guides/seo.md) | | **AEO** | Tracks whether answer engines cite you, drafts the pages and proof that earn mentions, and reports lift. Google AI Overview, ChatGPT, and Perplexity. | Nothing | — | | **Social** | Finds threads worth joining, drafts posts and replies in your voice, and schedules what you approve. Reddit today. | Nothing to find threads and draft. Your Reddit account to post. | [Social](./guides/reddit.md) | ## What every job shares | Part | What it is | How-to | |------|------------|--------| | **Events** | The product signals every job reads: signup, activation, billing. Sent through the API, the dashboard, or PostHog, Amplitude, and Mixpanel. | [Events](./guides/events.md) · [Connect analytics](./guides/connect-analytics.md) | | **Brand** | How your product looks and talks, seeded from your website. Every draft uses it. | [Brand](./guides/brand.md) | | **Creative** | Images and ad assets composed in your brand style. | [Creative](./guides/creative.md) | | **Revenue** | Conversions and revenue by channel, from Stripe and Google Ads. | [Revenue](./guides/revenue.md) | | **On-site messages** | Banners and modals for visitors to your website. | [On-site messages](./guides/on-site-messages.md) | ## How far Ranla acts on its own You choose a mode on **Settings → Ranla**: | Mode | What it does | |------|--------------| | **Draft** | Nothing sends, spends, publishes, posts, or lands in your repo until you do it. | | **Propose** | It asks before each email, ad, page, post, or pull request, in the app or Slack. | | **Autopilot** | It runs inside the limits you set. Big or expensive work still waits. | The frequency cap and the daily account cap you set are enforced on every send, and any audience over 5,000 people needs your approval in every mode. See [Modes](./guides/modes.md). ## Free and paid plans | | Free | Paid, from $99/mo | |---|------|-------------------| | **Chat with Ranla** | 500 credits a month | 5,000 to 125,000 credits a month | | **Drafts and proposals in all six jobs** | Yes | Yes | | **Every desk and report** | Yes | Yes | | **Pull requests in your repo** | Yes, they use credits | Yes, they use credits | | **Transactional email** | 3,000 a month, 100 a day, 1 domain | On the same credits as lifecycle email | | **Sending campaigns and automations** | No | Yes | | **Autopilot and desks that check in daily** | No | Yes, inside the limits you set | Every paid plan has every feature; plans differ only in monthly credits. Start on Free and upgrade from **Billing** when you want Ranla to send. See [ranla.ai/pricing](https://ranla.ai/pricing). ## Next steps - [Instrument your product](./instrument-quickstart.md): connect GitHub (Ranla opens a pull request) or send events with the SDK - [Quickstart: send email](./quickstart.md): API key, domain DNS, first `POST /emails` - [Modes](./guides/modes.md): how far Ranla acts on its own - [MCP server](./ai/mcp.md): work with Ranla from Cursor or Claude - Product site: [ranla.ai](https://ranla.ai) · Dashboard: [app.ranla.ai](https://app.ranla.ai) --- # Instrument your product Wire Ranla into your SaaS so lifecycle email runs on product data — not only `POST /emails` for password resets. There are **two ways** to get events into Ranla: 1. **Have Ranla wire it** — Connect GitHub; Ranla inspects your product and opens one pull request for you to review. 2. **Install it yourself** — Add the SDK (or REST) and emit events from your app. **Only need auth email today?** Skip to [Quickstart: send email](./quickstart.md). **Marketing site, not product?** That is one line of HTML and a separate install — see [Track your website](#track-your-website). --- ## What you are setting up | Piece | GitHub path | DIY path | |-------|-------------|----------| | Account on [app.ranla.ai](https://app.ranla.ai) | Required | Required | | GitHub App on your product repo | Required | — | | Pull request with lifecycle events | Ranla opens it; you merge | — | | API key + SDK / REST | Often included in the PR | You add it | | Product events flowing | After you merge and deploy | You call `events.trigger` | | Verified sending domain | Still needed to send from your brand | Same | | Growth workspace (hire Ranla) | Optional for campaigns / agent | Same | Ranla reads events from your app. Campaigns and automations react to those events. Sends go through the same API as transactional mail. You can use **both** paths: GitHub for the first wiring (or later moments), and the SDK anytime you want to emit an event by hand. You can also [connect analytics](./guides/connect-analytics.md) for moments you already track elsewhere. --- ## Path A — Have Ranla wire it (recommended) Best when you want Ranla to find the right call sites in your codebase and open a PR. Ranla never pushes to your default branch. ### 1. Open Install Ranla on Home 1. Sign in at [app.ranla.ai](https://app.ranla.ai). 2. Go to **Home** (`/overview`). 3. Expand **Install Ranla** → **Show installation options**. ### 2. Connect GitHub 1. Click **Connect GitHub**. 2. Install the Ranla GitHub App and grant access to the repository that runs your product (not a docs-only or marketing-only repo unless that is where the app code lives). 3. When GitHub is connected, use **Choose a repository** to pick which repo Ranla may read. Connecting GitHub does **not** require hiring Ranla. You can still send events and use the send API on the free tier. ### 3. Let Ranla inspect and open a PR In **Growth** (or the agent window after hire), ask Ranla to instrument your product — or continue from the Install Ranla / welcome flow if it already started a scan. Ranla will: 1. Read the selected repository. 2. Propose lifecycle events (signups, activation, subscription moments, and similar). 3. Open **one pull request** with the wiring for you to review. You review and merge on GitHub. Deploy as you normally would. After merge, product moments should start showing up in **Events**. ### 4. Confirm events are live 1. Open **Events** in the dashboard and check that new moments appear after real product use (or a test signup). 2. Open **Growth** after you hire Ranla to see how those events feed segments and campaigns. ### 5. Wire more moments later When you need a new product moment (for example “emit when the first meaningful action happens”): - Ask the agent to wire that event, or - Start **Wire** from **Events** (proposed row or empty state) Ranla opens another scoped pull request for that moment. Status stays visible on **Events** (wiring → PR open → merged → live once the first matching event arrives). --- ## Path B — Install it yourself Use this when you prefer to own the integration, or to add events the GitHub PR did not cover. ### 1. Create an account and API key 1. Sign up at [app.ranla.ai](https://app.ranla.ai). 2. Open **Home** (`/overview`) or **Settings → API keys** and click **Create API key**. 3. Copy the `rnl_…` secret once. ```bash export RANLA_API_KEY=rnl_your_key_here ``` ### 2. Install the SDK ```bash npm install @supersend/ranla ``` ```ts import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY!) ``` **Other languages** — same API, Ranla defaults (`https://api.ranla.ai`): | Language | Install | |----------|---------| | Python | `pip install ranla` · `from ranla import Ranla` | | Go | `go get github.com/Super-Send/supersendtx-sdks/go/ranla` | | PHP | `composer require ranla/ranla` | | Ruby | `gem install ranla` · `Ranla::Client` | The legacy `supersendtx` packages on each registry remain supported for existing integrations. Working in Cursor or Claude? You can also drive setup through the [MCP server](./ai/mcp.md) and [agent skill notes](./ai/agent-skill.md). ### 3. Track product events Send events when something meaningful happens in your product. Ranla uses these for segments, automations, and the growth agent. ```ts await client.events.trigger({ name: 'user.created', user_id: 'user_123', email: 'ada@yourdomain.com', data: { plan: 'trial', source: 'signup' }, idempotencyKey: 'evt_user_created_123', }) ``` Common first events: | Event | When to fire | |-------|----------------| | `user.created` | Account or workspace created | | `user.activated` | Finished onboarding or first value action | | `subscription.started` | Paid or trial started | | `subscription.churned` | Cancelled or expired | REST equivalent: ```bash curl -X POST https://api.ranla.ai/events \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: evt_user_created_123" \ -d '{ "name": "user.created", "user_id": "user_123", "email": "ada@yourdomain.com", "data": { "plan": "trial" } }' ``` See [Events API](./api/events.md) for fields, idempotency, and automation matching. ### 4. Verify a sending domain Lifecycle and transactional email must send from a domain you control. 1. **Domains → Add domain** in the dashboard. 2. Add DNS records (Cloudflare one-click if offered). 3. **Verify DNS** on the domain page. Until DNS is verified, use sandbox self-test sends to your account email. See [Quickstart: send email](./quickstart.md) §4 for the full DNS table. ### 5. Send a test email (optional but recommended) Confirm the pipe works before you rely on automations: ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "noreply@yourdomain.com", "to": "you@yourdomain.com", "subject": "Ranla pipe check", "html": "

Send path works.

" }' ``` Free tier: 3,000 emails/mo · 100/day · 1 domain. Add a payment method to unlock production sends to other recipients. --- ## Track your website Product events tell you what people do *after* they sign up. The browser script tells you who showed up in the first place. It is a separate one-line install, and it is what fills **Acquisition → Analytics**. ### 1. Paste the script 1. Open **Home** in the dashboard and expand **Connect your marketing site**. 2. Copy the snippet for your account and paste it before `` on every page you want counted. 3. Load a page on your own site — any page, no email link and no campaign click needed — then hit **Check again**. The badge turns **Installed** about two seconds after that pageview. Two seconds is the whole wait: the beacon is acknowledged in milliseconds and a buffer writes it to the database every two seconds, so a page load and a click is the entire check. **Acquisition → Analytics** runs on the same beat — it reads today's traffic from those rows directly and only falls back to the five-minute rollup for history older than the last tick, so the first pageview is on the chart long before that tick lands. The one thing that does lag: a report already open in a tab is cached for 60 seconds. Reload it rather than watching it. Using Google Tag Manager? Connect your Google account from the same place, or paste the Custom HTML tag shown there — a plain ` ``` To stop **your own** visits from counting, run this once in your browser console on your site: ```js localStorage.setItem('rnl_ignore', '1') ``` That is per browser, so each teammate does it on their own machine. The same script also powers [on-site messages](./guides/on-site-messages.md) and `stx.identify()`, so installing it once covers all three. --- ## Turn on lifecycle (hire Ranla) The growth workspace (agent, campaign cycles, autopilot) unlocks when you **hire Ranla** on a paid band. Until then, `/growth` shows an upsell — you can still connect GitHub, send events, and use the send API on the free tier. In the dashboard after hire: 1. Open **Growth** and review what Ranla sees from your events. 2. Approve or edit a proposed campaign. 3. Optionally wire an [automation](./api/automations.md) for `user.created` → welcome sequence. --- ## Next steps - [What is Ranla](./what-is-ranla.md) - [Track your website](#track-your-website) · [On-site messages](./guides/on-site-messages.md) - [Google Ads](./guides/google-ads.md) · [Revenue](./guides/revenue.md) - [Connect analytics](./guides/connect-analytics.md) · [Events API](./api/events.md) - [Automations](./api/automations.md) · [Templates](./api/templates.md) - [Quickstart: send email](./quickstart.md) - [Agent skill notes](./ai/agent-skill.md) · [MCP server](./ai/mcp.md) --- # Ranla — Quickstart Send your first transactional email in about five minutes. Migrating from another provider? Start with [Resend](./migration.md), [Postmark](./migration/postmark.md), [Amazon SES](./migration/ses.md), or [SendGrid](./migration/sendgrid.md), then mirror send + webhook flows from the hosted [API Explorer](https://docs.ranla.ai/api-explorer). Building auth email? See [password reset](./guides/password-reset-emails.md), [Supabase Auth](./guides/supabase.md), and [Clerk / Auth.js](./guides/auth-provider-email.md). Using React Email components? See [Send React Email](./guides/react-email.md). **Hosted docs:** [docs.ranla.ai](https://docs.ranla.ai) **Dashboard:** [app.ranla.ai](https://app.ranla.ai) **API:** `https://api.ranla.ai` --- ## For AI agents - **Index:** [docs.ranla.ai/llms.txt](https://docs.ranla.ai/llms.txt) - **Full bundle:** [docs.ranla.ai/llms-full.txt](https://docs.ranla.ai/llms-full.txt) - **OpenAPI:** [docs.ranla.ai/openapi.yaml](https://docs.ranla.ai/openapi.yaml) - **Agent skill notes:** [docs.ranla.ai/ai/agent-skill](https://docs.ranla.ai/ai/agent-skill) - **MCP server:** [docs.ranla.ai/ai/mcp](https://docs.ranla.ai/ai/mcp) (`ranla-mcp` on npm) - **AI app builders:** [docs.ranla.ai/builders](https://docs.ranla.ai/builders) (Lovable, Replit, Bolt, Base44, v0 prompts) Agents need an API key from **Settings → API keys** in the dashboard (step 3), or they can connect the hosted MCP server with OAuth instead. --- ## 1. Create an account 1. Open the dashboard at [app.ranla.ai](https://app.ranla.ai). 2. Sign up with email + password (or email code if you prefer). 3. If using email code: enter the code emailed to you. --- ## 2. Send a sandbox test (fastest path) No domain or DNS required for the first sandbox send: create an API key (step 3), then send from the shared sandbox sender `noreply@mail.ranla.ai` to your account email — see **Sandbox self-test** in step 5. ### Use with Cursor or Claude 1. Create an API key in **Settings → API keys**, or skip the key and connect the hosted MCP server, which signs in with OAuth. 2. Add the `ranla-mcp` MCP server — see [MCP server](./ai/mcp.md). 3. Let the agent wire `POST /emails` (or the SDK) into your app and run the sandbox self-test. See [`docs/ai/agent-skill.md`](./ai/agent-skill.md) and [`docs/ai/mcp.md`](./ai/mcp.md). --- ## 3. Create an API key 1. Open **Settings → API keys** in the dashboard and click **Create API key** (or use an existing `rnl_…` key). One key works for the whole workspace: the sending API, the CLI, and the MCP server. 2. Copy the secret — it is shown **once**. ```bash export RANLA_API_KEY=rnl_your_key_here ``` --- ## 4. Verify a sending domain (DNS setup — self-test from your domain on free) Add and verify your domain on Sandbox to complete DNS setup. On the free plan you can **self-test from your verified domain to your account email** to confirm DNS and branding. **Free production** (send to other recipients) unlocks after a **verified domain and a payment method on file** — we do not charge until you upgrade. Caps: 3,000 emails/mo · 100/day · 1 domain. Upgrade to Pro or Scale for more volume. ### Dashboard (recommended) 1. Go to **Domains → Add domain** and enter your domain (e.g. `yourdomain.com`). 2. On the domain page (or in guided setup): - If your DNS is on Cloudflare: click **Sign in to Cloudflare** when offered — authorize once and we add the records. - Or **Settings → Integrations** → paste a Cloudflare API token (Zone DNS Edit) → Save, then **Apply DNS (Cloudflare)**. - If your DNS is at GoDaddy: switch to **GoDaddy**, paste an API key + secret for a one-time apply. - Otherwise paste the shown TXT records at your DNS host (merge SPF if you already have one). 3. Click **Verify DNS**. 4. Optional after verification: set **Delivery settings** on the domain detail page for: - open tracking - click tracking - `tls_mode` (`opportunistic` or `enforced`) 5. Check the built-in DMARC/BIMI guidance on the same page before you roll out branded inbox features. | Type | Purpose | |------|---------| | TXT `_supersendtx.yourdomain.com` | Domain ownership | | TXT `{selector}._domainkey.yourdomain.com` | DKIM (domain signing key) | | CNAME `rp.yourdomain.com` | Return path → `rp.supersendtx.com` | | TXT `yourdomain.com` | SPF (`include:spf.supersendtx.com`) | | TXT `_dmarc.yourdomain.com` | DMARC | Ownership, SPF, DKIM, and return-path are required to verify. Verify also confirms mail-server DKIM readiness so sends are signed with your domain (not the shared pool). Local Docker auto-verifies when `RANLA_DEV_AUTO_VERIFY=true`. If a domain is already attached to another Ranla team, `POST /domains` returns a 409 with claim instructions instead of creating a duplicate owner. ### CLI (from your editor / terminal) Save a Cloudflare token under **Settings → Integrations** first (recommended). Then: ```bash export RANLA_API_KEY=rnl_your_key_here npx -y --package=ranla-cli -- ranla domains add yourdomain.com # Uses the Cloudflare token stored in Ranla (no local CLOUDFLARE_API_TOKEN needed) npx -y --package=ranla-cli -- ranla domains apply yourdomain.com --provider cloudflare npx -y --package=ranla-cli -- ranla domains verify yourdomain.com ``` Optional override: set `CLOUDFLARE_API_TOKEN` locally to apply with a machine token instead of the dashboard one. Vercel uses `VERCEL_API_TOKEN` and optional `VERCEL_TEAM_ID` the same way. GoDaddy can use stored dashboard credentials or local `GODADDY_API_KEY` / `GODADDY_API_SECRET`. --- ## 5. Send from your app Pick one integration method below. Replace `you@yourdomain.com` with an address on your **verified** domain. ### Sandbox self-test (before DNS) Use the shared sandbox sender on `mail.ranla.ai`, but only send to **your own account email**: ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "noreply@mail.ranla.ai", "to": "you@example.com", "subject": "Sandbox check", "html": "

Ranla sandbox works.

" }' ``` Once your domain is verified, you can self-test from your own domain to your account email: ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "you@example.com", "subject": "Verified domain check", "html": "

My domain works.

" }' ``` Add a payment method at **Settings → Billing** to unlock Free production (send to other recipients). Upgrade to Pro or Scale for more volume. ### curl (production) ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello from Ranla", "html": "

It works.

" }' ``` ### Node.js SDK ```bash npm install @supersend/ranla ``` ```ts import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY!) const { id, status } = await client.emails.send({ from: 'you@yourdomain.com', to: 'user@example.com', subject: 'Hello from Ranla', html: '

It works.

', }) console.log(id, status) ``` ### CLI ```bash RANLA_API_KEY=$RANLA_API_KEY npx -y --package=ranla-cli -- ranla emails send \ --from you@yourdomain.com \ --to user@example.com \ --subject "Hello from Ranla" \ --html "

It works.

" ``` Or install globally: `npm install -g ranla-cli`, then run `@supersend/ranla emails send …`. --- ## Success response ```json { "id": "msg_abc123…", "status": "sent" } ``` --- ## Plan limits When billing is enabled, Sandbox is for integration and self-test sends (hard daily and monthly caps). Until Free production is unlocked, sends are limited to your account email — from the shared sandbox sender or from a verified domain. Unlock Free production with a verified domain **and** a payment method on file (no charge until you upgrade). Paid Pool and Dedicated plans meter overage past included volume. Over Free/Sandbox limit: ```json { "error": { "message": "…", "code": "plan_limit", "upgrade_url": "/settings/billing" } } ``` HTTP **429**. Upgrade in the dashboard under **Settings → Billing**. ## Common errors | HTTP | Meaning | Fix | |------|---------|-----| | 401 | Invalid or missing API key | Check `Authorization: Bearer rnl_…` | | 403 | From domain not verified, sandbox/verified-domain recipient mismatch, or send to other recipients without Free production unlocked | Verify domain; self-test to your account email; add a payment method at Settings → Billing to unlock Free production | | 400 | Missing `from`, `to`, or `subject` | Send required fields + `html` or `text` | | 429 | Plan send limit (`code: plan_limit`) | Upgrade at Settings → Billing | | 503 | Mail delivery temporarily unavailable | Retry later or contact support | Full reference: [docs.ranla.ai/api-reference](https://docs.ranla.ai/api-reference) --- ## 6. Webhooks (optional) 1. Go to **Webhooks** in the dashboard (or use the API). 2. Add your HTTPS endpoint URL. 3. Copy the signing secret (`whsec_…`) — shown once. 4. Verify `SuperSendTX-Signature` on incoming `POST`s. ```ts await client.webhooks.create({ url: 'https://yourapp.com/webhooks/supersendtx', }) ``` See [docs.ranla.ai/api-reference](https://docs.ranla.ai/api-reference) for payload shape and signature verification. --- ## 7. Receive email (optional) Want an app inbox or agent mailbox? 1. Create a dedicated inbound subdomain such as `inbound.example.com`. 2. Add it with `inbound_enabled: true` (API) or `supersendtx domains add inbound.example.com --inbound true` (CLI). 3. Verify the domain, then publish the returned MX record. 4. Subscribe to `email.received` and/or poll `GET /received-emails`. See [`docs/api/inbound.md`](./api/inbound.md) for the full flow and the MX conflict warning. --- ## 8. Event-driven automations (optional) Ranla can trigger transactional sequences from custom events. **Dashboard:** open **Automations**, create from a starter (or blank) on the visual canvas, configure steps in the side panel (send-email can use a published **Templates** entry or inline HTML), then Activate. **CLI / API:** create and activate from a JSON file (handy in Cursor or CI): ```bash supersendtx automations create --file ./welcome.json supersendtx automations activate --id "$AUTOMATION_ID" ``` Then send the trigger event from your app: ```bash curl -X POST https://api.ranla.ai/events \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: evt_user_created_123" \ -d '{ "name": "user.created", "user_id": "user_123", "email": "user@example.com", "data": { "name": "Ada Lovelace" } }' ``` Response: ```json { "event": { "id": "ae_123", "name": "user.created", "user_id": "user_123", "email": "user@example.com", "data": { "name": "Ada Lovelace" }, "source": "api", "created_at": "2026-07-26T12:00:00.000Z" }, "matched_automations": 1, "resumed_runs": 0, "cancelled_runs": 0 } ``` See [`docs/api/automations.md`](./api/automations.md) and [`docs/api/events.md`](./api/events.md). --- ## Next steps - Docs home: [docs.ranla.ai](https://docs.ranla.ai) - API reference: [docs.ranla.ai/api-reference](https://docs.ranla.ai/api-reference) - Next.js quickstart: [docs.ranla.ai/frameworks/nextjs](https://docs.ranla.ai/frameworks/nextjs) - React Email: [docs.ranla.ai/guides/react-email](https://docs.ranla.ai/guides/react-email) - Node.js quickstart: [docs.ranla.ai/frameworks/node](https://docs.ranla.ai/frameworks/node) - Send API: [`docs/api/emails.md`](./api/emails.md) - Domains API: [`docs/api/domains.md`](./api/domains.md) - Inbound email API: [`docs/api/inbound.md`](./api/inbound.md) - Deliverability: [`docs/api/deliverability.md`](./api/deliverability.md) - Events: [`docs/api/events.md`](./api/events.md) - Templates: [`docs/api/templates.md`](./api/templates.md) - Automations: [`docs/api/automations.md`](./api/automations.md) - SMTP relay: [`docs/api/smtp.md`](./api/smtp.md) - Webhooks: [`docs/api/webhooks.md`](./api/webhooks.md) - AI onboarding: [docs.ranla.ai/llms.txt](https://docs.ranla.ai/llms.txt), [docs.ranla.ai/llms-full.txt](https://docs.ranla.ai/llms-full.txt), [`docs/ai/agent-skill.md`](./ai/agent-skill.md), [`docs/ai/mcp.md`](./ai/mcp.md) (`ranla-mcp` npm package) - OpenAPI spec: [docs.ranla.ai/openapi.yaml](https://docs.ranla.ai/openapi.yaml) --- # Authentication Ranla authenticates API requests with Bearer API keys that start with `rnl_` or `stx_`. Newly created keys still start with `rnl_`. A `rnl_` key with the same secret after the prefix authenticates as the existing key. ## Obtain a key 1. Open the [dashboard](https://app.ranla.ai). 2. Go to **Settings → API keys** and click **Create API key**. One key works across the workspace: the sending API, the CLI, and the MCP server. 3. Copy the secret — it is shown **once**. ```bash export RANLA_API_KEY=rnl_your_key_here ``` Keep keys server-side only. Never ship them in browser bundles or public repos. ## Request header Send the key on every management and send request: ```bash curl https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello", "html": "

It works.

" }' ``` ### Node SDK ```javascript import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY) ``` ### CLI ```bash export RANLA_API_KEY=rnl_your_key_here npx -y --package=ranla-cli -- ranla emails send \ --from you@yourdomain.com \ --to user@example.com \ --subject "Hello" \ --html "

It works.

" ``` ## Base URLs | Surface | URL | |---------|-----| | API | `https://api.ranla.ai` | | Dashboard | `https://app.ranla.ai` | ## Unauthorized responses Missing or invalid keys return **401** with: ```json { "error": { "message": "Unauthorized" } } ``` See [Errors](/errors) for the full error shape. --- # Errors Ranla returns JSON errors that match the OpenAPI contract. ## Shape ```json { "error": { "message": "Human-readable summary", "details": {} } } ``` `details` is optional and may include field-level validation info. ## Common status codes | Status | When | |--------|------| | `400` | Invalid JSON or failed validation | | `401` | Missing/invalid `Authorization: Bearer rnl_…` | | `403` | Authenticated but not allowed (e.g. unverified sending domain) | | `404` | Resource not found | | `409` | Conflict (e.g. domain already claimed) | | `429` | Rate limited | | `502` | Upstream mail delivery failure | ## Examples ### Unverified domain (403) ```json { "error": { "message": "Sending domain is not verified" } } ``` ### Validation (400) ```json { "error": { "message": "Validation failed", "details": { "to": "Required" } } } ``` ## Idempotency Some write endpoints accept an `Idempotency-Key` header. Replays with the same key return the original success response instead of creating duplicates — see the [API Explorer](/api-explorer) and OpenAPI spec for which operations support it. --- # Migration from Resend Also migrating from other providers? - [Postmark](/migration/postmark) - [Amazon SES](/migration/ses) - [SendGrid](/migration/sendgrid) --- If you are moving an existing Resend integration, the Ranla send flow maps closely: - Bearer API keys - `POST /emails` with a familiar JSON body - Webhooks / activity for delivery outcomes The main migration steps are: 1. swap the base URL 2. swap your API key to `rnl_...` 3. verify a sending domain in Ranla 4. update any provider-specific optional fields --- ## Quick mapping | Resend | Ranla | |--------|---------------| | `https://api.resend.com` | `https://api.ranla.ai` | | `re_...` API key | `rnl_...` API key | | `POST /emails` | `POST /emails` | | `from` must be verified | `from` must be verified | | dashboard activity | dashboard activity + deliverability summary | --- ## Minimal curl diff ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello", "html": "

It works.

" }' ``` --- ## Accepted payload aliases Ranla accepts a few migration-friendly aliases on the HTTP send API: - `htmlBody` or `html_body` -> `html` - `textBody`, `text_body`, or `plain_body` -> `text` - `replyTo` -> `reply_to` - `scheduledAt` -> `scheduled_at` Attachment aliases are also accepted: - `filename` -> `name` - `contentType` -> `content_type` - `content` -> `data` - `contentId` -> `content_id` These aliases are meant to make low-friction migrations easier; new code should still prefer the canonical field names in docs/examples. --- ## Domain verification differences Unlike provider-managed sandbox products, Ranla expects you to verify your own sending domain for production traffic. Before DNS is ready, you can still self-test with the shared sandbox sender: ```json { "from": "noreply@mail.ranla.ai", "to": "you@example.com", "subject": "Sandbox check", "html": "

Sandbox works.

" } ``` Sandbox restriction: - the sandbox `from` domain is only allowed when all recipients match the account email on the API key owner Once your own domain is verified, switch `from` back to your branded domain. --- ## DNS apply Ranla can write DNS for you in two ways: - Cloudflare: save a token once under **Settings -> Integrations** - GoDaddy: paste one-time API credentials on the domain detail page, or pass them through the SDK/CLI locally See [`docs/api/domains.md`](./api/domains.md). --- ## SMTP If your old integration still depends on SMTP, create credentials in the dashboard (**SMTP**) or via `POST /smtp-credentials`, then connect to `smtp.supersendtx.com:587` with username `supersendtx`. Same verified domains and plan limits as the HTTP API. Prefer `POST /emails` for new app code. Details: [`docs/api/smtp.md`](./api/smtp.md). --- # Migration from Postmark Move transactional sends from Postmark to Ranla without changing your product’s email jobs — receipts, password resets, alerts, and invites still go out over `POST /emails`. --- ## Why teams switch - **HTTP API + npm SDK** with a short path from API key to first send - **Owned transactional mail infrastructure**, not a thin layer on rented cloud email - **Sandbox → Pool → Dedicated** so you can test free, run production on a paid shared transactional network, then isolate on managed servers and IPs when you need allowlists --- ## Quick mapping | Postmark | Ranla | |----------|--------------| | Server API token | `rnl_...` API key (`Authorization: Bearer`) | | `https://api.postmarkapp.com` | `https://api.ranla.ai` | | `POST /email` | `POST /emails` | | Verified Sender Signature / domain | Verified sending domain in Ranla | | Message Streams (Transactional) | Transactional-only product (no cold/marketing mix) | | Webhooks | Webhooks + dashboard activity | --- ## Minimal send ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello", "html": "

It works.

" }' ``` Node: ```bash npm install @supersend/ranla ``` ```js import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY) await client.emails.send({ from: 'you@yourdomain.com', to: 'user@example.com', subject: 'Hello', html: '

It works.

', }) ``` --- ## Migration checklist 1. Create a Ranla account and API key 2. Add and verify your sending domain ([Domains](/domains)) 3. Point your app’s send client at `https://api.ranla.ai` with `rnl_...` 4. Map Postmark fields: `From` → `from`, `To` → `to`, `Subject` → `subject`, `HtmlBody` → `html`, `TextBody` → `text`, `ReplyTo` → `reply_to` 5. Reconfigure webhooks to Ranla endpoints ([Webhooks](/webhooks)) 6. Send a canary through Sandbox or your verified domain, then cut over HTTP aliases accepted today: `htmlBody` / `html_body`, `textBody` / `text_body` / `plain_body`, `replyTo` → `reply_to`. Postmark’s PascalCase `HtmlBody` is **not** accepted — map it to `html` (or `htmlBody`) in your client. New code should use canonical names (`html`, `text`, `reply_to`). --- ## Domains and DNS Production `from` addresses must use a domain verified in Ranla. Cloudflare and GoDaddy apply flows can write records for you — see [Domains](/domains). Before DNS is ready, self-test with the shared sandbox sender on `mail.ranla.ai` (recipients limited to your account email). Details: [Quickstart](/quickstart). --- ## Related - [Migration from Resend](/migration) - [Migration from Amazon SES](/migration/ses) - [Quickstart](/quickstart) --- # Migration from Amazon SES If you send transactional mail through Amazon SES (SDK, SMTP, or a thin API on top of SES), you can move the application-facing send path to Ranla while keeping the same jobs: auth mail, receipts, alerts, and notifications. --- ## What changes | SES-oriented setup | Ranla | |--------------------|--------------| | AWS credentials / IAM | `rnl_...` API key | | Regional SES endpoint | `https://api.ranla.ai` | | `SendEmail` / `SendRawEmail` | `POST /emails` | | SES-verified identity | Ranla verified domain | | Configuration sets / event destinations | Webhooks + dashboard activity | | Shared SES IP pools (typical) | Pool (owned transactional network) or Dedicated (managed servers + IPs) | Ranla runs on **owned mail infrastructure**, not as a reseller of SES. That matters when you care who else shares the pipe — and when you later need Dedicated isolation. --- ## Minimal send ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello", "html": "

It works.

" }' ``` ```js import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY) await client.emails.send({ from: 'you@yourdomain.com', to: 'user@example.com', subject: 'Hello', html: '

It works.

', }) ``` --- ## Migration checklist 1. Sign up at [app.ranla.ai](https://app.ranla.ai) and create an API key 2. Verify your sending domain ([Domains](/domains)) — SPF, DKIM, return path 3. Replace SES SDK calls with Ranla HTTP or the `supersendtx` npm package 4. Map bodies: HTML → `html`, text → `text`, reply addresses → `reply_to` 5. Point bounce/complaint/delivery handling at Ranla [webhooks](/webhooks) 6. Run parallel canary sends, then shift production traffic --- ## Sandbox vs production - **Sandbox:** send from `noreply@mail.ranla.ai` to your account email only while integrating - **Free production:** verified domain **and** a payment method on file (no charge until you upgrade) — 3,000 emails/mo · 100/day · 1 domain on the shared transactional network (Pool) - **Pro (from $20/mo):** from 50,000 emails/mo · up to 10 domains · declining overage by tier - **Scale:** higher volume tiers with declining overage (up to 1,000 domains) - **Dedicated (from $299/mo):** managed server and IPs when you need isolation See [Pricing](https://ranla.ai/pricing) and [Quickstart](/quickstart). --- ## SMTP SES SMTP users can move to Ranla SMTP relay: host `smtp.supersendtx.com`, port `587` (STARTTLS), username `supersendtx`, password from a dashboard SMTP credential (`stxsmtp_…`). Same verified domains, plan limits, and suppressions as the HTTP API. Prefer `POST /emails` for new app code; use SMTP when the integration only exposes an SMTP form. Details: [SMTP](/smtp). --- ## Related - [Migration from Resend](/migration) - [Migration from Postmark](/migration/postmark) - [Quickstart](/quickstart) --- # Migration from SendGrid Move transactional sends from SendGrid to Ranla — receipts, password resets, alerts, and invites still go out over `POST /emails` with a simpler JSON body than SendGrid's v3 mail send payload. --- ## Why teams switch - **Straightforward HTTP API** — one `POST /emails` instead of nested `personalizations` / `content` arrays - **Owned transactional mail infrastructure**, separate from cold/outbound reputation - **Sandbox → Pool → Dedicated** — test for free, run production on a shared transactional network, isolate on managed servers when you need allowlists --- ## Quick mapping | SendGrid | Ranla | |----------|--------------| | `SG.` API key | `rnl_…` API key (`Authorization: Bearer`) | | `https://api.sendgrid.com/v3/mail/send` | `https://api.ranla.ai/emails` | | `personalizations[].to[]` | `to` (string or array) | | `from.email` + `from.name` | `from` (single address string) | | `content[]` with `type` / `value` | `html` and/or `text` | | Dynamic templates (`template_id`) | Template `alias` on send | | Event Webhook | Webhooks + dashboard activity | | ASM / unsubscribe groups | Managed unsubscribe + suppressions | --- ## Minimal send **SendGrid (v3):** ```bash curl -X POST https://api.sendgrid.com/v3/mail/send \ -H "Authorization: Bearer $SENDGRID_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "personalizations": [{ "to": [{ "email": "user@example.com" }] }], "from": { "email": "you@yourdomain.com" }, "subject": "Hello", "content": [{ "type": "text/html", "value": "

It works.

" }] }' ``` **Ranla:** ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello", "html": "

It works.

" }' ``` --- ## Node.js **SendGrid:** ```ts import sgMail from '@sendgrid/mail' sgMail.setApiKey(process.env.SENDGRID_API_KEY!) await sgMail.send({ to: 'user@example.com', from: 'you@yourdomain.com', subject: 'Hello', html: '

It works.

', }) ``` **Ranla:** ```ts import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY!) await client.emails.send({ from: 'you@yourdomain.com', to: 'user@example.com', subject: 'Hello', html: '

It works.

', }) ``` --- ## Webhooks SendGrid Event Webhook posts JSON arrays of events. Ranla sends one signed JSON object per delivery with `SuperSendTX-Signature` (HMAC-SHA256). Map event names as follows: | SendGrid event | Ranla | |----------------|--------------| | `processed` | `email.sent` | | `delivered` | `email.delivered` | | `deferred` | `email.delivery_delayed` | | `bounce` | `email.bounced` | | `dropped` | `email.failed` or `email.suppressed` | | `open` | `email.opened` | | `click` | `email.clicked` | | `spamreport` | `email.complained` | Use **Send test event** on the dashboard webhooks page (or `POST /emails/test`) to verify your handler before cutover. --- ## Checklist 1. Create a Ranla API key (`rnl_…`) 2. Add and verify your sending domain (SPF, DKIM, return-path) 3. Swap the client to `api.ranla.ai` and map fields per table above 4. Recreate webhook endpoints and update signature verification 5. Send from Sandbox, then production after domain verify --- ## Compare Product overview: [Transactional email on Ranla](https://ranla.ai/platform/transactional). --- # Migration from Rewardful Move an affiliate program from Rewardful to Ranla affiliates. Most of the move is a find-and-replace: links still use `?via=`, checkout still passes a referral id to Stripe, and commissions still come from your Stripe invoices. --- ## Quick mapping | Rewardful | Ranla | |-----------|--------------| | API Secret (HTTP Basic auth, secret as username) | `rnl_…` full-access API key (`Authorization: Bearer`) | | `https://api.getrewardful.com/v1` | `https://api.ranla.ai` | | Affiliate | Partner — `/affiliates` | | Campaign | Campaign — `/affiliate-campaigns` | | Affiliate link / `token` | Link — `/affiliates/{id}/links`, `token` | | Affiliate coupon | Per-partner promotion code | | Referral (`conversion_state`: `visitor`, `lead`, `conversion`) | Referral — `/affiliate-referrals`, `status` `lead` → `active`. Anonymous visitors are counted as clicks, not referrals. | | Commission (`state`: `pending`, `due`, `paid`, `voided`) | Commission — `/affiliate-commissions`, `status` `pending` → `earned` → `paid`, or `reversed` | | Payout | Payout — `/affiliate-payouts` | | Magic Link (SSO) | `POST /affiliates/{id}/login-link` | ## Browser script | Rewardful | Ranla | |-----------|--------------| | Queue snippet + ` ``` ### Several domains Marketing site on `yourapp.com` and checkout on `app.yourapp.io`? List the other domains and the script adds `?referral=` to links pointing there, so the visit carries over: ```html ``` ### Pages without a link To credit a partner on a page they did not link to (a dedicated landing page, a partner-specific pricing page), call: ```js stx.referralSource('jane') ``` --- ## 2. Record the signup After someone signs up, tell the script who they are. This turns the anonymous click into a **lead** tied to their email: ```js stx.identify({ email: user.email }) // or, the same thing under the name other affiliate tools use: stx.referralSignup(user.email) ``` Call it on signup (and on login, which is harmless). If the campaign pays a lead bounty, the partner earns it when this signup makes their first payment. No browser in the loop — a mobile app, an invite flow, a form handled entirely on your server? Record the referral with the API instead: ```bash curl -X POST https://api.ranla.ai/affiliate-referrals \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"token":"jane","email":"new-customer@example.com"}' ``` See [Affiliates API → Server-side referral tracking](/affiliates#server-side-referral-tracking). --- ## 3. Hand the referral to Stripe Once the person pays, Stripe is where the commission comes from. Any **one** of these links a Stripe customer to the referral. They are checked in this order, and the first partner to claim a customer keeps them. ### Stripe Payment Links — automatic Links to `https://buy.stripe.com/…` on a page with the script get `client_reference_id` added for you. Nothing to wire. ### Stripe Checkout — `client_reference_id` `window.stx.referral` holds the current referral id (an empty string when there is none). Pass it when you create the Checkout Session. Wait for it with `stx.ready`: ```js stx.ready(async () => { const res = await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ priceId, referral: window.stx.referral }), }) window.location = (await res.json()).url }) ``` ```ts // server const session = await stripe.checkout.sessions.create({ mode: 'subscription', line_items: [{ price: priceId, quantity: 1 }], client_reference_id: referral || undefined, success_url: 'https://yourapp.com/welcome', }) ``` ### Custom subscriptions — `metadata.ranla_referral` Creating customers or subscriptions yourself? Put the referral id (or the partner's link token) in metadata on the customer, subscription or invoice: ```ts await stripe.customers.create({ email, metadata: { ranla_referral: referral }, }) ``` ### Server-rendered forms — hidden inputs The script fills any input marked `data-stx-referral` with the referral id, so it arrives with the form post: ```html
``` Store it and pass it to Stripe as `client_reference_id` or `metadata.ranla_referral`. ### Same email If none of the above is present, a Stripe customer whose email matches a lead's email is attributed to that lead. ### Coupon codes Each partner can have their own promotion code (`JANE20`). A customer who pays with it is attributed to that partner even without a click — useful for podcasts, videos and offline mentions. On a page the partner linked to, `stx.coupon` holds the partner's code as `{ id, code, label }` (`stx.coupon.code` is the text to show) and `stx.affiliate` their name, so you can show "Jane's 20% discount is applied". --- ## Excluding payments Add `metadata.ranla = "false"` to a Stripe customer, subscription or invoice and it never earns a commission — for internal accounts, partner deals you pay separately, or refunds you settle by hand. --- ## What gets flagged A referral is still recorded, but earns nothing, when: - the person has the partner's own email, or the same company email domain (**self-referral**); - the customer is already attributed to another partner (**duplicate**); - the signup arrived after the cookie window ended (**expired**); - the partner is suspended. Flagged referrals show in the dashboard for review. --- ## Checking it works 1. Open `https://yourapp.com/?via=` in a private window. 2. In the console, `stx.referral` should be a non-empty id. 3. Sign up with a test email. The partner's page in the dashboard shows the lead within a few seconds. 4. Pay with a Stripe test card. The commission appears after the next Stripe sync (every 15 minutes), `pending` until its hold ends. Also see: [Affiliates API](/affiliates) · [Webhooks](/webhooks) --- # AI app builders Use Ranla in Lovable, Replit, Bolt, Base44, v0, and similar AI builders. **Two kinds of install surface:** 1. **Agent tools (MCP)** — the builder’s AI can call Ranla while you develop (Replit one-click MCP; Cursor/Claude via [MCP](../ai/mcp.md)). 2. **App runtime (Secrets + code)** — your deployed app sends mail via `RANLA_API_KEY` and `POST /emails` (or [SMTP](../api/smtp.md)). Most guides below cover (2). Replit also has a one-click path for (1). ## Pick your builder | Builder | Best for | Install surface | |---------|----------|-----------------| | [Lovable](/builders/lovable) | React + Supabase apps, chat-driven features | Secrets + agent prompt (HTTP or SMTP) | | [Replit](/builders/replit) | Full-stack Replit Agent projects | MCP badge + Secrets + agent prompt | | [Bolt.new](/builders/bolt) | Vite / React prototypes from chat | `.env` + agent prompt | | [Base44](/builders/base44) | Base44 apps with optional custom email | When to use Ranla vs built-in mail | | [v0](/builders/v0) | Next.js from Vercel v0 | Route Handler + Vercel env | ## Before you paste a prompt 1. Create a [Ranla account](https://app.ranla.ai) and an API key (`rnl_…`). 2. Store it as **`RANLA_API_KEY`** in the builder’s secrets / env UI (server-side only). 3. For a first send before your domain verifies, use the sandbox rules in [Quickstart](../quickstart.md) and [Agent skill notes](../ai/agent-skill.md). ## Standard env var ```bash RANLA_API_KEY=rnl_your_key_here ``` Some builders also accept `STX_API_KEY` — if their UI suggests that name, map it to the same `rnl_…` value. Prefer `RANLA_API_KEY` in generated code for consistency with our SDK and docs. ## Reference - [Agent skill notes](../ai/agent-skill.md) — integration defaults for AI agents - [OpenAPI](../openapi.yaml) — API contract - [Next.js](../frameworks/nextjs.md) — server-side send pattern - [Auth provider email](../guides/auth-provider-email.md) — Clerk / Auth.js hooks - [Supabase Auth email](../guides/supabase.md) — Send Email hook + Edge Function --- # Ranla + Lovable Lovable’s install surface is **Secrets + a chat prompt**: store your `rnl_` key, paste the prompt, and let Lovable wire server-side sends (often via Supabase Auth). There is no separate Lovable “connector package.” The agent writes the Edge Function / hook against `POST /emails` (or [SMTP](../api/smtp.md) if you prefer Supabase’s custom SMTP UI). ## When to use Ranla - Password reset, verification, receipts, and other **transactional** mail - Branded `from` on your own domain after DNS verify - Supabase Auth via a Send Email hook / Edge Function, or [custom SMTP](../api/smtp.md) with a Ranla credential Lovable projects often use Supabase — see [Supabase Auth email](../guides/supabase.md) for the hook path and the SMTP alternative. ## 1. Add your API key (Secrets) 1. Open [app.ranla.ai](https://app.ranla.ai) → **Settings → API keys** → create a key (`rnl_…`). 2. In Lovable, open your project **Settings** → **Secrets** (or the env/secrets panel Lovable exposes for backend code). 3. Add: ```text RANLA_API_KEY=rnl_your_key_here ``` Use the secret name exactly — server routes and Edge Functions should read `process.env.RANLA_API_KEY`. Never put the key in client-side React components. ## 2. Paste this prompt in Lovable chat ```text Integrate Ranla transactional email into this app. Docs (read first): - https://docs.ranla.ai/guides/supabase - https://docs.ranla.ai/ai/agent-skill - https://docs.ranla.ai/openapi.yaml - https://docs.ranla.ai/builders/lovable - https://docs.ranla.ai/smtp Rules: 1. Store the API key in RANLA_API_KEY (already in Lovable Secrets). Never expose rnl_ keys in the browser. 2. Prefer POST https://api.ranla.ai/emails with Authorization: Bearer $RANLA_API_KEY (Supabase Edge Function or server route). 3. Alternative: configure Supabase custom SMTP with Ranla (host smtp.supersendtx.com, username supersendtx, password from dashboard SMTP credentials). 4. Until domain verification and Free production unlock (both required: verified domain + payment method on file), sandbox sends must use from "noreply@mail.ranla.ai" and to only our account email. 5. Add a server-side send path for password-reset / verification email. 6. Use canonical fields: html, text, reply_to. Prefer npm package supersendtx if the backend is Node. After domain verify + payment method on file (Free production, no charge until upgrade), switch from to noreply@ourdomain.com. ``` Replace “our account email” with the email on your Ranla account if the agent asks. ## 3. Minimal server send (Node / Edge) ```ts const res = await fetch('https://api.ranla.ai/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RANLA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: 'noreply@yourdomain.com', to: 'user@example.com', subject: 'Verify your email', html: '

Click the link to verify.

', }), }) ``` Or install `supersendtx` and use the SDK — see [Next.js](../frameworks/nextjs.md). ## Sandbox self-test Send to your Ranla account email only, with `from: "noreply@mail.ranla.ai"`, until your domain is verified and Free production is unlocked (payment method on file): ```json { "from": "noreply@mail.ranla.ai", "to": "YOUR_ACCOUNT_EMAIL", "subject": "Ranla sandbox check", "html": "

It works.

" } ``` ## Production 1. Add your domain in the Ranla dashboard. 2. Apply DNS (Cloudflare integration or registrar credentials). 3. Verify, add a payment method to unlock Free production (no charge until you upgrade), then update `from` to an address on that domain. ## Optional: MCP while you build If you use Cursor (or another MCP client) alongside Lovable, connect hosted MCP at `https://mcp.ranla.ai/mcp` with OAuth — see [MCP server](../ai/mcp.md). That helps the coding agent; your Lovable app still needs Secrets (or SMTP) for runtime sends. ## Links - [Quickstart](../quickstart.md) - [Domains API](../api/domains.md) - [SMTP relay](../api/smtp.md) - [MCP server](../ai/mcp.md) - [Password reset guide](../guides/password-reset-emails.md) - [Supabase Auth email](../guides/supabase.md) --- # Ranla + Replit Two install surfaces, depending on the job: | Surface | Use when | |---------|----------| | **MCP (Agent tools)** | Replit Agent should call Ranla (send, domains, templates) while building | | **Secrets + app code** | Your running repl sends mail (password reset, invites, notifications) | Most projects use both: MCP while building, Secrets for production paths in the app. ## Add to Replit (MCP) One-click install of the hosted MCP server (`https://mcp.ranla.ai/mcp`). Replit uses OAuth — no API key in the link. [![Install Ranla](https://replit.com/badge?caption=Install%20Ranla)](https://replit.com/integrations?mcp=eyJkaXNwbGF5TmFtZSI6IlJhbmxhIiwiYmFzZVVybCI6Imh0dHBzOi8vbWNwLnJhbmxhLmFpL21jcCJ9) Or open: [Add Ranla MCP](https://replit.com/integrations?mcp=eyJkaXNwbGF5TmFtZSI6IlJhbmxhIiwiYmFzZVVybCI6Imh0dHBzOi8vbWNwLnJhbmxhLmFpL21jcCJ9) After install, authorize with your Ranla account when prompted. Full MCP docs: [MCP server](../ai/mcp.md). ## Wire email into your app (Secrets) ### 1. Add your API key 1. Create an API key at [app.ranla.ai](https://app.ranla.ai) (`rnl_…`). 2. In Replit, open **Tools** → **Secrets** (or **Integrations** → secrets, depending on Replit UI). 3. Create a secret: | Key | Value | |-----|-------| | `RANLA_API_KEY` | `rnl_your_key_here` | Replit injects secrets as environment variables. Access via `process.env.RANLA_API_KEY` (Node) or `os.environ["RANLA_API_KEY"]` (Python). Never expose the key to the client. ### 2. Paste this prompt in Replit Agent ```text Integrate Ranla transactional email into this Replit project. Docs: - https://docs.ranla.ai/ai/agent-skill - https://docs.ranla.ai/openapi.yaml - https://docs.ranla.ai/builders/replit - https://docs.ranla.ai/ai/mcp Rules: 1. Read RANLA_API_KEY from Replit Secrets (already configured). Never log or expose the key to the client. 2. Send with POST https://api.ranla.ai/emails and Authorization: Bearer . 3. Until domain verification and Free production unlock (both required: verified domain + payment method on file), use from "noreply@mail.ranla.ai" and send only to the Ranla account email. 4. Implement server-side email for at least one flow (welcome, reset, or notification). 5. Use html and text fields; prefer supersendtx npm package on Node. For production: verify a domain, add a payment method to unlock Free production (no charge until you upgrade), then set from to an address on that domain. Upgrade to Pro or Scale for more volume. ``` ### 3. Minimal Express route (Node) ```js import express from 'express' const app = express() app.use(express.json()) app.post('/api/send-test', async (req, res) => { const apiKey = process.env.RANLA_API_KEY if (!apiKey) return res.status(500).json({ error: 'Missing RANLA_API_KEY' }) const upstream = await fetch('https://api.ranla.ai/emails', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(req.body), }) const data = await upstream.json() res.status(upstream.status).json(data) }) ``` ## Sandbox self-test Send to your Ranla account email only, with `from: "noreply@mail.ranla.ai"`, until your domain is verified and Free production is unlocked (payment method on file). ## Production 1. Verify a sending domain in Ranla. 2. Add a payment method to unlock Free production (no charge until you upgrade), or upgrade for higher volume. 3. Set `from` to `noreply@yourdomain.com` (or similar on your verified domain). ## Links - [MCP server](../ai/mcp.md) - [Quickstart](../quickstart.md) - [Node.js](../frameworks/node.md) - [Errors](../errors.md) --- # Ranla + Bolt.new Add transactional email to a Bolt-generated app with a `.env` secret and a chat prompt. Bolt often outputs Vite or React projects — keep sends on the server. ## When to use Ranla - Bolt prototypes that need real password-reset or notification email - Moving from demo to production with your own sending domain - Replacing a `RESEND_API_KEY` pattern with Ranla (`RANLA_API_KEY`) ## 1. Add your API key 1. Get a key from [app.ranla.ai](https://app.ranla.ai). 2. In the Bolt project, add to **`.env`** (local) and your host’s env UI when you deploy: ```bash RANLA_API_KEY=rnl_your_key_here ``` 3. Ensure `.env` is in `.gitignore` — never commit `rnl_` keys. If Bolt deploys to a platform with its own env panel (Netlify, Vercel, etc.), add the same variable there for production. ## 2. Paste this prompt in Bolt chat ```text Integrate Ranla transactional email into this project. Docs: - https://docs.ranla.ai/ai/agent-skill - https://docs.ranla.ai/openapi.yaml - https://docs.ranla.ai/builders/bolt Rules: 1. Use RANLA_API_KEY from .env on the server only — never in client bundles or VITE_ prefixed vars. 2. POST https://api.ranla.ai/emails with Authorization: Bearer and JSON body (from, to, subject, html). 3. Until domain verification and Free production unlock (verified domain + payment method on file), sandbox from "noreply@mail.ranla.ai", to account email only. 4. Add a small API route or serverless function for transactional sends; do not call the API from the browser. 5. If this is Next.js, follow the App Router server pattern in https://docs.ranla.ai/frameworks/nextjs After domain verify + payment method on file, use from on our verified domain for Free production. Upgrade for more volume. ``` ## 3. Server-only pattern Bolt may generate a Vite SPA. Add a server endpoint (or deploy a small API route) that proxies to Ranla: ```ts // Example: server handler (adapt to your Bolt stack) export async function sendTransactionalEmail(payload: { to: string subject: string html: string }) { const res = await fetch('https://api.ranla.ai/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RANLA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: process.env.MAIL_FROM ?? 'noreply@yourdomain.com', ...payload, }), }) if (!res.ok) throw new Error(await res.text()) return res.json() } ``` ## Sandbox Use `noreply@mail.ranla.ai` and your account email for the first test — see [Quickstart](../quickstart.md). ## Links - [Migration from Resend](../migration.md) — field mapping if you started with Resend env names - [Next.js](../frameworks/nextjs.md) --- # Ranla + Base44 Base44 apps can send email through built-in platform features. Use Ranla when you need **your own domain**, higher control over deliverability, or the same transactional stack as the rest of your product. ## When Base44 built-in email is enough - Early prototypes and internal tools - Low volume without a custom domain requirement - Flows where Base44’s default sender is acceptable ## When to add Ranla - Branded mail from `@yourdomain.com` after DNS verify - Password reset / auth mail you want on owned transactional infrastructure - Webhooks and delivery visibility via Ranla dashboard - Consistent API across non-Base44 services (same `POST /emails` everywhere) ## 1. Add your API key 1. Create `rnl_…` at [app.ranla.ai](https://app.ranla.ai). 2. In Base44, add a project secret or environment variable (exact UI varies by Base44 project type): ```text RANLA_API_KEY=rnl_your_key_here ``` Wire server-side code or automation steps to read that variable — never expose it in client-visible config. ## 2. Paste this prompt in Base44 ```text Add Ranla for transactional email in this Base44 app (instead of or alongside built-in email). Docs: - https://docs.ranla.ai/ai/agent-skill - https://docs.ranla.ai/openapi.yaml - https://docs.ranla.ai/builders/base44 Rules: 1. Use RANLA_API_KEY from project secrets — server-side only. 2. POST https://api.ranla.ai/emails with Bearer auth for transactional sends (reset, verify, receipts). 3. Sandbox until domain verify: from "noreply@mail.ranla.ai", to Ranla account email only. 4. Explain when built-in Base44 email is still fine vs when Ranla is required (custom domain, production volume). 5. Use html/text; verify domain in Ranla before production from addresses. Link users to https://app.ranla.ai for domain setup. ``` ## 3. HTTP send ```json POST https://api.ranla.ai/emails Authorization: Bearer rnl_… Content-Type: application/json { "from": "noreply@yourdomain.com", "to": "user@example.com", "subject": "Your receipt", "html": "

Thanks for your order.

" } ``` ## Production checklist - [ ] Domain added and verified in Ranla - [ ] `from` uses verified domain - [ ] API key only in secrets, not in frontend - [ ] Bounce/complaint handling via [webhooks](../api/webhooks.md) if needed ## Links - [Quickstart](../quickstart.md) - [Domains](../api/domains.md) - [Auth provider email](../guides/auth-provider-email.md) --- # Ranla + v0 (Vercel) v0 generates Next.js apps. Run Ranla from a **Route Handler or Server Action** and store `RANLA_API_KEY` in Vercel environment variables. ## When to use Ranla - v0 apps deployed to Vercel that need password reset, magic links, or receipts - Server Components / Route Handlers — same pattern as [Next.js](../frameworks/nextjs.md) - Production sends from your verified domain ## 1. Add your API key **Local** — `.env.local`: ```bash RANLA_API_KEY=rnl_your_key_here ``` **Vercel** — Project → **Settings** → **Environment Variables**: | Name | Environments | |------|----------------| | `RANLA_API_KEY` | Production, Preview, Development | Redeploy after adding the variable. ## 2. Paste this prompt in v0 chat ```text Integrate Ranla transactional email into this Next.js app. Docs: - https://docs.ranla.ai/ai/agent-skill - https://docs.ranla.ai/frameworks/nextjs - https://docs.ranla.ai/openapi.yaml - https://docs.ranla.ai/builders/v0 Rules: 1. RANLA_API_KEY is server-only (process.env) — never use NEXT_PUBLIC_ for the API key. 2. Send via POST https://api.ranla.ai/emails or npm package supersendtx from a Route Handler / Server Action. 3. Sandbox: from "noreply@mail.ranla.ai", to account email only until domain verify. 4. Add app/api/... route or server action for at least one transactional email flow. 5. Use html, text, reply_to. Follow Next.js server patterns — no client-side rnl_ keys. Document that Vercel env var RANLA_API_KEY must be set before deploy. ``` ## 3. Route Handler example Create `app/api/email/send/route.ts`: ```ts import { NextResponse } from 'next/server' import { Ranla } from '@supersend/ranla' const tx = new Ranla(process.env.RANLA_API_KEY!) export async function POST(request: Request) { const body = await request.json() const result = await tx.emails.send({ from: body.from ?? 'noreply@yourdomain.com', to: body.to, subject: body.subject, html: body.html, text: body.text, }) return NextResponse.json(result) } ``` Install: `npm install @supersend/ranla` ## Sandbox Until your domain verifies, test with the sandbox sender and your Ranla account email — see [Agent skill notes](../ai/agent-skill.md). ## Production 1. Verify domain in Ranla dashboard. 2. Set production `from` to your domain. 3. Confirm `RANLA_API_KEY` is set on Vercel Production. ## Links - [Next.js guide](../frameworks/nextjs.md) - [Quickstart](../quickstart.md) - [OpenAPI](../openapi.yaml) --- # Emails Send transactional email and list recent sends. Migrating from Resend? See [`docs/migration.md`](../migration.md). --- ## Send email `POST /emails` Send a transactional email from a verified domain. Base URL: `https://api.ranla.ai` --- ## Authentication ```http Authorization: Bearer rnl_… ``` API keys are created in the dashboard (**Settings → 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`: ```json { "error": { "message": "One or more recipients opted out of this email category", "code": "validation_error", "details": { "category_unsubscribed": ["user@example.com"], "category": "product", "email_id": "msg_…" } } } ``` Global suppressions return the same status with `details.suppressed` instead. ```json { "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Product updates", "html": "

Hi there.

Unsubscribe

", "unsubscribe": true } ``` ### Example ```json { "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello from Ranla", "html": "

It works.

" } ``` With display name: ```json { "from": { "email": "you@yourdomain.com", "name": "Your App" }, "to": ["user@example.com", "other@example.com"], "subject": "Hello", "html": "

Hi

", "text": "Hi", "reply_to": ["support@yourdomain.com"], "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: ```json { "from": "noreply@mail.ranla.ai", "to": "owner@example.com", "subject": "Sandbox check", "html": "

Sandbox works.

" } ``` 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: ```json { "from": "you@yourdomain.com", "to": "owner@example.com", "subject": "Verified domain check", "html": "

My domain works.

" } ``` 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 ```json { "id": "msg_abc123…", "status": "sent" } ``` Scheduled sends return: ```json { "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`: ```http 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`. If the mail server was unavailable and did not take the message (**502** or **503**), the key is released, so retrying with the same key sends the email. If the mail server could not confirm the send (**504**: it timed out, the connection dropped, or the reply was unreadable), the email may still be delivered. The key keeps that response, so a retry with the same key returns the 504 instead of risking a duplicate. The email is recorded as `failed` because delivery was not confirmed, so resend with a new key only if a duplicate is acceptable. ```ts await client.emails.send({ …, idempotencyKey: 'order-123' }) ``` --- ## Retrieve email `GET /emails/{id}` Returns one send by public id (`msg_…`). ```ts 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: ```ts await client.emails.resend('msg_abc123…') ``` CLI: ```bash 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: ```json { "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Tomorrow", "html": "

See you tomorrow.

", "scheduled_at": "2026-08-01T12:00:00.000Z" } ``` Scheduled emails can be updated only while their status is `scheduled`: ```http PATCH /emails/msg_abc123… ``` ```json { "scheduled_at": "2026-08-01T13:00:00.000Z" } ``` Cancel a scheduled email: ```json { "cancel": true } ``` SDK: ```ts await client.emails.update('msg_abc123…', { scheduledAt: '2026-08-01T13:00:00.000Z' }) await client.emails.cancel('msg_abc123…') ``` CLI: ```bash 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: ```json { "emails": [ { "from": "you@yourdomain.com", "to": "user@example.com", "subject": "Hello", "html": "

Hi

" } ] } ``` The response is index-aligned: ```json { "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: ```ts 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 ```json { "emails": [ { "id": "msg_abc123…", "from": "you@yourdomain.com", "to": ["user@example.com"], "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 ```ts const { emails, next_cursor } = await client.emails.list({ limit: 10 }) ``` ### Error format All errors return: ```json { "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 | | 422 | Recipient suppressed or opted out, or the mail server refused the message (`upstream_error`) | | 429 | Free plan send limit (`plan_limit`) | | 502 | Upstream mail delivery error | | 503 | Mail delivery temporarily unavailable | | 504 | Mail server could not confirm the send; the email may still be delivered | ### 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 ```bash curl -X POST https://api.ranla.ai/emails \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from":"you@yourdomain.com","to":"user@example.com","subject":"Hello","html":"

Hi

"}' ``` ### Node.js ```ts import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY!) await client.emails.send({ from: 'you@yourdomain.com', to: 'user@example.com', subject: 'Hello', html: '

Hi

', }) ``` Throws `SuperSendTXError` on failure (`error.status`, `error.message`, `error.details`). ### CLI ```bash supersendtx emails send \ --from you@yourdomain.com \ --to user@example.com \ --subject "Hello" \ --html "

Hi

" \ --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`](../../openapi/supersendtx.yaml) --- # Domains API Manage sending domains, DNS verification, inbound receiving flags, tracking preferences, TLS mode, one-click DNS apply, and DMARC/BIMI guidance. Base URL: `https://api.ranla.ai` Auth: ```http Authorization: Bearer rnl_... ``` --- ## Create a domain `POST /domains` ```bash curl -X POST https://api.ranla.ai/domains \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "example.com" }' ``` Success response: ```json { "domain": { "id": "d_123", "name": "example.com", "status": "pending", "created_at": "2026-07-26T00:00:00.000Z", "verified_at": null, "open_tracking": false, "click_tracking": false, "tls_mode": "opportunistic" }, "records": [ { "type": "TXT", "host": "_supersendtx.example.com", "value": "supersendtx-verify=...", "purpose": "Domain ownership" } ] } ``` To request inbound receiving during creation, include `"inbound_enabled": true`. The response will also include `inbound_records` (MX) and `inbound_note`. Full receiving flow: [`docs/api/inbound.md`](./inbound.md). Expected DNS records (after create syncs with the mail server): - ownership TXT: `_supersendtx.example.com` - DKIM TXT: selector host such as `ss-abc123._domainkey.example.com` with the real public key - return-path CNAME: `rp.example.com` → `rp.supersendtx.com` (DNS only / not proxied) - SPF include: `include:spf.supersendtx.com` - DMARC starter record Ranla registers the domain on the transactional mail server at create time and replaces placeholder DKIM with the real signing records before apply/verify. ### Cross-team claim conflict If the domain is already attached to another Ranla team, the API returns **409** with claim instructions: ```json { "error": { "message": "Domain already belongs to another Ranla team. Remove it from the current team or contact support to claim it.", "code": "conflict", "details": { "reason": "domain_claim_required", "domain": "example.com", "instructions": [ "Ask the current Ranla team admin to remove the domain if you still have access.", "If you own the domain but no longer control the original team, contact support and include proof of DNS control." ] } } } ``` --- ## Get a domain `GET /domains/{id}` Returns the domain, required DNS records, inbound MX guidance, and best-effort DMARC/BIMI analysis: ```json { "domain": { "id": "d_123", "name": "example.com", "status": "verified", "created_at": "2026-07-26T00:00:00.000Z", "verified_at": "2026-07-26T00:05:00.000Z", "open_tracking": true, "click_tracking": false, "tls_mode": "enforced", "inbound_enabled": true, "inbound_status": "active", "inbound_error": null }, "records": [], "inbound_records": [ { "type": "MX", "host": "example.com", "priority": 10, "value": "mx.supersendtx.com", "purpose": "Inbound receiving" } ], "inbound_note": "Inbound receiving requires pointing this hostname's MX record to our mail servers. That conflicts with any existing mailbox provider on the same hostname, so use a dedicated subdomain such as inbound.example.com when Google Workspace or Microsoft 365 should keep handling your main mailboxes.", "analysis": { "dmarc": { "host": "_dmarc.example.com", "configured": true, "record": "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com", "policy": "quarantine", "rua": ["mailto:dmarc@example.com"], "guidance": "DMARC is enforced. Keep report mailboxes monitored and tighten alignment as you roll out BIMI." }, "bimi": { "host": "default._bimi.example.com", "configured": false, "record": null, "location": null, "authority": null, "requires_dmarc_enforcement": false, "guidance": "Optional: add a BIMI record at default._bimi once your branded SVG logo and optional VMC are ready." } } } ``` ### DMARC/BIMI notes - **DMARC** is recommended for every sending domain. - **BIMI** is optional, but most mailbox providers expect DMARC enforcement (`p=quarantine` or `p=reject`) before BIMI is effective. - `analysis` is advisory. It does not block sending, and it does not replace your own deliverability monitoring. --- ## Verify DNS `POST /domains/{id}` ```json { "action": "verify" } ``` When ownership, SPF, DKIM, and the return-path CNAME are visible in public DNS, Ranla asks the mail server to re-check delivery DNS and marks the domain verified only when mail-server **DKIM** and **return-path** statuses are OK. That step activates customer-domain DKIM signing (`d=yourdomain`) so DMARC can pass. Without DKIM OK, mail would fall back to the shared pool signer. DMARC is reported back but does not currently block verification. Re-running verify on an already-verified domain re-checks mail-server DKIM (useful after DNS propagation). Success responses include: ```json { "verified": true, "signing_ready": true, "mail_server_signing": { "dkim_ok": true, "return_path_ok": true, "dkim_status": "OK", "return_path_status": "OK" } } ``` If public DNS looks correct but the mail server has not yet accepted the DKIM TXT or return-path CNAME, verify returns **400** with `mail_server_signing.dkim_ok` / `return_path_ok` reflecting the failure — wait for propagation and verify again. --- ## Apply DNS `POST /domains/{id}` ### Cloudflare via stored dashboard token ```json { "action": "apply", "provider": "cloudflare" } ``` ### Cloudflare via inline credentials ```json { "action": "apply", "provider": "cloudflare", "credentials": { "apiToken": "cf_...", "zoneId": "..." } } ``` ### GoDaddy via one-time credentials ```json { "action": "apply", "provider": "godaddy", "credentials": { "apiKey": "gd_key", "apiSecret": "gd_secret" } } ``` You can also save GoDaddy credentials under **Settings → Integrations** and omit `credentials` on apply. ### Vercel via stored dashboard token ```json { "action": "apply", "provider": "vercel" } ``` The domain must use **Vercel nameservers**. For team-scoped domains, connect Vercel under **Settings → Integrations** with an optional team id, or pass `credentials.teamId` / `credentials.vercelTeamId` inline. ### Vercel via inline credentials ```json { "action": "apply", "provider": "vercel", "credentials": { "apiToken": "vercel_...", "teamId": "team_..." } } ``` Response shape: ```json { "provider": "godaddy", "domain": "example.com", "results": [ { "purpose": "SPF", "host": "example.com", "action": "merged", "detail": "v=spf1 include:spf.supersendtx.com ~all" } ] } ``` --- ## Update tracking, inbound, and TLS preferences `PATCH /domains/{id}` ```bash curl -X PATCH https://api.ranla.ai/domains/example.com \ -H "Authorization: Bearer $RANLA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "inbound_enabled": true, "open_tracking": true, "click_tracking": true, "tls_mode": "enforced" }' ``` Aliases `openTracking`, `clickTracking`, `tlsMode`, and `inboundEnabled` are also accepted. ### Inbound enablement - `inbound_enabled: true` marks the domain for receiving. - Before verification, inbound stays `pending`. - After verification, Ranla activates inbound receiving and updates `inbound_status` to `active` or `error`. - See [`docs/api/inbound.md`](./inbound.md) for received-email retrieval and `email.received` webhooks. ### TLS modes - `opportunistic` — use TLS when the downstream mail path supports it. - `enforced` — require TLS for the downstream mail path. ### Tracking and TLS preferences `open_tracking`, `click_tracking`, and `tls_mode` are stored on the domain immediately when you update them. When open or click tracking is enabled: 1. Ranla provisions a tracking domain for your hostname. 2. The domain detail response includes a **tracking CNAME** (`ps.yourdomain.com` → `track.supersendtx.com`). 3. Apply and verify DNS so the mail server can rewrite links and inject open pixels. 4. `tracking_dns.ok` in `GET /domains/{id}` reports whether the CNAME is live. If delivery-side provisioning fails, settings are still saved but `tracking_error` or `tls_error` may be returned on `PATCH` with a customer-facing message (not raw infrastructure errors). --- ## SDK ```ts import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY!) await client.domains.create({ name: 'example.com' }) await client.domains.apply('example.com') await client.domains.apply('example.com', { provider: 'vercel' }) await client.domains.apply('example.com', { provider: 'godaddy', credentials: { apiKey: process.env.GODADDY_API_KEY!, apiSecret: process.env.GODADDY_API_SECRET!, }, }) await client.domains.verify('example.com') const detail = await client.domains.get('example.com') const updated = await client.domains.update('example.com', { open_tracking: true, tls_mode: 'enforced', }) ``` --- ## CLI ```bash supersendtx domains add example.com supersendtx domains apply example.com --provider cloudflare supersendtx domains apply example.com --provider vercel GODADDY_API_KEY=... GODADDY_API_SECRET=... supersendtx domains apply example.com --provider godaddy supersendtx domains verify example.com ``` --- ## Delete a domain `DELETE /domains/{id}` Removes the domain from your account and revokes sending authorization for that hostname. ```bash supersendtx domains delete example.com ``` If upstream teardown fails, the domain is kept and the API returns **502**. --- ## Common errors | Status | Meaning | |--------|---------| | 400 | invalid domain, DNS not ready, missing provider credentials | | 401 | missing or invalid auth | | 403 | domain limit reached | | 404 | domain not found | | 409 | domain already exists or is already claimed | | 502 | Domain registration, teardown, or DNS provider API failed | Full contract: [`openapi/supersendtx.yaml`](../../openapi/supersendtx.yaml) --- # Webhooks Receive signed HTTP callbacks for email lifecycle events. Base URL: `https://api.ranla.ai` Manage endpoints in the dashboard (**Webhooks**) or via the API / SDK / CLI below. --- ## Authentication ```http Authorization: Bearer rnl_… ``` Dashboard session cookies also work for browser requests to `/api/webhooks`. --- ## Supported events | Event | When fired | |-------|------------| | `email.received` | An inbound email arrived on a receive-enabled domain; retrieve full content from `/received-emails/{id}` | | `email.sent` | Ranla accepted the message for delivery | | `email.delivered` | The message was delivered to the recipient mailbox provider | | `email.delivery_delayed` | Delivery was delayed or temporarily held | | `email.bounced` | Hard/soft bounce; recipient is auto-suppressed | | `email.complained` | Recipient marked the message as spam/complaint | | `email.opened` | Open pixel loaded when open tracking is on | | `email.clicked` | Tracked link clicked when click tracking is on | | `email.failed` | Delivery failed; permanent SMTP failures also auto-suppress the recipient | | `email.suppressed` | Send blocked because a recipient is on the suppression list | | `email.scheduled` | Email accepted with `scheduled_at` | | `contact.unsubscribed` | Recipient completed a managed unsubscribe. `data.scope` is `all` (global suppression) or `category` (product/newsletter opt-out only) | | `automation.started` | An active automation matched an incoming event and a run was created | | `automation.step_completed` | One automation step completed successfully | | `automation.completed` | A run finished all steps (or ended early on a false condition) | | `automation.failed` | A run failed (send failure, timeout, validation, etc.) | | `affiliate.created` | A partner joined, was invited, imported or created over the API | | `affiliate.approved` | A partner became active | | `affiliate.updated` | A partner's profile, campaign, payout method or status changed | | `referral.created` | A partner's referral was recorded as a lead | | `referral.converted` | A referral became a paying customer | | `commission.created` | A commission was written (pending during its hold) | | `commission.earned` | A commission's hold ended | | `commission.reversed` | A commission was reversed (refund, dispute or by hand) | | `commission.paid` | A commission was paid in a payout | | `payout.created` | A partner payout was drafted | | `payout.paid` | A partner payout went out | | `payout.failed` | A partner payout failed | Open/click tracking are domain settings (`open_tracking` / `click_tracking`) configured per domain. Affiliate events and their payloads are described in [Affiliates → Webhooks](/affiliates#webhooks). --- ## Create endpoint `POST /webhooks` ```json { "url": "https://yourapp.com/webhooks/supersendtx", "events": ["email.delivered", "email.bounced"] } ``` `events` is optional — defaults to all supported events. ### 200 — Created ```json { "webhook": { "id": "clx…", "url": "https://yourapp.com/webhooks/supersendtx", "events": ["email.delivered", "email.bounced"], "enabled": true, "created_at": "2026-07-25T12:00:00.000Z", "updated_at": "2026-07-25T12:00:00.000Z" }, "secret": "whsec_…" } ``` Copy `secret` immediately — it is only returned once. Use it to verify incoming webhook signatures. --- ## List endpoints `GET /webhooks` Cursor pagination: `limit`, `cursor` → `{ webhooks, has_more, next_cursor }`. --- ## Retrieve endpoint `GET /webhooks/{id}` ```json { "webhook": { "id": "clx…", "url": "https://yourapp.com/webhooks/supersendtx", "events": ["email.delivered", "email.bounced"], "enabled": true, "created_at": "2026-07-25T12:00:00.000Z", "updated_at": "2026-07-25T12:00:00.000Z" } } ``` --- ## Update endpoint `PATCH /webhooks/{id}` ```json { "url": "https://yourapp.com/new-path", "events": ["email.bounced"], "enabled": false } ``` All fields are optional. --- ## Delete endpoint `DELETE /webhooks/{id}` ```json { "ok": true } ``` --- ## Test sink `POST /emails/test` (full-scope API key) ```json { "event": "email.bounced", "email_id": "msg_…", "deliver": true } ``` Builds a webhook payload (optionally for an existing email) and, when `deliver` is not `false`, enqueues delivery to your subscribed endpoints. Useful in CI. --- ## Incoming webhook payload Ranla `POST`s JSON to your endpoint URL when a subscribed event occurs. ### Email event example ```json { "type": "email.received", "created_at": "2026-07-25T12:05:00.000Z", "data": { "email_id": "inb_abc123…", "to": ["sales@inbound.example.com"], "from": "Sender ", "subject": "Hello", "status": "received", "rcpt_to": "sales@inbound.example.com", "cc": [], "message_id": "", "in_reply_to": null, "references": [], "spam_status": "NotSpam", "attachment_count": 1, "attachments": [ { "filename": "invoice.pdf", "content_type": "application/pdf", "size": 48291 } ], "received_at": "2026-07-25T12:04:59.000Z" } } ``` For inbound events, attachment metadata is included but attachment bytes are not — fetch `/received-emails/{id}` for full bodies and base64 attachment data. For bounces, `type` is `email.bounced`, `status` is `bounced`, and `bounce_reason` may contain a provider reason string. ### Automation event example ```json { "type": "automation.step_completed", "created_at": "2026-07-26T12:05:00.000Z", "data": { "automation_id": "a1b2c3", "automation_name": "Welcome flow", "run_id": "r1s2t3", "status": "running", "event_name": "user.created", "step_index": 0, "step_type": "send_email", "error": null } } ``` ### Contact unsubscribe examples Global unsubscribe (`scope: "all"`) adds the address to suppressions and blocks every send: ```json { "type": "contact.unsubscribed", "created_at": "2026-08-02T12:05:00.000Z", "data": { "email": "user@example.com", "source": "unsubscribe", "scope": "all", "category": null, "category_label": null, "suppression_id": "sup_…", "category_opt_out_id": null, "email_id": "msg_…" } } ``` Category opt-out (`scope: "category"`) stops product or newsletter mail only — transactional messages still send: ```json { "type": "contact.unsubscribed", "created_at": "2026-08-02T12:05:00.000Z", "data": { "email": "user@example.com", "source": "unsubscribe", "scope": "category", "category": "product", "category_label": "product emails", "suppression_id": null, "category_opt_out_id": "opt_…", "email_id": "msg_…" } } ``` Check `data.scope` (and `data.category` when scope is `category`) before treating the address as fully suppressed. --- ## Signature verification Each delivery includes: ```http SuperSendTX-Signature: t=1721908800,v1=abc123… ``` ### SDK helper (preferred) ```ts import { Ranla, verifyWebhookSignature } from '@supersend/ranla' const ok = verifyWebhookSignature(secret, rawBody, request.headers.get('supersendtx-signature')) // or: const client = new Ranla(process.env.RANLA_API_KEY!) client.webhooks.verify(secret, rawBody, header) ``` Verify by: 1. Read the raw request body as a string (before JSON parsing). 2. Parse `t` (Unix timestamp) and `v1` (hex HMAC) from the header. 3. Reject if `t` is more than 5 minutes from your server clock. 4. Compute `HMAC-SHA256(secret, "${t}.${rawBody}")` and compare to `v1` (constant-time). Return `2xx` quickly from your handler. Failed deliveries are retried at **0s, 5s, 30s, 2m and 2m**. --- ## SDK ```ts import { Ranla } from '@supersend/ranla' const client = new Ranla(process.env.RANLA_API_KEY!) const { webhook, secret } = await client.webhooks.create({ url: 'https://yourapp.com/webhooks/supersendtx', }) const { webhooks } = await client.webhooks.list() const one = await client.webhooks.get(webhooks[0].id) await client.webhooks.update(one.webhook.id, { enabled: false }) await client.webhooks.delete(webhooks[0].id) ``` --- ## CLI ```bash supersendtx webhooks list supersendtx webhooks create --url https://yourapp.com/hooks supersendtx webhooks delete --id clx… ``` Requires `RANLA_API_KEY` or `--api-key`. --- ## OpenAPI Machine-readable spec: [`openapi/supersendtx.yaml`](../../openapi/supersendtx.yaml) --- # Templates Create reusable email templates with `{{variable}}` placeholders, then send with `POST /emails` using `template: { id, variables }`. Base URL: `https://api.ranla.ai` --- ## Authentication ```http Authorization: Bearer rnl_… ``` Requires a **full**-scope API key (or dashboard session). Sending-scoped keys cannot manage templates. --- ## Model | Field | Notes | |-------|--------| | `name` | Required; unique per account | | `alias` | Optional unique handle (not empty, not starting with `rnl_` or `stx_`) | | `status` | `draft` (default) or `published` | | `format` | `html` (legacy) or `blocks` (visual builder document) | | `subject` / `html` / `text` | Support `{{var}}` placeholders | | `preheader` | Optional inbox preview text | | `design` | TemplateDocument JSON when `format` is `blocks` | | `variables` | Variable names (auto-extracted for blocks templates) | | `published_version_id` | Immutable version used for sends | | `unsubscribe_enabled` | When `true`, sends using this template inherit managed unsubscribe unless overridden per send | | `category` | `transactional` (default), `product`, or `newsletter`. Product and newsletter support category-scoped opt-out | | `warnings` | Non-blocking deliverability hints for `html` (e.g. SVG logos) | Placeholders in **HTML** bodies are HTML-escaped when interpolated. Subject and text are not. Publishing snapshots an immutable version. Sends always resolve `published_version_id`, so draft edits never break live aliases. See [docs/templates/builder.md](../templates/builder.md). ### Global CSS (`blocks` templates) `design.settings.globalCss` accepts ordinary CSS that is compiled for the inbox on render: - Rules whose selector is a bare tag (`body`, `p`, `h1`, `h2`, `h3`, `a`, `img`, `hr`) become **inline styles** on those elements and override per-block styling. Inlining matters because Gmail drops `