# rail > Transactional email server with mTLS authentication rail is a pure-Go email server for service-to-service transactional email (notifications, alerts, receipts). Not for bulk marketing. ## Auth mTLS client certificates. No API keys. The certificate encodes: - CN: client identity - Email SANs: allowed sender addresses - URI SANs: webhook URLs for inbound delivery ## Send email POST /api/v1/send (mTLS required) ```json { "from": "agent@smtp.ataca.io", "to": ["user@example.com"], "subject": "Hello", "body_text": "Plain text body", "body_html": "

HTML body

" } ``` Response: `{"id": "01jx...", "recipients": 1, "status": "queued"}` Optional fields: request_id, cc, reply_to, unsubscribe_url, body_html. When body_html is omitted, rail auto-generates an HTML alternative from body_text (multipart/alternative). Also accepts SMTP on port 465 (implicit TLS, same mTLS cert). ## Receive email Inbound mail to local domains is POSTed to webhook URLs as JSON: ```json { "type": "inbound", "message_id": "01jx...", "from": "sender@example.com", "to": ["agent@smtp.ataca.io"], "subject": "Re: Hello", "raw_message": "", "received_at": "2025-01-15T10:30:00Z" } ``` Reply tracking: omit reply_to and rail injects a VERP address. Replies arrive as type "reply" with in_reply_to set to the original message ID. Hard bounces of messages you sent arrive as type "bounce" (no raw_message; message_id is a unique bounce-event id, original_message_id correlates), with bounced_recipient, bounce_status (e.g. "5.1.1"), bounce_diagnostic, and bounce_type "hard". The bounced recipient is also auto-suppressed. Soft bounces do not fire a webhook. Verified spam complaints against messages you sent arrive as type "complaint" (no raw_message; message_id is a unique complaint-event id, original_message_id correlates), with complained_recipient, feedback_type (e.g. "abuse"), and complaint_source "arf". Fires when an ARF feedback report to rail's abuse address MAC-verifies against the message's VERP tags; the complained recipient is also auto-suppressed. Unverified reports and plain abuse mail are recorded for review only and do not fire a webhook. ## Form submissions (public, opt-in) POST /f/{token} — no client certificate. Accepts HTML form posts (urlencoded or multipart) for a pre-configured form target and delivers them as email to its recipients. Special fields: _replyto, _subject, _redirect (origin-allowlisted), _gotcha (honeypot — leave empty). Send "Accept: application/json" for a JSON response `{"ok":true,"id":"01jx..."}`; browser posts get a 303 redirect. Only available when the operator enabled forms. Targets are created with `rail forms create` or self-served over mTLS: POST /api/v1/forms {"name","from","to","subject","redirect_url", "allowed_origins","rate_limit","cc_submitter","field_labels"} → 201 with the token; GET /api/v1/forms lists your targets; GET/PATCH/DELETE /api/v1/forms/{token} (another client's token is 404). PATCH takes the same field set as POST, all optional: an absent field keeps its current value, an explicit ""/[]/{} clears it; from/token/client_id are immutable and ignored if sent. cc_submitter: true CC's the submitter's email/_replyto address a copy (rate-limited per address, suppression-checked). field_labels maps a submitted field name to a display label (operator-only, set at create time); unlisted fields auto-prettify (first_name -> First Name). When the operator configures forms captcha, a spam-likely submission is answered with a challenge instead of being accepted: JSON callers get 403 {"ok":false,"error":"challenge_required","challenge":{"provider":"turnstile", "site_key","nonce"}} — render the Turnstile widget, then re-POST the same fields plus _challenge (nonce) and cf-turnstile-response (widget token). Browser posts get a 200 HTML interstitial hosting the widget. Every accepted submission (one that produced an outbound email — honeypot, challenged, rate-limited, and all-suppressed submissions are never stored) is persisted: GET /api/v1/forms/{token}/submissions lists them newest-first (?before= keyset cursor, ?limit= default 50/max 100), and GET /api/v1/forms/{token}/submissions/{id} fetches one. Both are owner-scoped like the form target itself — 404 for a token or id you don't own. ## Check delivery status GET /api/v1/messages/{id}/deliveries (mTLS required) Returns every envelope recipient. Per-recipient status: pending, delivered, bounced, or untracked (no delivery record, e.g. a local-domain recipient; carries a note and attempts: 0). ## Endpoints - POST /api/v1/send — send email (mTLS) - GET /api/who — inspect your client cert (mTLS) - GET /api/v1/messages/{id}/deliveries — delivery status (mTLS) - POST /f/{token} — public form submission (opt-in) - POST/GET /api/v1/forms, GET/PATCH/DELETE /api/v1/forms/{token} — manage form targets (mTLS) - GET /api/v1/forms/{token}/submissions, GET .../submissions/{id} — persisted form submissions (mTLS) - GET /healthz — liveness - GET /readyz — readiness - GET /stats — relay statistics (JSON) - GET /dashboard — browser dashboard (opt-in; password login, not for agents) - GET /openapi.yaml — OpenAPI 3.1 spec - GET /agents — human-readable agent integration guide - GET /.well-known/ai-plugin.json — AI plugin manifest ## Links - Agent guide: https://smtp.ataca.io/agents - API docs: https://smtp.ataca.io/docs - OpenAPI spec: https://smtp.ataca.io/openapi.yaml