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>
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.
| Field | Type | Description |
|---|---|---|
| to required | string[] | Recipient email addresses |
| subject required | string | Email subject line |
| html | string | HTML body content |
| text | string | Plain text body |
| from_addr | string | Sender. Default: no-reply@sinnerops.fyi |
| cc | string[] | Carbon copy recipients |
| bcc | string[] | Blind carbon copy |
| reply_to | string | Reply-To address |
| template_id | string | Template ID or slug |
| template_vars | object | Variables for template substitution |
| attachments | object[] | [{filename, content (base64), content_type}] |
| tags | object | Custom 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
| HTTP | Meaning | Common Cause |
|---|---|---|
| 400 | Bad Request | Missing required field, invalid email, password too short |
| 401 | Unauthorized | Missing, invalid, or revoked API key |
| 404 | Not Found | Email ID or template slug doesn't exist |
| 409 | Conflict | Template slug already exists |
| 500 | Server Error | SMTP failure — check /health |
error response
{ "detail": "Provide html, text, or template_id" }