SM

SwayamMail

Developer Docs

Documentation

Ship reliable email in days, not weeks.

Public developer documentation for integrating with the SwayamMail API. Learn how to create API keys, authenticate requests, and send email safely using sandbox and production keys.

Base URL: http://localhost:3000
Version: v0.4 beta

Overview

SwayamMail exposes a developer API for sending email. Use API keys in the x-api-key header and choose between sandbox and production keys depending on your testing needs.

Auth

API keys via x-api-key header.

Verification

Production keys require a verified domain and approved From address.

Sandbox

Test keys mark jobs delivered without touching SES.

Quickstart

  1. Step 1

    Create a sending domain and publish DNS records.

  2. Step 2

    Approve From addresses under the verified domain.

  3. Step 3

    Issue API keys for sandbox and production.

  4. Step 4

    Send your first email using POST /v1/send.

Authentication

Developer API

Authenticate by sending your API key in the header.

x-api-key: sk_test_...

API keys

Create up to 5 active keys per tenant in the dashboard. Keys are shown once on creation and stored as hashes.

Create key

Go to the Dashboard → API keys to create a new key.

sk_test_... / sk_prod_...

List keys

Manage keys inside the dashboard UI.

Revoke key

Use the dashboard to revoke a compromised key.

Preview

Store only the preview in logs.

...x9k2

Send emails

Send to direct emails, contact IDs, or list IDs. You must provide at least one recipient array.

POST /v1/send

{
  "from": "hello@yourdomain.in",
  "subject": "Welcome to SwayamMail",
  "html": "<h1>Hello there</h1>",
  "toEmails": ["customer@example.com"],
  "contactIds": [],
  "listIds": []
}

Response

{
  "accepted": 1,
  "jobIds": ["f7d..."],
  "publishedJobs": []
}

Headers

x-api-key: sk_prod_...

Transactional email API

For reports, invoices and other one-to-one mail sent from your own app or dashboard. You send ready-made HTML; no unsubscribe footer is added and links are not rewritten. Opens are tracked if your plan includes it (set track_opens: false to skip). Files are sent as links and shown as attachment cards at the bottom of the email. Send an Idempotency-Keyheader so a retried request never sends twice.

POST /v1/emails
x-api-key: sk_prod_...
Idempotency-Key: report-acme-2026-10

{
  "from": "Agency Reports <reports@yourdomain.in>",
  "to": ["client@acme.com"],
  "cc": ["pm@yourdomain.in"],
  "reply_to": "pm@yourdomain.in",
  "subject": "Your October report",
  "html": "<h1>October at a glance</h1>...",
  "text": "October at a glance...",
  "attachments": [
    { "filename": "Invoice-Oct.pdf", "url": "https://files.yourdomain.in/inv-oct.pdf" }
  ],
  "tags": { "client": "acme", "kind": "monthly-report" }
}

Response (202)

{
  "id": "f7d1...",
  "status": "queued",
  "to": ["client@acme.com"],
  "subject": "Your October report",
  "tags": { "client": "acme" }
}

Other endpoints

POST /v1/emails/batch
{ "emails": [ {...}, {...} ] }   // up to 100

GET /v1/emails/:id
// status, timestamps and event history

Limits: 50 recipients across to/cc/bcc, 200 KB of HTML, 10 attachment links, 10 tags. Reusing an Idempotency-Key with the same body returns the original email (HTTP 200); with a different body it returns 409. In a batch, item i uses key <key>:i.

Email OTP

Send and check one-time login or signup codes without building any of it yourself. We generate the code, email it from a fast lane that never waits behind campaigns, and check it for you. Codes are stored only as hashes, work once, expire after 10 minutes and lock after 5 wrong tries (all adjustable in OTP settings). The email is plain, with the code in the subject and no tracking.

POST /v1/otp/send
x-api-key: sk_prod_...

{
  "email": "user@example.com",
  "ip": "203.0.113.7",          // your user's IP: enables per-IP limits
  "captcha_token": "0.Xa...",   // when CAPTCHA is on in OTP settings
  "purpose": "login"            // optional label
}

202 { "id": "8c1e...", "email": "user@example.com", "expires_at": "2026-10-30T10:10:00Z" }
POST /v1/otp/verify
x-api-key: sk_prod_...

{ "email": "user@example.com", "code": "482913", "id": "8c1e..." }   // id is optional

200 { "valid": true, "id": "8c1e...", "email": "user@example.com" }
200 { "valid": false, "reason": "invalid", "attempts_remaining": 3 }
// reason: invalid | expired | too_many_attempts | already_used | superseded | not_found

Sender: codes come from the address set as from_email in OTP settings (use a verified subdomain such as otp.yourbrand.com), or by default from noreply@yourcompany.<our OTP domain>. Limits: one code per address per minute and 5 per hour, 20 per IP per hour, and your account's per-minute rate. Asking for a new code cancels the previous one. Sandbox keys send nothing and return the code in the response so you can test your flow. If too many of your codes bounce, OTP sending pauses until support reviews the account, so protect public forms with CAPTCHA (Cloudflare Turnstile or hCaptcha).

Webhooks

Set one URL per account under Webhooks in the dashboard. Events: email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked, email.failed. Respond with any 2xx within 10 seconds. Other responses are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h.

POST https://your-app.com/webhooks/swayam
Swayam-Event: email.delivered
Swayam-Delivery-Id: 0b6e...
Swayam-Signature: t=1761815000,v1=5f2c...

{
  "id": "0b6e...",
  "type": "email.delivered",
  "created_at": "2026-10-30T09:30:00Z",
  "data": {
    "email_id": "f7d1...",
    "from": "reports@yourdomain.in",
    "to": ["client@acme.com"],
    "subject": "Your October report",
    "email_type": "transactional",
    "tags": { "client": "acme" },
    "details": { "email": "client@acme.com" }
  }
}

Verify the signature before trusting a request (Node.js):

const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET)
  .update(`${t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
const valid = fresh && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));

Delivery status

Check a single send by job ID. Responses omit HTML for privacy.

GET /v1/sends/:jobId

Response

{
  "jobId": "uuid",
  "status": "delivered",
  "to": "user@example.com",
  "from": "hello@yourdomain.in",
  "subject": "Welcome",
  "createdAt": "2026-04-29T13:00:00Z",
  "sentAt": "2026-04-29T13:00:02Z",
  "failedAt": null
}

List sends

Fetch recent sends with filters. Supports status, to, from, subject, startDate, endDate, page, and limit.

GET /v1/sends?status=delivered&limit=20&page=1

Response

{
  "page": 1,
  "limit": 20,
  "total": 120,
  "items": [
    {
      "jobId": "uuid",
      "status": "queued",
      "to": "user@example.com",
      "from": "hello@yourdomain.in",
      "subject": "Welcome",
      "createdAt": "2026-04-29T13:00:00Z",
      "sentAt": null,
      "failedAt": null
    }
  ]
}

Sandbox behavior

sk_test_

Jobs are marked delivered immediately. No SQS, no SES.

sandbox: true

sk_prod_

Jobs are queued and delivered via SES.

status: queued

Errors

Errors return JSON with an error field and optional details.

{
  "error": "Invalid payload",
  "issues": {
    "fieldErrors": {
      "from": ["Invalid email"]
    }
  }
}

Rate limits

Beta limits are generous. If you receive 429, reduce send throughput or reach out for a higher allowance.

Changelog

April 2026

Added API keys with sandbox mode and production prefixes.

March 2026

Domain verification flow and sending addresses released.