SMTP / HTTP API

文档

快速开始

  1. 创建账号并验证注册邮箱。验证前不能发信或创建凭据;验证后 T0 仅可使用已验证的发件域名免费向本账号注册邮箱发送。对外发信需要审核通过并订阅套餐。
  2. 在控制台添加域名,发布返回的 DKIM TXT、Return-Path CNAME,并设置有效的 DMARC TXT 策略。使用接口返回的准确 selector、公钥与 Return-Path 目标。检查 Return-Path 目标的 SPF,不要在同一名称下添加第二条 SPF 记录。域名验证返回 dkim_ok、return_path_ok 和 dmarc_ok,没有单独的 SPF 标志。
  3. 创建绑定 transactional 或 marketing 发件流的 SMTP 账号或 API Key,立即保存仅展示一次的密钥。提交发信用途申请;审核通过后 T1 每日 1,000 封、每秒 2 封起步。连续 14 个健康日(退信 < 2%、投诉 < 0.1%)额度翻倍;有发信但不健康的日子清零进度,没有发信的日子不增加进度。

USDT 充值在早期阶段人工入账,请联系 postmaster@fays.si。审核不承诺完成时长。

SMTP

连接 smtp.fays.si:587,使用 STARTTLS 并验证服务端证书,再通过 AUTH 和控制台凭据鉴权。587 使用显式 STARTTLS。From 必须属于本账号已验证的域名。每个凭据绑定一条发件流;营销邮件自动补齐 List-Unsubscribe 与 List-Unsubscribe-Post 一键退订头。

# .env — STARTTLS required
SMTP_HOST=smtp.fays.si
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USERNAME=your_smtp_username
SMTP_PASSWORD=your_smtp_password

POST /v1/emails

基址:https://mail.fays.si。全部 /v1/ 路由要求 Authorization: Bearer <API Key>,JSON 请求使用 Content-Type: application/json。不要在浏览器代码中暴露 Key。未知 JSON 字段会忽略;请求体上限 32 MiB。

curl https://mail.fays.si/v1/emails \
  -H "Authorization: Bearer $FAYS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-48213" \
  -d '{
    "from": "Shop <hello@example.com>",
    "to": ["reader@example.net"],
    "subject": "Your order is confirmed",
    "html": "<p>Thank you!</p>",
    "stream": "transactional"
  }'
from
必填,可含显示名;域名须属于当前账号且已验证。
to / cc / bcc / reply_to
地址数组。to + cc + bcc 原始条目合计 1–50,发信前规范化并去重。Bcc 不写入邮件头。reply_to 设置回复地址。邮箱地址部分必须为 ASCII,不支持 SMTPUTF8。
subject / html / text
主题为 1–998 字符,不含 CR/LF。html 与 text 至少一个非空。文本、HTML 和附件解码后的字节合计不超过 20 MiB。
attachments
数组,每项包含 filename、标准 base64 content、可选 content_type。未提供类型时按扩展名推断,未知则为 application/octet-stream。
headers
最多 20 个字符串值。名称只含 ASCII 字母、数字、连字符;值及地址显示名不得含 CR/LF。以下保留头不可覆盖。
stream
transactional 或 marketing。可省略;提供时必须与 API Key 绑定的发件流一致。
tags
最多 10 个字符串,每个 1–64 个字符,仅限 A–Z、a–z、0–9、下划线与连字符。
metadata
最多 20 个字符串键值对;键长 1–40 字符,值最多 500 字符。
from to cc bcc reply-to sender subject date message-id mime-version
content-type content-transfer-encoding content-disposition return-path
received dkim-signature list-unsubscribe list-unsubscribe-post

完整请求

{
  "from": "Shop <hello@example.com>",
  "to": [
    "reader@example.net"
  ],
  "cc": [],
  "bcc": [],
  "reply_to": [
    "support@example.com"
  ],
  "subject": "Order confirmed",
  "text": "Thank you!",
  "html": "<p>Thank you!</p>",
  "headers": {
    "X-Order-ID": "48213"
  },
  "attachments": [
    {
      "filename": "note.txt",
      "content": "SGVsbG8=",
      "content_type": "text/plain"
    }
  ],
  "stream": "transactional",
  "tags": [
    "order"
  ],
  "metadata": {
    "order_id": "48213"
  }
}

200 响应

{
  "recipients": [
    {
      "email": "reader@example.net",
      "id": "0123456789abcdef0123456789abcdef",
      "status": "queued"
    }
  ]
}

逐收件人状态为 queued、suppressed 或 failed。被抑制的收件人无 id,不扣费、不计额度。至少一人 queued 返回 HTTP 200;全部被抑制返回 422,全部注入失败返回 503。注入失败退回费用,后续退信不退款。queued 表示平台接收,不代表已进入收件箱。

Idempotency-Key

两个发信 POST 接口支持可选的 1–255 字符幂等键,按账号共享。请求身份包含 HTTP 方法、路径和原始正文,包括 JSON 空白。同 Key、同请求重放保存的状态码与响应;不同请求返回 422 idempotency_key_reused,尚在处理返回 409 idempotency_in_progress。记录在 24 小时后清理。顶层 429/503 会释放 Key 以允许重试,其他结果保存。进程崩溃可能留下处理中记录,直到清理。不要盲目重试已部分接收的请求。

POST /v1/emails/batch

emails 接受 1–100 封。先校验全部字段与发件域名,任一校验失败则整批不发;校验通过后逐封发送。HTTP 200 的 data 按输入顺序排列,可含逐封错误。

{
  "emails": [
    {
      "from": "Shop <hello@example.com>",
      "to": [
        "reader@example.net"
      ],
      "cc": [],
      "bcc": [],
      "reply_to": [
        "support@example.com"
      ],
      "subject": "Order confirmed",
      "text": "Thank you!",
      "html": "<p>Thank you!</p>",
      "headers": {
        "X-Order-ID": "48213"
      },
      "attachments": [
        {
          "filename": "note.txt",
          "content": "SGVsbG8=",
          "content_type": "text/plain"
        }
      ],
      "stream": "transactional",
      "tags": [
        "order"
      ],
      "metadata": {
        "order_id": "48213"
      }
    }
  ]
}
200
{
  "data": [
    {
      "recipients": [
        {
          "email": "reader@example.net",
          "id": "0123456789abcdef0123456789abcdef",
          "status": "queued"
        }
      ]
    }
  ]
}

整体 HTTP 200 的批次会缓存,即使其中某封返回 429 或 503。仅将失败项放入新请求,使用新的 Idempotency-Key 或不带 Key。重用原 Key 只会重放原响应,不会重发失败项。

GET /v1/emails/{id}

id 为响应中逐收件人的 32 位小写十六进制邮件 ID,仅能访问本账号邮件。邮件与事件保留 30 天;事件按发生时间排序。

GET /v1/emails/0123456789abcdef0123456789abcdef
200
{
  "id": "0123456789abcdef0123456789abcdef",
  "from_domain": "example.com",
  "to": "reader@example.net",
  "subject": "Order confirmed",
  "stream": "transactional",
  "status": "queued",
  "last_response": "",
  "tags": [
    "order"
  ],
  "metadata": {
    "order_id": "48213"
  },
  "created_at": "2026-10-27T12:00:00Z",
  "events": []
}

域名

添加、验证域名,读取 DNS 记录或设置按域名追踪开关。验证结果取决于实际 DNS,verify 后仍可能为 pending。下面的 DNS 值是占位,实际应使用响应的准确值。PATCH 仅更改已提供的追踪字段。

GET /v1/domains

请求

GET /v1/domains

响应

200
{
  "data": [
    {
      "name": "example.com",
      "status": "pending",
      "dkim_ok": false,
      "return_path_ok": false,
      "dmarc_ok": false,
      "track_transactional": false,
      "track_marketing": true,
      "records": [
        {
          "type": "TXT",
          "name": "<selector>._domainkey.example.com",
          "value": "v=DKIM1; k=rsa; p=<public-key>"
        },
        {
          "type": "CNAME",
          "name": "fm.example.com",
          "value": "<return-path-host>"
        },
        {
          "type": "TXT",
          "name": "_dmarc.example.com",
          "value": "v=DMARC1; p=none;"
        }
      ]
    }
  ]
}

POST /v1/domains

请求

{
  "name": "example.com"
}

响应

201
{
  "name": "example.com",
  "status": "pending",
  "dkim_ok": false,
  "return_path_ok": false,
  "dmarc_ok": false,
  "track_transactional": false,
  "track_marketing": true,
  "records": [
    {
      "type": "TXT",
      "name": "<selector>._domainkey.example.com",
      "value": "v=DKIM1; k=rsa; p=<public-key>"
    },
    {
      "type": "CNAME",
      "name": "fm.example.com",
      "value": "<return-path-host>"
    },
    {
      "type": "TXT",
      "name": "_dmarc.example.com",
      "value": "v=DMARC1; p=none;"
    }
  ]
}

GET /v1/domains/{name}

请求

GET /v1/domains/example.com

响应

200
{
  "name": "example.com",
  "status": "pending",
  "dkim_ok": false,
  "return_path_ok": false,
  "dmarc_ok": false,
  "track_transactional": false,
  "track_marketing": true,
  "records": [
    {
      "type": "TXT",
      "name": "<selector>._domainkey.example.com",
      "value": "v=DKIM1; k=rsa; p=<public-key>"
    },
    {
      "type": "CNAME",
      "name": "fm.example.com",
      "value": "<return-path-host>"
    },
    {
      "type": "TXT",
      "name": "_dmarc.example.com",
      "value": "v=DMARC1; p=none;"
    }
  ]
}

POST /v1/domains/{name}/verify

请求

POST /v1/domains/example.com/verify

响应

200
{
  "name": "example.com",
  "status": "verified",
  "dkim_ok": true,
  "return_path_ok": true,
  "dmarc_ok": true,
  "track_transactional": false,
  "track_marketing": true,
  "records": [
    {
      "type": "TXT",
      "name": "<selector>._domainkey.example.com",
      "value": "v=DKIM1; k=rsa; p=<public-key>"
    },
    {
      "type": "CNAME",
      "name": "fm.example.com",
      "value": "<return-path-host>"
    },
    {
      "type": "TXT",
      "name": "_dmarc.example.com",
      "value": "v=DMARC1; p=none;"
    }
  ]
}

PATCH /v1/domains/{name}

请求

{
  "track_transactional": true,
  "track_marketing": false
}

响应

200
{
  "name": "example.com",
  "status": "pending",
  "dkim_ok": false,
  "return_path_ok": false,
  "dmarc_ok": false,
  "track_transactional": true,
  "track_marketing": false,
  "records": [
    {
      "type": "TXT",
      "name": "<selector>._domainkey.example.com",
      "value": "v=DKIM1; k=rsa; p=<public-key>"
    },
    {
      "type": "CNAME",
      "name": "fm.example.com",
      "value": "<return-path-host>"
    },
    {
      "type": "TXT",
      "name": "_dmarc.example.com",
      "value": "v=DMARC1; p=none;"
    }
  ]
}

DELETE /v1/domains/{name}

请求

DELETE /v1/domains/example.com

响应

204

抑制名单

硬退信、投诉与退订按账号抑制。GET 按邮箱升序分页,limit 默认 100、上限 1,000;把 next 作为下一页 after。满页 next 为末条邮箱,否则为 null。重复添加保留原原因,删除不存在的地址返回 404。204 响应无正文。

GET /v1/suppressions

请求

GET /v1/suppressions?limit=100&after=

响应

200
{
  "data": [
    {
      "email": "reader@example.net",
      "reason": "manual",
      "created_at": "2026-10-27T12:00:00Z"
    }
  ],
  "next": null
}

POST /v1/suppressions

请求

{
  "email": "reader@example.net"
}

响应

201
{
  "email": "reader@example.net"
}

DELETE /v1/suppressions/{email}

请求

DELETE /v1/suppressions/reader%40example.net

响应

204

Webhook

每账号最多 5 个。非空 events 数组可选 delivered、bounced、deferred、complained、unsubscribed、opened、clicked。URL 必须为 HTTPS,不含嵌入凭据且解析到公网地址;不会跟随重定向。secret 仅在创建时返回,请保存完整的 whsec_ 字符串。

GET /v1/webhooks

请求

GET /v1/webhooks

响应

200
{
  "data": [
    {
      "id": 1,
      "url": "https://example.com/hooks/fays",
      "events": [
        "delivered",
        "bounced"
      ],
      "created_at": "2026-10-27T12:00:00Z"
    }
  ]
}

POST /v1/webhooks

请求

{
  "url": "https://example.com/hooks/fays",
  "events": [
    "delivered",
    "bounced"
  ]
}

响应

201
{
  "id": 1,
  "url": "https://example.com/hooks/fays",
  "events": [
    "delivered",
    "bounced"
  ],
  "created_at": "2026-10-27T12:00:00Z",
  "secret": "whsec_<save-this-secret>"
}

DELETE /v1/webhooks/{id}

请求

DELETE /v1/webhooks/1

响应

204

事件正文

{
  "id": "4619066a-1a93-470e-9033-206197c6f07e",
  "type": "delivered",
  "created_at": "2026-10-27T12:00:00Z",
  "data": {
    "occurred_at": "2026-10-27T12:00:00Z",
    "email_id": "0123456789abcdef0123456789abcdef",
    "to": "reader@example.net",
    "from_domain": "example.com",
    "subject": "Order confirmed",
    "stream": "transactional",
    "tags": [
      "order"
    ],
    "metadata": {
      "order_id": "48213"
    },
    "response": "250 OK"
  }
}

created_at 为入队时间,data.occurred_at 为事件发生时间,均为 UTC。退信额外包含 bounce_classification,点击包含 url。打开仅记录首次,点击每个目标 URL 仅记录首次。使用事件 id 去重。删除 Webhook 同时删除关联投递记录。

Fays-Signature

格式:t=<Unix 秒>,v1=<小写十六进制>。以完整 secret 为密钥,用 HMAC-SHA256 签名 ASCII 时间戳、一个点和原始 HTTP 正文字节。验签前不要解析后重新序列化 JSON。校验五分钟时钟窗口并常量时间比较,成功后再解析和幂等处理事件。保持接收端时钟同步。

Node.js / TypeScript

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySignature(
  secret: string,
  signature: string,
  rawBody: Uint8Array,
  now = Math.floor(Date.now() / 1000),
): boolean {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature);
  if (!match || Math.abs(now - Number(match[1])) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(match[1] + ".")
    .update(rawBody)
    .digest();
  return timingSafeEqual(expected, Buffer.from(match[2], "hex"));
}

Go

package webhook

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "regexp"
  "strconv"
  "time"
)

var signaturePattern = regexp.MustCompile(`^t=([0-9]+),v1=([a-f0-9]{64})$`)

func VerifySignature(secret, signature string, rawBody []byte, now time.Time) bool {
  parts := signaturePattern.FindStringSubmatch(signature)
  if parts == nil { return false }
  stamp, err := strconv.ParseInt(parts[1], 10, 64)
  if err != nil { return false }
  signedAt := time.Unix(stamp, 0)
  if signedAt.Before(now.Add(-5*time.Minute)) || signedAt.After(now.Add(5*time.Minute)) { return false }
  got, err := hex.DecodeString(parts[2])
  if err != nil { return false }
  mac := hmac.New(sha256.New, []byte(secret))
  mac.Write([]byte(parts[1] + "."))
  mac.Write(rawBody)
  return hmac.Equal(mac.Sum(nil), got)
}

投递与重试

请求含 Content-Type: application/json 与 User-Agent: FaysMail-Webhooks/1。任意 2xx 为成功;其他状态、重定向和网络错误都会重试,单次请求超时 10 秒。第 k 次失败等待 30 秒 × 2^(k−1);下一次尝试超过创建后 24 小时则标记 failed。工作进程每 2 秒领取最多 20 条。非 pending 投递记录在 7 天后清理,暂不支持手动重放。

入站邮件

先验证发信域名归属,再把该域名或其子域名添加为入站域名。发布 MX:优先级 10,目标 mx.fays.si。使用根域名会接管整个域名的收信。最低优先级数值的全部 MX 必须指向平台;验证通过后激活,也可由管理员在切换 DNS 前预先激活。已激活域名之后 MX 不匹配不会自动停用。

入站 SMTP 使用 25 端口,每封最多 20 MiB、50 个收件人。账号必须 active 且套餐 active 或 grace;每天额度取已订阅版本,账号覆盖值优先,UTC 00:00 重置,不扣发信额度或余额。无套餐返回 550 5.7.1,额度超限返回 452 4.2.2。优先匹配已启用的精确路由,再匹配 *;本地部分不区分大小写,+tag 不参与匹配但在载荷中保留大小写。路由可推送到 Webhook 或已确认的转发地址。

GET /v1/inbound/forward-targets

列出转发地址与可用状态

200 {data: [{id,email,status,created_at}],available}

POST /v1/inbound/forward-targets

添加地址并发送 72 小时有效的确认链接;每地址每日最多 3 次

{"email":"you@example.net"} → 201

POST /v1/inbound/forward-targets/{id}/resend

重发确认邮件;resend_limit 返回 Retry-After

200

DELETE /v1/inbound/forward-targets/{id}

删除地址并停用引用它的路由

204

GET /v1/inbound/domains

列出入站域名

200 {data: [{id,name,status,mx_ok,activated_by,mx_record,created_at}]}

POST /v1/inbound/domains

添加已拥有域名或子域名

{"name":"in.example.com"} → 201

DELETE /v1/inbound/domains/{id}

删除域名及其路由

204

POST /v1/inbound/domains/{id}/verify

检查 MX 并激活

200 {id,name,status,mx_ok,mx_record,…}

GET /v1/inbound/routes

列出路由,不含密钥

200 {data: [{id,inbound_domain_id,domain,local_part,action,webhook_url,include_raw,enabled,failing,…}]}

POST /v1/inbound/routes

创建路由,一次性保存密钥

{"inbound_domain_id":1,"local_part":"support","action":"webhook","webhook_url":"https://example.net/inbound","include_raw":false} → 201 {…,secret}

PATCH /v1/inbound/routes/{id}

可改 webhook_url、enabled、include_raw、forward_target_id;action 不可变

{"enabled":false} → 200

DELETE /v1/inbound/routes/{id}

删除路由

204

GET /v1/inbound/messages

每页 50 条,created_at 倒序再按 id;元数据与认证摘要,不含 envelope

?after=&route_id=&status= → 200 {data: […],next: string|null}

GET /v1/inbound/messages/{id}

单条元数据,含 envelope

200 {id,created_at,route_id,action,rcpt,mail_from,subject,status,spam_score,auth,envelope,…}

转发

须有启用的专用转发出口(平台账号除外)。打开邮件中的链接后点击确认按钮,单纯访问链接不会确认。未确认地址不可用于路由,已启用的入站域名不可作为转发目标。

{"inbound_domain_id":1,"local_part":"*","action":"forward","forward_target_id":1}

转发路由不返回 Webhook 密钥。每路由每日最多 200 封,同时受账号转发额度限制。垃圾分 ≥ 6 或 DMARC fail 且策略为 reject 时过滤。外发采用 SRS 与 ARC,最多重试 3 天。连续 5 次硬退信停用路由并通知账号,成功送达清零计数。日志包含 forward_to 和 filtered / bounced 状态。

签名入站载荷

{
  "type": "inbound.received",
  "id": "019a2ba0-0000-7000-8000-000000000001",
  "received_at": "2026-10-28T12:00:00Z",
  "route": {
    "id": 1,
    "local_part": "support",
    "tag": "Order-9"
  },
  "envelope": {
    "mail_from": "sender@example.net",
    "rcpt_to": "support+Order-9@in.example.com",
    "remote_ip": "203.0.113.10",
    "helo": "mail.example.net",
    "tls": true
  },
  "from": {
    "name": "Sender",
    "address": "sender@example.net"
  },
  "to": [
    {
      "name": "",
      "address": "support+Order-9@in.example.com"
    }
  ],
  "cc": [],
  "reply_to": [],
  "subject": "Hello",
  "message_id": "example@example.net",
  "in_reply_to": "",
  "references": [],
  "date": "2026-10-28T11:59:00Z",
  "text": "Hello",
  "html": "",
  "headers": [
    [
      "Subject",
      "Hello"
    ]
  ],
  "attachments": [],
  "auth": {
    "spf": "pass",
    "dkim": [
      {
        "domain": "example.net",
        "result": "pass"
      }
    ],
    "dmarc": "pass",
    "arc": "none"
  },
  "spam": {
    "score": 0.1,
    "action": "no action",
    "symbols": []
  }
}

POST 携带 Content-Type: application/json、Fays-Inbound-Id 和 Fays-Signature。使用上方相同的 HMAC-SHA256 校验函数,以完整路由 whsec_ 密钥验证原始 HTTP body。按 Fays-Inbound-Id 幂等处理,重试可能重复已成功的推送。headers 保留顺序和重复项;附件含 filename、content_type、size、content_id、disposition、content_base64。include_raw 附带 raw_base64;损坏 MIME 强制附带原文并把 text 置空。正文仅保留在邮件队列,不在元数据 API 中提供。

2xx 成功;406 / 410 永久失败;其他状态和网络错误重试至最多 24 小时,首次间隔 1 分钟、最大 1 小时。单次请求超时 30 秒,不跟随重定向;URL 必须为公网 HTTPS,沿用事件 Webhook 的 SSRF 校验。路由删除或停用返回 route_disabled。垃圾分 ≥ 15 或 reject 动作拒收入站邮件;扫描不可用返回临时 451。退信和入站的混合事务只丢弃垃圾入站副本。

入站错误码

HTTP错误码含义
409inbound_domain_exists入站域名已存在
422inbound_domain_not_owned请先验证所属父域名
422inbound_domain_reserved平台保留域名
409route_exists路由已存在
422forward_not_available转发暂不可用
409forward_target_exists转发目标已存在
422forward_target_unverified转发目标尚未确认
422forward_target_loop转发目标使用入站域名
422confirm_token_invalid确认令牌无效或已过期
429resend_limit确认邮件重发超限

限速与错误

HTTP 429 包含秒数 Retry-After:速率超限为 1,每日额度超限为到下一个 UTC 00:00 的向上取整秒数。SMTP 速率或每日额度超限返回 451 4.7.1,应延迟重试。T1 从每日 1,000 封、每秒 2 封起步,可能有账号级覆盖。批量发送即使 HTTP 为 200,也应检查逐封结果。

{"error":{"code":"rate_limited","message":"Rate limit exceeded"}}
HTTP错误码含义
400invalid_jsonJSON 无效
401invalid_api_key缺失或无效的 Key,或账号已关闭
401unauthorized需要账号身份
402plan_required需要有效套餐
402insufficient_balance预付余额不足
403account_inactive账号不可发信
404not_found资源不存在或不属于本账号
409domain_exists域名已被添加
409idempotency_in_progress同 Key 的请求仍在处理
422validation_error字段错误或超过体积限制
422domain_not_verified本账号尚未验证发件域名
422stream_mismatch发件流与 API Key 不一致
422tier_restrictedT0 仅能向本账号注册邮箱发送
422all_recipients_suppressed全部收件人被抑制
422free_mail_domain不允许添加免费邮箱域名
422webhook_limit每账号最多 5 个 Webhook
422idempotency_key_reused同 Key 已用于其他请求
429rate_limited每秒速率超限;Retry-After: 1
429daily_limit_reached每日额度超限;Retry-After 为距 UTC 00:00 秒数
503temporary_error服务或依赖暂时故障