文档
快速开始
- 创建账号并验证注册邮箱。验证前不能发信或创建凭据;验证后 T0 仅可使用已验证的发件域名免费向本账号注册邮箱发送。对外发信需要审核通过并订阅套餐。
- 在控制台添加域名,发布返回的 DKIM TXT、Return-Path CNAME,并设置有效的 DMARC TXT 策略。使用接口返回的准确 selector、公钥与 Return-Path 目标。检查 Return-Path 目标的 SPF,不要在同一名称下添加第二条 SPF 记录。域名验证返回 dkim_ok、return_path_ok 和 dmarc_ok,没有单独的 SPF 标志。
- 创建绑定 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_passwordPOST /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/0123456789abcdef0123456789abcdef200
{
"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响应
204Webhook
每账号最多 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"} → 201POST /v1/inbound/forward-targets/{id}/resend
重发确认邮件;resend_limit 返回 Retry-After
200DELETE /v1/inbound/forward-targets/{id}
删除地址并停用引用它的路由
204GET /v1/inbound/domains
列出入站域名
200 {data: [{id,name,status,mx_ok,activated_by,mx_record,created_at}]}POST /v1/inbound/domains
添加已拥有域名或子域名
{"name":"in.example.com"} → 201DELETE /v1/inbound/domains/{id}
删除域名及其路由
204POST /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} → 200DELETE /v1/inbound/routes/{id}
删除路由
204GET /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 | 错误码 | 含义 |
|---|---|---|
| 409 | inbound_domain_exists | 入站域名已存在 |
| 422 | inbound_domain_not_owned | 请先验证所属父域名 |
| 422 | inbound_domain_reserved | 平台保留域名 |
| 409 | route_exists | 路由已存在 |
| 422 | forward_not_available | 转发暂不可用 |
| 409 | forward_target_exists | 转发目标已存在 |
| 422 | forward_target_unverified | 转发目标尚未确认 |
| 422 | forward_target_loop | 转发目标使用入站域名 |
| 422 | confirm_token_invalid | 确认令牌无效或已过期 |
| 429 | resend_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 | 错误码 | 含义 |
|---|---|---|
| 400 | invalid_json | JSON 无效 |
| 401 | invalid_api_key | 缺失或无效的 Key,或账号已关闭 |
| 401 | unauthorized | 需要账号身份 |
| 402 | plan_required | 需要有效套餐 |
| 402 | insufficient_balance | 预付余额不足 |
| 403 | account_inactive | 账号不可发信 |
| 404 | not_found | 资源不存在或不属于本账号 |
| 409 | domain_exists | 域名已被添加 |
| 409 | idempotency_in_progress | 同 Key 的请求仍在处理 |
| 422 | validation_error | 字段错误或超过体积限制 |
| 422 | domain_not_verified | 本账号尚未验证发件域名 |
| 422 | stream_mismatch | 发件流与 API Key 不一致 |
| 422 | tier_restricted | T0 仅能向本账号注册邮箱发送 |
| 422 | all_recipients_suppressed | 全部收件人被抑制 |
| 422 | free_mail_domain | 不允许添加免费邮箱域名 |
| 422 | webhook_limit | 每账号最多 5 个 Webhook |
| 422 | idempotency_key_reused | 同 Key 已用于其他请求 |
| 429 | rate_limited | 每秒速率超限;Retry-After: 1 |
| 429 | daily_limit_reached | 每日额度超限;Retry-After 为距 UTC 00:00 秒数 |
| 503 | temporary_error | 服务或依赖暂时故障 |