SMTP 与 HTTP API
创建 API 密钥,通过 SMTP 或 HTTP API 发送邮件,包含请求字段、响应、错误码和示例。
通过发信流发信有两种方式:从任意应用或邮件库走 SMTP,或者调用 HTTP API。两者共用同一套 API 密钥、额度、抑制名单和事件。开始之前,请先验证发信域名并创建 API 密钥。
API 密钥
- 打开发信流,进入 API 密钥 标签页。
- 点击 创建密钥,填写 名称(例如
production)并确认。 - 复制显示的密钥,然后点击 我已保存。密钥只显示这一次,之后列表中只显示它的 前缀,例如
ytm_aB3dE6gH…。
密钥都以 ytm_ 开头。每个发信流最多可有 20 个有效密钥。列表会显示每个密钥最近一次使用的时间。要停用某个密钥,点击 吊销:使用该密钥的程序会立即失效。密钥只能用于它所属的发信流。
API 密钥只能放在你的服务器上,不要写进浏览器代码、移动应用或公开仓库。任何拿到密钥的人都能以你的域名发信。
SMTP
发信流的 概览 标签页会显示 SMTP 配置:
| 配置项 | 值 |
|---|---|
| 主机 | 该发信流显示的 SMTP 主机,通常为 smtp.tm.yeapcloud.net |
| 端口 | 587(STARTTLS)或 465(TLS) |
| 用户名 | apikey(就是这个单词本身) |
| 密码 | 该发信流的任一 API 密钥 |
| 类别头 | X-Yeap-Category |
请以控制台显示的主机为准。部分发信流通过其他服务器组发信,主机名也不同。
SMTP 发信规则:
- From 邮件头必须使用该发信流已验证域名下的地址。
- 每个收件人(To、Cc 和 Bcc)都会收到单独的一份副本,拥有独立的邮件 ID,并各计为一封邮件。
- 要设置分类,可以添加
X-Yeap-Category: password-reset这样的邮件头,它会在投递前被移除。允许的字符为字母、数字、.、_和-,最长 64 个字符。 - 单封邮件最大 25 MB,每个 SMTP 事务最多 1,000 个收件人。
- 每封邮件都会带上
X-TM-ID邮件头(即发送记录和事件中显示的邮件 ID),以及供邮箱服务商反馈环使用的Feedback-ID邮件头。 - 在营销发信流上,你设置的
List-Unsubscribe邮件头会被替换为 Yeap Cloud 的一键退订邮件头。SMTP 不会修改邮件正文,所以请自行在内容中加入通信地址和退订链接。详见抑制名单与退订。 - 打开和点击跟踪仅在 HTTP API 中可用。
使用 Python 标准库的示例:
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "Example <noreply@example.com>"
msg["To"] = "user@example.org"
msg["Subject"] = "Reset your password"
msg["X-Yeap-Category"] = "password-reset"
msg.set_content("Click the link to reset your password: https://example.com/reset/abc")
with smtplib.SMTP("smtp.tm.yeapcloud.net", 587) as s:
s.starttls()
s.login("apikey", "ytm_your_api_key")
s.send_message(msg)SMTP 响应码
服务器拒收邮件时,响应码会说明原因。4xx 为临时错误:稍后重试。5xx 为永久错误:先解决问题再重新发送。
| 响应码 | 消息 | 原因与处理 |
|---|---|---|
535 5.7.8 | Authentication failed / API key revoked | 用户名或密钥错误,或密钥已被吊销。用户名必须是 apikey。 |
454 4.7.0 | Temporary authentication failure / Too many authentication failures | 服务器暂未就绪,或你的 IP 登录失败次数过多。稍等后重试。 |
451 4.7.1 | Stream is paused | 发信流已被自动暂停。查看发信流上显示的原因并联系客服。 |
550 5.7.1 | Stream is banned / suspended / terminated | 发信流已被封禁、订阅已停用,或发信流已释放。 |
550 5.7.1 | From domain … is not verified for this stream | 验证该域名,或检查它是否已共用给或绑定到该发信流。 |
550 5.7.1 | Sandbox mode: recipient is not a verified address | 发信流处于沙箱中。请验证该收件人,或发往你自己的域名。 |
550 5.7.1 | Recipient is on a suppression list / All recipients are suppressed | 该地址在抑制名单中(退信、投诉、退订或手动添加)。 |
452 4.7.1 | Hourly sending limit reached, try later | 每小时上限(或沙箱每日上限)已用完。下一个小时再试。 |
550 5.7.1 | Monthly sending quota exhausted | 每月邮件量已用完且不允许超量:免费版,或已达到超量消费上限。 |
452 4.5.3 | Too many recipients | 单个事务的收件人超过 1,000 个。 |
553 5.1.3 | Invalid recipient address | 收件地址格式错误。 |
554 5.7.1 | Message rejected: virus detected | 邮件含有病毒。发信流会被立即封禁,所有 API 密钥均被吊销。 |
554 5.7.1 | Message rejected as spam | 内容被判定为垃圾邮件。多次被拒会导致发信流被暂停。 |
451 4.3.0 | Content scanner unavailable, try again later | 营销邮件必须通过扫描才会被接收,请稍后重试。其他 451 4.3.0 响应为服务器临时错误:重试即可。 |
554 5.6.0 | Malformed message headers | 修正邮件头。 |
HTTP API
| 项目 | 值 |
|---|---|
| 基础 URL | https://api.tm.yeapcloud.net/v1(也可使用 https://api.yeap.cloud/api/tm/v1) |
| 认证 | Authorization: Bearer ytm_your_api_key |
| 格式 | 请求和响应体均为 JSON,Content-Type: application/json |
| 频率限制 | 每个 API 密钥每分钟 600 次请求 |
出错时返回 HTTP 状态码和如下响应体:
{"error": {"code": "invalid_request", "message": "subject is required (single line)"}}| HTTP 状态码 | error.code | 含义 |
|---|---|---|
| 400 | invalid_json、invalid_request | 请求体不是 JSON 对象,或批量请求为空或过大 |
| 401 | unauthorized | API 密钥缺失、格式错误、已吊销或不存在 |
| 403 | stream_paused、stream_banned、stream_suspended | 发信流无法发信 |
| 403 | from_domain_unverified、invalid_from | 发件人域名未对该发信流验证,或发件人地址无效 |
| 403 | postal_address_required | 营销发信流未填写通信地址。请在 营销设置 标签页填写。 |
| 403 | virus | 附件含有病毒,发信流已被封禁。 |
| 404 | not_found | 该发信流中不存在这封邮件 |
| 422 | invalid_request | 某个字段缺失或无效,message 会指明是哪一个。 |
| 429 | rate_limited | 一分钟内请求超过 600 次。请降低频率后重试。 |
| 503 | no_node | 当前没有可用的发信服务器。请稍后重试。 |
发送邮件:POST /v1/send
| 字段 | 类型 | 说明 |
|---|---|---|
from | string | 必填。单个地址,例如 noreply@example.com 或 Example <noreply@example.com>。域名必须已对该发信流验证。 |
to | string 或 array | 必填。单个地址、逗号分隔的字符串或数组。数组元素也可以是对象 {"email": "...", "name": "..."}。 |
cc、bcc | string 或 array | 可选。格式同 to。Bcc 地址不会出现在邮件头中。 |
reply_to | string 或 array | 可选。Reply-To 地址。 |
subject | string | 必填。单行,最长 998 个字符。 |
html、text | string | 至少填写一个。同时提供两者效果最好。 |
headers | object | 可选的额外邮件头,例如 {"X-Campaign": "spring"}。标准邮件头(From、To、Subject、Reply-To、Message-ID、List-Unsubscribe 等)不能在这里设置。 |
attachments | array | 可选。元素格式为 {"filename": "invoice.pdf", "content_base64": "...", "type": "application/pdf"}。最多 10 个文件,总大小不超过 10 MB。 |
category | string | 可选。字母、数字、.、_、-,最长 64 个字符。 |
tags | array of strings | 可选。最多 10 个标签,供你自己筛选,每个最长 64 个字符。 |
track_opens、track_clicks | boolean | 可选。为本封邮件开启或关闭打开跟踪(跟踪像素)和点击跟踪(链接改写为经由 t.tm.yeapcloud.net 跳转)。仅对 HTML 有效。不填则使用发信流的默认设置。两者默认均关闭。 |
variables | object | 可选。替换 subject、html 和 text 中的 {{name}} 占位符。在 html 中,值会做 HTML 转义。未知的占位符保持原样。 |
单次调用中 to、cc 和 bcc 合计最多 50 个收件人。每个收件人都会收到单独的一份副本,拥有独立的邮件 ID。如需向大量收件人发送不同内容,请使用批量接口。
示例:
curl -X POST https://api.tm.yeapcloud.net/v1/send \
-H "Authorization: Bearer ytm_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Example <noreply@example.com>",
"to": ["alice@example.org"],
"subject": "Your order {{order_id}} has shipped",
"html": "<p>Hi {{name}}, your order is on its way.</p>",
"text": "Hi {{name}}, your order is on its way.",
"category": "shipping",
"tags": ["order"],
"variables": {"name": "Alice", "order_id": "A-1024"}
}'响应:
{
"message_ids": ["01JAQ3Z9V8K4M2N6P7R5S1T0XW"],
"rejected": []
}message_ids 列出已接收的副本。rejected 列出未发送的收件人,每项包含 rcpt 和 reason。即使部分甚至全部收件人被拒,调用也可能返回成功,所以请务必检查 rejected:
reason | 含义 |
|---|---|
invalid_recipient | 地址无效 |
suppressed:<scope>:<reason> | 地址在抑制名单中,例如 suppressed:global:hard_bounce |
sandbox_recipient_not_verified | 发信流处于沙箱中,且该收件人不在允许范围内 |
rate_limited_hourly、rate_limited_sandbox_or_hourly | 每小时上限(或沙箱每日上限)已用完 |
monthly_quota_exceeded | 每月邮件量已用完且不允许超量 |
enqueue_failed | 邮件未能入队,不计入额度。请重试。 |
使用 requests 的 Python 示例:
import requests
resp = requests.post(
"https://api.tm.yeapcloud.net/v1/send",
headers={"Authorization": "Bearer ytm_your_api_key"},
json={
"from": "Example <noreply@example.com>",
"to": "alice@example.org",
"subject": "Welcome",
"html": "<p>Welcome aboard!</p>",
"text": "Welcome aboard!",
"category": "welcome",
},
timeout=30,
)
resp.raise_for_status()
result = resp.json()
print(result["message_ids"], result["rejected"])批量发送:POST /v1/batch
一次请求最多发送 500 封邮件。每一项的字段与 /v1/send 相同。
{
"messages": [
{"from": "noreply@example.com", "to": "a@example.org", "subject": "Hi A", "text": "Hello A"},
{"from": "noreply@example.com", "to": "b@example.org", "subject": "Hi B", "text": "Hello B"}
]
}响应中每封邮件对应一个结果,顺序与请求一致:
{
"results": [
{"index": 0, "message_ids": ["01JAQ..."], "rejected": []},
{"index": 1, "error": {"code": "invalid_request", "message": "subject is required (single line)"}, "rejected": []}
]
}查询邮件:GET /v1/messages
| 查询参数 | 说明 |
|---|---|
status | 按状态筛选:queued、delivered、deferred、bounced、complained、rejected 或 failed |
to | 按收件地址筛选 |
since | 只返回在此时间及之后创建的邮件,例如 2026-10-01T00:00:00Z |
limit | 每页条数,1–100(默认 50) |
before | 获取下一页时,传入上一次响应中的 next_before 值 |
响应格式为 {"data": [...], "next_before": "..."}。最后一页的 next_before 为 null。邮件记录保留 30 天。
GET /v1/messages/{id} 返回单封邮件及其事件:
{
"id": "01JAQ3Z9V8K4M2N6P7R5S1T0XW",
"stream_id": 12,
"source": "api",
"to": "alice@example.org",
"from": "Example <noreply@example.com>",
"subject": "Welcome",
"category": "welcome",
"tags": [],
"status": "delivered",
"smtp_code": 250,
"smtp_response": "250 2.0.0 OK",
"opened_at": null,
"clicked_at": null,
"unsubscribed_at": null,
"created_at": "2026-10-20 10:15:02",
"updated_at": "2026-10-20 10:15:04",
"events": [
{"type": "delivered", "at": "2026-10-20 10:15:04", "rcpt": "alice@example.org", "smtp_code": 250, "mx": "mx.example.org"}
]
}抑制名单:/v1/suppressions
这些接口用于管理 API 密钥所属发信流的抑制名单。
| 请求 | 请求体或查询参数 | 响应 |
|---|---|---|
GET /v1/suppressions | 查询参数 email(可选)、limit(1–500,默认 100)、offset | {"data": [{"id", "scope", "email", "reason", "category", "note", "created_at"}]} |
POST /v1/suppressions | {"email": "..."} 或 {"emails": [...]}(最多 1,000 个),可选 category | {"added": 2, "invalid": []} |
DELETE /v1/suppressions | {"email": "..."},可选 category(放在请求体或查询参数中) | {"removed": 1} |
通过 API 添加的地址,原因为 manual。账号级和全局抑制无法通过 API 修改。详见抑制名单与退订。