agent integration
Operational
Agent Integration Guide
How AI agents send and receive email via rail. Authentication is mTLS client certificates — no API keys, no OAuth, no shared secrets. See also: API docs · OpenAPI spec · ai-plugin.json
Setup
rail certs issue \
  --name my-agent \
  --email agent@smtp.ataca.io \
  --webhook https://my-agent.example.com/hooks/email
FieldMeaning
CNAgent identity — appears in logs, rate-limit buckets
Email SANsAddresses the agent is allowed to send as
URI SANsWebhook URLs where inbound mail is POSTed
Sending Email — HTTP API (recommended)
curl -s --cert my-agent.crt --key my-agent.key --cacert ca.crt \
  https://smtp.ataca.io/api/v1/send \
  -H "Content-Type: application/json" \
  -d '{
    "from": "agent@smtp.ataca.io",
    "to": ["user@example.com"],
    "subject": "Task complete",
    "body_text": "Your report is ready.",
    "body_html": "<p>Your report is <b>ready</b>.</p>"
  }'
# Response (200):
{"id": "01jx...", "recipients": 1, "status": "queued"}
FieldPurpose
request_idIdempotency key, echoed in response
ccCC recipients (array)
reply_toOverride Reply-To (disables VERP tracking)
unsubscribe_urlInjects List-Unsubscribe per RFC 8058
import requests
resp = requests.post(
  "https://smtp.ataca.io/api/v1/send",
  cert=("my-agent.crt", "my-agent.key"),
  verify="ca.crt",
  json={
    "from": "agent@smtp.ataca.io",
    "to": ["user@example.com"],
    "subject": "Task complete",
    "body_text": "Your report is ready.",
  },
)
resp.raise_for_status()
print(resp.json()) # {"id": "01jx...", "recipients": 1, "status": "queued"}
import { readFileSync } from "fs";
import https from "https";
const resp = await fetch("https://smtp.ataca.io/api/v1/send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    from: "agent@smtp.ataca.io",
    to: ["user@example.com"],
    subject: "Task complete",
    body_text: "Your report is ready.",
  }),
  agent: new https.Agent({
    cert: readFileSync("my-agent.crt"),
    key: readFileSync("my-agent.key"),
    ca: readFileSync("ca.crt"),
  }),
});
console.log(await resp.json());
cert, _ := tls.LoadX509KeyPair("my-agent.crt", "my-agent.key")
caPEM, _ := os.ReadFile("ca.crt")
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caPEM)
client := &http.Client{Transport: &http.Transport{
  TLSClientConfig: &tls.Config{
    Certificates: []tls.Certificate{cert},
    RootCAs: pool,
  },
}}
body := `{"from":"agent@smtp.ataca.io","to":["user@example.com"],` +
  `"subject":"Task complete","body_text":"Your report is ready."}`
resp, _ := client.Post("https://smtp.ataca.io/api/v1/send",
  "application/json", strings.NewReader(body))
Sending Email — SMTP (port 465)
import smtplib, ssl
from email.message import EmailMessage
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_cert_chain("my-agent.crt", "my-agent.key")
ctx.load_verify_locations("ca.crt")
msg = EmailMessage()
msg["From"] = "agent@smtp.ataca.io"
msg["To"] = "user@example.com"
msg["Subject"] = "Task complete"
msg.set_content("Your report is ready.")
with smtplib.SMTP_SSL("smtp.ataca.io", 465, context=ctx) as smtp:
  smtp.send_message(msg)
Receiving Email
Sender internet MTA rail :25 (STARTTLS) local domain check HTTP POST to your webhook
{
  "type": "inbound",
  "message_id": "01jx...",
  "from": "user@example.com",
  "to": ["agent@smtp.ataca.io"],
  "subject": "Re: Task complete",
  "raw_message": "<base64-encoded RFC 5322 message>",
  "received_at": "2025-01-15T10:30:00Z"
}
{
  "type": "reply",
  "message_id": "01jy...",
  "in_reply_to": "01jx...", // original message ID
  "from": "user@example.com",
  "to": ["agent@smtp.ataca.io"],
  "subject": "Re: Task complete",
  "raw_message": "<base64>",
  "received_at": "2025-01-15T10:30:00Z",
  "original_sender": "agent@smtp.ataca.io",
  "original_recipients": ["user@example.com"]
}
{
  "type": "bounce",
  "message_id": "01jx...", // the ORIGINAL message that bounced
  "received_at": "2025-01-15T10:30:00Z",
  "original_message_id": "01jw...",
  "bounced_recipient": "user@example.com",
  "bounce_status": "5.1.1",
  "bounce_diagnostic": "smtp; 550 5.1.1 User unknown",
  "bounce_type": "hard"
}
// Only hard (5.x.x) bounces fire a webhook; the recipient is also auto-suppressed.
// Soft bounces are retried by rail and send no webhook.
from flask import Flask, request
app = Flask(__name__)
@app.route("/hooks/email", methods=["POST"])
def receive_email():
  payload = request.json
  print(f"From: {payload['from']}, Subject: {payload['subject']}")
  if payload["type"] == "reply":
    print(f"Reply to: {payload['in_reply_to']}")
  elif payload["type"] == "bounce":
    print(f"Bounced: {payload['bounced_recipient']} ({payload['bounce_status']})")
  return "", 200 # 2xx = delivered; 4xx/5xx = rail retries
import express from "express";
const app = express();
app.use(express.json());
app.post("/hooks/email", (req, res) => {
  const { type, from, subject, in_reply_to } = req.body;
  console.log(`From: ${from}, Subject: ${subject}`);
  if (type === "reply") console.log(`Reply to: ${in_reply_to}`);
  res.sendStatus(200);
});
app.listen(3000);
Reply Tracking (VERP)
# When your agent sends without setting reply_to, rail injects:
Reply-To: reply+<msg_id>.<hmac>@smtp.ataca.io
# When the recipient replies:
# 1. Rail receives the reply on :25
# 2. Matches it to the original message via the signed tag
# 3. Delivers to your webhook with type:"reply" and in_reply_to set
# To disable tracking for a specific message, set reply_to explicitly:
{"reply_to": "support@example.com", ...}
Checking Delivery Status
curl -s --cert my-agent.crt --key my-agent.key --cacert ca.crt \
  https://smtp.ataca.io/api/v1/messages/01jx.../deliveries
# Returns per-recipient outcomes:
# delivered, bounced, or deferred — with timestamps and error details
Best Practices
#Practice
1Use the HTTP API — simpler than SMTP, better error reporting, JSON native
2Check response status — 2xx = queued; 4xx = retry later; 5xx = do not retry
3Include both body_text and body_html for maximum mail client compatibility
4Handle webhook retries — return 2xx promptly; rail retries on failure
5Use request_id for idempotent retries on the send side
6Add unsubscribe_url for user-facing mail (required by Gmail/Yahoo)
7Don't send bulk marketing — rail is for transactional mail only
API Discovery
EndpointFormatPurpose
/openapi.yamlOpenAPI 3.1Machine-readable API spec
/.well-known/ai-plugin.jsonJSONAI agent plugin manifest
/llms.txtPlain textLLM-readable service summary