SinnerOps Mail API

A self-hosted transactional email API. Send, template, and track emails from any project with a single HTTP call or SMTP connection.

Base URL https://mail-api.sinnerops.fyi
Authentication
All API endpoints require a Bearer token. Pass your API key in the Authorization header or as X-API-Key.
Authorization: Bearer <your_api_key>
or  X-API-Key: <your_api_key>
shell
# Send your API key in the header
curl https://mail-api.sinnerops.fyi/v1/emails \
  -H "Authorization: Bearer so_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{...}'
Quick Start

Send your first email in 30 seconds

Create an API key from the admin panel, then hit the send endpoint.

curl
curl -X POST https://mail-api.sinnerops.fyi/v1/emails \
  -H "Authorization: Bearer so_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["user@example.com"],
    "subject": "Hello from SinnerOps",
    "html": "<h1>It works!</h1><p>Your mail server is live.</p>",
    "text": "It works! Your mail server is live."
  }'
python
import requests

resp = requests.post(
    "https://mail-api.sinnerops.fyi/v1/emails",
    headers={"Authorization": "Bearer so_YOUR_KEY"},
    json={
        "to": ["user@example.com"],
        "subject": "Hello!",
        "html": "<p>Hello from my app!</p>",
    }
)
print(resp.json())  # {"id": "abc123", "object": "email"}
node.js
const res = await fetch('https://mail-api.sinnerops.fyi/v1/emails', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer so_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: ['user@example.com'],
    subject: 'Hello!',
    html: '<p>Hello from Node!</p>',
  }),
});
const data = await res.json(); // { id: 'abc123' }
Send Email
POST /v1/emails
Send a transactional email. Supports HTML, plain text, attachments, templates, and variable substitution. DKIM-signed automatically.
FieldTypeDescription
to requiredstring[]Recipient email addresses
subject requiredstringEmail subject line
htmlstringHTML body content
textstringPlain text body
from_addrstringSender. Default: no-reply@sinnerops.fyi
ccstring[]Carbon copy recipients
bccstring[]Blind carbon copy
reply_tostringReply-To address
template_idstringTemplate ID or slug
template_varsobjectVariables for template substitution
attachmentsobject[][{filename, content (base64), content_type}]
tagsobjectCustom key-value metadata
request
{
  "from_addr": "noreply@sinnerops.fyi",
  "to": ["alice@example.com"],
  "cc": ["bob@example.com"],
  "subject": "Your order is confirmed",
  "html": "<h1>Thanks Alice!</h1><p>Order #1234 confirmed.</p>",
  "text": "Thanks Alice! Order #1234 confirmed.",
  "attachments": [{
    "filename": "receipt.pdf",
    "content_type": "application/pdf",
    "content": "<base64-encoded-data>"
  }],
  "tags": { "order_id": "1234", "project": "shop" }
}
response 201
{
  "id": "3f8a91c24d7b",
  "object": "email"
}
List Emails
GET/v1/emails?page=1&limit=50&status=sent
List emails sent via this API key, newest first. Filter by status: queued · sent · failed.
response
{
  "data": [{
    "id": "3f8a91c24d7b",
    "from": "noreply@sinnerops.fyi",
    "to": ["alice@example.com"],
    "subject": "Your order is confirmed",
    "status": "sent",
    "created_at": "2026-11-02T12:00:00Z",
    "sent_at": "2026-11-02T12:00:01Z"
  }],
  "total": 142,
  "page": 1
}
Get Email
GET/v1/emails/{id}
Retrieve a single email and its delivery status.
Batch Send
POST/v1/emails/batch
Send up to 100 emails in one request. The request body is an array of email objects.
request
[
  { "to": ["a@example.com"], "subject": "Welcome A", "html": "<p>Hi A!</p>" },
  { "to": ["b@example.com"], "subject": "Welcome B", "html": "<p>Hi B!</p>" }
]
Templates
Store reusable HTML templates with variable substitution using ${variable} syntax (Python string.Template). Reference by ID or slug.
POST/v1/templates

Create Template

request
{
  "name": "Welcome Email",
  "slug": "welcome",
  "subject": "Welcome to ${app_name}, ${name}!",
  "html_body": "<h1>Hi ${name}!</h1><p>Welcome to ${app_name}.</p><a href='${cta_url}'>Get started</a>",
  "text_body": "Hi ${name}! Welcome to ${app_name}. Get started: ${cta_url}"
}
POST/v1/emails

Use Template

request
{
  "to": ["newuser@example.com"],
  "subject": "(overridden by template)",
  "template_id": "welcome",
  "template_vars": {
    "name": "Alice",
    "app_name": "MyApp",
    "cta_url": "https://myapp.com/onboard"
  }
}
GET/v1/templates

List

GET/v1/templates/{id_or_slug}

Get

PUT/v1/templates/{id_or_slug}

Update

DEL/v1/templates/{id_or_slug}

Delete

API Keys
Issue separate keys per project. Keys are shown only once on creation — store them securely.
POST/v1/api-keys
request
{ "name": "charon-app", "daily_limit": 500 }
response 201
{
  "id": "a1b2c3d4e5f6",
  "name": "charon-app",
  "token": "so_Abc123...",    // ← save this, shown once
  "prefix": "so_Abc123"
}
GET/v1/api-keys

List Keys

DEL/v1/api-keys/{id}

Revoke

Webhooks
Receive delivery events (delivered, bounced, deferred) as HTTP POST callbacks to your endpoint. Optionally sign payloads with a secret.
POST/v1/webhooks
request
{
  "url": "https://myapp.com/webhooks/mail",
  "events": ["delivered", "bounced"],
  "secret": "my_webhook_secret"
}
payload delivered to your endpoint
{
  "event": "delivered",
  "timestamp": "2026-11-02T13:00:00Z",
  "status": "sent",
  "info": "250 OK queued as ...",
  "line": "... postfix/smtp: status=sent ..."
}
Stats
GET/v1/stats
response
{
  "total": 1420,
  "sent": 1390,
  "failed": 12,
  "queued": 18,
  "today": 47,
  "delivery_rate": 97.9
}
Health
GET/health

— No auth required

response
{ "status": "ok", "smtp": true, "domain": "sinnerops.fyi", "version": "1.0.0" }
Direct SMTP
Your mail server also accepts standard SMTP for clients that prefer it over the REST API.
SMTP settings
Host:       smtp.sinnerops.fyi  (or mail.sinnerops.fyi)
Port:       587  (STARTTLS)   or   465  (SSL/TLS)
Username:   no-reply@sinnerops.fyi
Password:   MailApi!2026xK9
Encryption: STARTTLS / TLS
python smtplib
import smtplib
from email.mime.text import MIMEText

msg = MIMEText("Hello world", "html")
msg["From"] = "no-reply@sinnerops.fyi"
msg["To"]   = "user@example.com"
msg["Subject"] = "Test"

with smtplib.SMTP("smtp.sinnerops.fyi", 587) as s:
    s.starttls()
    s.login("no-reply@sinnerops.fyi", "MailApi!2026xK9")
    s.send_message(msg)
node.js · nodemailer
const nodemailer = require('nodemailer');
const transport = nodemailer.createTransport({
  host: 'smtp.sinnerops.fyi', port: 587, secure: false,
  auth: { user: 'no-reply@sinnerops.fyi', pass: 'MailApi!2026xK9' }
});
await transport.sendMail({
  from: 'no-reply@sinnerops.fyi',
  to: 'user@example.com',
  subject: 'Hello',
  html: '<p>Hello world</p>'
});
Error Codes
HTTPMeaningCommon Cause
400Bad RequestMissing required field, invalid email, password too short
401UnauthorizedMissing, invalid, or revoked API key
404Not FoundEmail ID or template slug doesn't exist
409ConflictTemplate slug already exists
500Server ErrorSMTP failure — check /health
error response
{ "detail": "Provide html, text, or template_id" }