文档中心 / 邮件验证码 OTP
邮件验证码 OTP API
用于登录、注册、找回密码等场景:由你的服务端调用多H云接口,向用户邮箱发送 6 位数字验证码并完成校验。验证码一次性有效,校验成功后立即作废。
Base URL:
完整地址示例:
https://cloud.cosmicps.com/store-api/api完整地址示例:
https://cloud.cosmicps.com/store-api/api/otp/v1/send
鉴权
在请求头携带 API Key(控制台「API 密钥」页面复制,格式形如 dh_…):
X-Api-Key: dh_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
也可放在 JSON Body 的 apiKey 字段(不推荐,易进日志)。未开通邮件推送权益或 Key 已禁用时会返回错误。
| 项 | 说明 |
|---|---|
| 获取 Key | 开通「邮件推送」或「云起步套餐」后自动发放;控制台可轮换 / 新建 / 禁用 |
| 多密钥 | 最多 5 个有效 Key;可设置 IP 白名单(控制台保存,用于运维约定) |
| 轮换 | 轮换后旧 Key 立即失效,请同步更新业务配置 |
发送验证码
POST /otp/v1/send
请求头
| Header | 必填 | 说明 |
|---|---|---|
Content-Type | 是 | application/json |
X-Api-Key | 是 | 你的 API Key |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
to | string | 是 | 收件人邮箱(用户邮箱) |
scene | string | 否 | 业务场景标记,如 login / register / reset,写入邮件模板可用变量并便于你方统计 |
vars | object | 否 | 自定义模板变量,键值会替换模板中的 {{key}} |
apiKey | string | 否 | 同 Header;不推荐 |
成功响应
{
"code": 0,
"data": {
"requestId": "a1b2c3d4e5f67890",
"to": "user@example.com",
"expiresInSec": 600,
"from": "your-brand@mail.cosmicps.com"
}
}
| 字段 | 说明 |
|---|---|
requestId | 本次发送记录 ID(内部使用) |
expiresInSec | 有效秒数,默认 600(可由控制台 TTL 配置) |
from | 实际发件地址 |
响应不会返回验证码明文。验证码仅出现在用户邮箱中。
校验验证码
POST /otp/v1/verify
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
to | string | 是 | 与发送时相同的邮箱 |
code | string | 是 | 用户输入的 6 位验证码 |
成功 / 失败(HTTP 200,看 data.ok)
// 正确
{ "code": 0, "data": { "ok": true } }
// 错误或过期(仍可能 HTTP 200)
{ "code": 0, "data": { "ok": false, "msg": "验证码错误" } }
{ "code": 0, "data": { "ok": false, "msg": "验证码已过期" } }
校验成功后该验证码立即删除,不可重复使用。请在你的业务里:data.ok === true 时再发放登录态。
错误码与 HTTP 状态
| HTTP | code | msg 示例 | 处理建议 |
|---|---|---|---|
| 401 | 401 | 无效 API Key | 检查 Key 是否复制完整、是否已轮换/禁用 |
| 403 | 403 | 邮箱验证码权益已过期或未开通 | 控制台开通邮件推送或云起步套餐并续费 |
| 429 | 429 | 发送过于频繁 | 触发每分钟限流;默认 30 次/分钟,可在控制台调低 |
| 400 | 400 | 收件邮箱无效 | 校验 to 格式 |
| 4xx/5xx | * | 其他 | 读 msg;发信通道异常时可能失败,可重试并查控制台用量 |
统一错误外形:
{ "code": 401, "msg": "无效 API Key" }
控制台配置(影响邮件内容)
路径:控制台 → 邮件推送
| 配置项 | 说明 | 默认 |
|---|---|---|
| 发件显示名 | 邮件 From 名称与主题前缀 | 多H云 |
| 专属发信地址 | {fromLocal}@mail.cosmicps.com | 按账号派生 |
| Reply-To | 用户点回复时的地址 | 你的登录邮箱 |
| 正文模板 | 支持 {{code}} {{ttl}} {{scene}} 及 vars 自定义键 | 您的验证码是 {{code}}… |
| TTL(分钟) | 1–60 | 10 |
| 每分钟上限 | 1–120,按账号维度限流 | 30 |
代码示例
cURL
# 发送
curl -sS -X POST 'https://cloud.cosmicps.com/store-api/api/otp/v1/send' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: dh_YOUR_KEY' \
-d '{"to":"user@example.com","scene":"login"}'
# 校验
curl -sS -X POST 'https://cloud.cosmicps.com/store-api/api/otp/v1/verify' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: dh_YOUR_KEY' \
-d '{"to":"user@example.com","code":"482915"}'
JavaScript(Node / 后端)
const BASE = 'https://cloud.cosmicps.com/store-api/api';
const KEY = process.env.DUOH_OTP_KEY;
async function sendOtp(to, scene = 'login') {
const r = await fetch(BASE + '/otp/v1/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': KEY
},
body: JSON.stringify({ to, scene })
});
const j = await r.json();
if (!r.ok || j.code) throw new Error(j.msg || 'send failed');
return j.data; // { requestId, to, expiresInSec, from }
}
async function verifyOtp(to, code) {
const r = await fetch(BASE + '/otp/v1/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': KEY
},
body: JSON.stringify({ to, code })
});
const j = await r.json();
if (!r.ok || j.code) throw new Error(j.msg || 'verify failed');
return j.data; // { ok: true } | { ok: false, msg }
}
// 登录流程示意
// 1) await sendOtp(email, 'login')
// 2) 用户输入 code 后:
const result = await verifyOtp(email, code);
if (result.ok) {
// 签发你自己的 session / JWT
} else {
// 提示 result.msg
}
Python
import os, requests
BASE = "https://cloud.cosmicps.com/store-api/api"
KEY = os.environ["DUOH_OTP_KEY"]
H = {"Content-Type": "application/json", "X-Api-Key": KEY}
def send_otp(to: str, scene: str = "login"):
r = requests.post(f"{BASE}/otp/v1/send", headers=H, json={"to": to, "scene": scene}, timeout=20)
j = r.json()
if r.status_code >= 400 or j.get("code"):
raise RuntimeError(j.get("msg") or r.text)
return j["data"]
def verify_otp(to: str, code: str):
r = requests.post(f"{BASE}/otp/v1/verify", headers=H, json={"to": to, "code": code}, timeout=20)
j = r.json()
if r.status_code >= 400 or j.get("code"):
raise RuntimeError(j.get("msg") or r.text)
return j["data"]
最佳实践
- 只在服务端持有 API Key;浏览器调你自己的
/auth/send-code。 - 对同一邮箱做业务侧冷却(如 60s),减少 429 与骚扰投诉。
scene建议固定枚举:login/register/reset/bind。- 校验成功后再创建会话;失败不要泄露「邮箱是否注册」以外的细节时可统一文案「验证码无效」。
- 生产环境开启密钥轮换与 IP 白名单约定;Key 泄露立即轮换。
- 在控制台「邮件推送」查看今日/本月发送与限流次数,异常时先查权益是否过期。
遇到发信失败或审核类问题,请在控制台提交工单;审核类通知会发至运营邮箱处理。
多H云 Docs