API 文档
公开 API /v1——使用 API 密钥认证,通过你自己的软件自动租用邮箱
你的 API 密钥
你尚未登录,请登录后获取你自己的 API 密钥。
请将此密钥放在 Authorization: Bearer <key> 请求头或 ?api_key=<key> 查询参数中发送。点击眼睛图标可显示完整密钥;需要更换时点击生成新密钥(旧密钥立即失效)。概览
基础 URL
https://otpgmail.net认证
请在每个请求中携带你的 API 密钥,放在
Authorization: Bearer og_xxx... 请求头中(推荐)或 ?api_key=og_xxx... 查询字符串中。Idempotency-Key(防止重复下单)
每个
POST /v1/orders 请求都应包含 Idempotency-Key: <每个订单一个新 UUID> 请求头。如果请求超时或连接中断,请使用相同的密钥再次发送,服务器会返回原来的响应,而不会再创建一个订单。每个新订单请生成新的 UUID,仅在重试时才复用同一个 UUID。一次处理多个订单:单次调用查询状态
要租用几十、几百个邮箱?请一次读取多个订单的状态:
POST /v1/orders/status 每次调用最多返回 100 个订单(在速率限制中只算一次请求),GET /v1/orders 列出你的订单——重启后用 ?status=waiting_code 即可取回所有仍在等待验证码的订单。每个错误也会告诉你是否值得重试(error.retryable、error.retryAfterSec)。详情请见多订单状态一节。邮箱类型:Gmail 或 iCloud
默认获得
@gmail.com 邮箱。如需租用 @icloud.com 邮箱,请在 POST /v1/orders 的请求体中加入 "domain": "icloud.com"。省略 domain 即表示 gmail.com,现有对接无需任何修改即可继续使用。iCloud 仅适用于在 GET /v1/services 中 icloud 字段不为 null 的服务;iCloud 的价格可能与 Gmail 不同(见 icloud.price)。每个订单响应都包含 domain 字段,说明该订单使用的邮箱类型。部分服务还提供 @gmx.com 邮箱(目前仅 Pokemon,代码 pkm,仓库 2):发送 "domain": "gmx.com"。服务是否提供该类型,取决于其在 GET /v1/services 的 offers[] 中是否有 domain = "gmx.com" 的条目;其他任何服务都会返回 DOMAIN_UNAVAILABLE。gmx.com 不适用于批量租用。仓库:一个服务,多个邮箱来源
同一个服务可以从多个仓库出售,每个仓库有各自的价格、库存和验证码规则(
GET /v1/services 中的 offers[] 数组)。创建订单时发送 "warehouse": 2 可指定仓库;"warehouse": 99 ⇒ 系统会选择有库存的最便宜仓库,缺货时自动换下一个(适合 Facebook、Instagram 等经常缺货的服务);0 ⇒ 有库存的最便宜仓库。现有客户端无需修改:省略 warehouse ⇒ 与以前一样使用仓库 1,price、stock 和 icloud 描述的都是仓库 1(没有仓库 1 的服务 ⇒ 有库存的最便宜仓库)。详情请见仓库。批量租用(批次)
批次需在网页端购买(批量租用页面);API 可用于列出批次和读取验证码。Gmail(仓库 1):每个邮箱都有一个
proxyUrl,即无需 API 密钥的验证码链接。iCloud(仓库 3):私有邮箱;使用 GET /v1/batches/{id}/mails/{mailId}/codes 读取验证码,或在网页端打开 readUrl。详情请见批量租用一节。购买短效 Gmail 账号(mail shop)
除了租用邮箱,你还可以从 2 个仓库买断完整的 Gmail 账号——仓库 1:邮箱、密码、Cookie + 收件箱链接(也可取 JSON);仓库 2:邮箱、密码 + 快速登录链接;仓库 99 会先买最便宜的(对应网页端的购买短效 Gmail页面)。该页面能做的事,API 全都能做:查看仓库、价格和库存,购买(必须携带
Idempotency-Key),跟踪交付进度,下载账号以及读取邮件。详情请见购买短效 Gmail一节。让你的客户自己查看验证码(/otp 页面)
你是否在转售自己租用的邮箱(通过网页或 API)?请在账户页面开启分享码,然后把链接
https://otpgmail.net/zh/otp#k7m2q9:[email protected](k7m2q9 = 你的分享码)发给客户。客户无需登录即可看到该邮箱的最新验证码,看不到价格或你的余额;该页面只执行免费操作(绝不会从你的账户扣费)。供工具 / 机器人使用的 JSON 链接(你的或你客户的):使用路径形式的链接
https://otpgmail.net/zh/otp/k7m2q9:[email protected](与 # 链接一样会打开 /otp 页面),在末尾加上 ?json ⇒ 服务器返回 JSON,内容与页面显示的数据完全相同:cards[]——该邮箱上每个租用中的服务一张卡片(email、serviceName、state、items[] = 最新的最多 5 个验证码 / 链接,新的在前),以及 serverTime。state:waiting 等待第一个验证码 · listening 等待新验证码 · received 已收到验证码 · needs_owner 需要卖家续期 · expired 已过期 · banned 邮箱被锁。多个邮箱:用逗号分隔(最多 50 个)。无需 API 密钥和 Cookie,CORS 开放;只读(“加载新 OTP”/“再次租用此邮箱”仍在 /otp 页面上)。防猜测限制与页面相同:分享码错误 / 该邮箱没有可显示的订单 ⇒ 404 NOT_FOUND;多次出错或同一邮箱每分钟读取超过 30 次 ⇒ 429 RATE_LIMITED,附带 retryAfter(秒)。在 /otp#… 链接后加 ?json 不会返回 JSON:浏览器从不把 # 之后的部分发送给服务器。curl "https://otpgmail.net/zh/otp/k7m2q9:[email protected]?json"{
"success": true,
"data": {
"cards": [
{
"ref": "Q3vX9kL2mP0aB7cD",
"serviceCode": "fb",
"serviceName": "Facebook",
"logo": "/logos/fb.png?v=20261007",
"domain": "gmail.com",
"email": "[email protected]",
"state": "received",
"until": null,
"items": [
{
"kind": "code",
"value": "529170",
"label": null,
"at": 1760000405
},
{
"kind": "code",
"value": "381904",
"label": null,
"at": 1759999030
}
],
"total": 2,
"canLoad": true
}
],
"serverTime": 1760000530
}
}GitHub 上的完整示例代码
可直接运行的 Python、Node.js、PHP、C# (.NET) 和 curl 客户端:租用邮箱、等待验证码、自动取消并退款,缺货时在 iCloud 与 Gmail 之间切换:github.com/letoan0902/otpgmail-email-otp-api(英文)· thue-otp-gmail(越南语)。Python 注意事项:请使用
requests,不要使用裸 urllib(其默认 User-Agent 会被 CDN 返回 403)。Webhook
无需不停轮询每个订单:注册一个 URL,一旦订单收到新验证码或被取消 / 退款,就会收到一条带签名的 POST——见 Webhook。
接口
GET/v1/balance
返回请求所用 API 密钥对应账户的余额(单位:VND)。
示例curl
Python
Node.js
curl "https://otpgmail.net/v1/balance" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"balance": 25000
}
}可能的错误: UNAUTHORIZEDGET/v1/services
列出已启用的服务:代码、名称、Gmail 价格(单位:VND)和当前 Gmail 库存;一个 icloud 块,包含 @icloud.com 邮箱的价格和库存;以及一个 offers[] 数组:出售该服务的每个仓库,每个“仓库 × 邮箱类型”一条记录(价格、库存、验证码数量、规则)。数据有缓存:库存约每 30–60 秒刷新一次,价格刷新频率更低。
icloud = null 表示该服务暂不支持 iCloud(服务商不提供,或已暂时停用),只能租用 Gmail。对于有 icloud 块的服务,创建订单时发送 "domain": "icloud.com",即可按 icloud.price 获得 @icloud.com 邮箱。price / stock / icloud.price 描述的是仓库 1,即未指定 warehouse 的订单所在的仓库(没有仓库 1 的服务 ⇒ 有库存的最便宜仓库);各仓库的价格和库存见 offers[](参见“仓库”)。 stock 是货源公布的数量(仓库 1:约每 30 秒刷新一次),并不保证一次调用能租到这么多:邮箱是逐个分配的,货源可能中途用完——这时 POST /v1/orders 会返回较少的订单并附带 partial 字段。仓库 2 和 4 显示的也是各自货源的数量。
示例curl
Python
Node.js
curl "https://otpgmail.net/v1/services" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": [
{
"code": "dr",
"name": "OpenAI (ChatGPT)",
"price": 350,
"stock": 616525,
"icloud": {
"price": 630,
"stock": 12464
},
"offers": [
{
"warehouse": 2,
"domain": "gmail.com",
"price": 350,
"stock": 616525,
"maxCodes": null,
"rules": null,
"fee": {
"freeHours": 6,
"paidPct": 50,
"maxHours": 24
},
"isDefault": true
},
{
"warehouse": 1,
"domain": "gmail.com",
"price": 630,
"stock": 12464,
"maxCodes": 3,
"rules": null,
"fee": null,
"isDefault": false
},
{
"warehouse": 1,
"domain": "icloud.com",
"price": 630,
"stock": 12464,
"maxCodes": 5,
"rules": null,
"fee": null,
"isDefault": true
}
],
"note": null
},
{
"code": "fb",
"name": "Facebook",
"price": 1590,
"stock": 88,
"icloud": null,
"offers": [
{
"warehouse": 1,
"domain": "gmail.com",
"price": 1590,
"stock": 88,
"maxCodes": 10,
"rules": null,
"fee": null,
"isDefault": false
},
{
"warehouse": 4,
"domain": "gmail.com",
"price": 1580,
"stock": 2310,
"maxCodes": null,
"rules": null,
"fee": null,
"isDefault": true
}
],
"note": null
}
]
}可能的错误: UNAUTHORIZEDPOST/v1/orders
创建一个接收验证码的邮箱订单,默认是 Gmail,发送 domain: "icloud.com" 时则为 iCloud。服务商一分配邮箱,就会从你的余额扣费。返回订单列表(一个订单,quantity > 1 时为多个)。
部分交付:quantity 只是上限
- 邮箱是逐个分配的。如果服务商中途缺货(例如此刻仓库 1 该服务已没有邮箱),调用会停止,并以 HTTP 200 返回已创建的订单——
data可能少于 quantity,且只对data中的订单扣费。GET /v1/services中的stock并不保证一次调用能拿到多少。 - 此时响应在
data旁还会带一个顶层字段partial(全部交付时不会出现,因此只读data的机器人无需任何修改):requested、delivered、missing和reason={code, message, retryable, retryAfterSec},其中code是导致调用停止的错误(NO_MAILS_AVAILABLE、OUT_OF_STOCK、ALL_WAREHOUSES_EXHAUSTED、INSUFFICIENT_BALANCE、WAITING_LIMIT_REACHED…)。 - 缺货通常只是暂时的——在仓库 1,服务商往往几秒后又会有邮箱。当
reason.retryable= true 时,等待reason.retryAfterSec秒(3–5 秒),然后用新的Idempotency-Key再请求缺少的missing个邮箱(旧密钥只会原样返回这次的响应);如此重复直到凑齐。 - 部分交付后紧接着的调用可能返回 409
WAREHOUSE_UNAVAILABLE/OUT_OF_STOCK或 503NO_MAILS_AVAILABLE,这是正常现象:调用之间继续间隔 3–5 秒,只有连续多次调用都失败(例如 10 次、约一分钟)才放弃;之后可稍后再试,或发送"warehouse": 99让系统自动切换到其他仓库。retryable= false(INSUFFICIENT_BALANCE、WAITING_LIMIT_REACHED…)⇒ 请先解决原因(充值,或等待未完成的订单结束)。 - 错误响应的
error中也带有同样的两个字段:error.retryable和error.retryAfterSec(例如 409OUT_OF_STOCK⇒true、5;402INSUFFICIENT_BALANCE⇒false、null)——参见错误码。
必须携带 Idempotency-Key 请求头(UUID)。每次新的租用请生成新的 UUID;超时后重试时请复用同一个密钥,以免创建重复订单。此调用会等待服务商分配邮箱,通常约需 0.3 秒,高峰期(少见)最长可达 10 秒,因此请将客户端超时设为约 15 秒,不要在 5 秒后就放弃并提前重试。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
service | string | 是 | 服务代码(来自 GET /v1/services,例如 "ig"、"tg"、"fb") |
quantity | number | 否 | 要租用的邮箱数量上限(默认 1;单次请求最多 200)——若仓库中途缺货,实际返回会更少:请以 data[] 的数量为准,并查看 partial |
domain | string | 否 | 邮箱类型:"gmail.com"(默认)、"icloud.com" 或 "gmx.com"。省略 = gmail.com,现有对接无需修改。仅当服务在 GET /v1/services 中的 icloud 字段不为 null 时才能租用 iCloud;"gmx.com" 仅适用于 offers[].domain = "gmx.com" 的服务(目前仅 Pokemon,代码 pkm),否则调用会返回 DOMAIN_UNAVAILABLE。其他任何值 ⇒ VALIDATION_ERROR。 |
warehouse | number | 否 | 要租用的仓库。省略 = 仓库 1,与多仓库功能上线前完全一致(没有仓库 1 的服务回退到最便宜的仓库),现有客户端无需修改。0 = 有库存的最便宜仓库(只会尝试一个仓库)。1–4 = 指定仓库(来自 GET /v1/services 中的 offers[].warehouse);如果该仓库不出售此服务/邮箱类型、已暂停或缺货 ⇒ WAREHOUSE_UNAVAILABLE。99 = 自动:按价格从低到高依次尝试有库存的仓库,某个仓库缺货或失败时自动换下一个;全部用尽 ⇒ ALL_WAREHOUSES_EXHAUSTED。推荐用于经常缺货的服务(Facebook、Instagram 等)。参见“仓库”。 |
curl
Python
Node.js
# 为每个新订单生成一个随机的 Idempotency-Key
# 超时后重试时复用同一个密钥(防止重复下单)
# domain: "gmail.com"(默认,可选)| "icloud.com"(租用 @icloud.com 邮箱)| "gmx.com"(仅限提供该类型的服务,例如 Pokemon)
# warehouse: 0 | 1–4 | 99(可选,省略则与以前一样使用仓库 1;0 = 有库存的最便宜仓库;99 = 自动,仓库缺货时自动换下一个;见 GET /v1/services 中的 offers[])
curl -X POST "https://otpgmail.net/v1/orders" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"service":"ig","quantity":1,"domain":"gmail.com"}'{
"success": true,
"data": [
{
"orderId": "abc123xyz",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "waiting_code",
"price": 1060,
"otp": [],
"codeDeadlineAt": 1753270000,
"rentExpiresAt": 1753356400,
"createdAt": 1753268200,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
}
]
}响应示例——部分交付(quantity 5,交付 2 个邮箱)
可能的错误: {
"success": true,
"data": [
{
"orderId": "abc123xyz",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "waiting_code",
"price": 1060,
"otp": [],
"codeDeadlineAt": 1753270000,
"rentExpiresAt": 1753356400,
"createdAt": 1753268200,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
},
{
"orderId": "jkl012mno",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "waiting_code",
"price": 1060,
"otp": [],
"codeDeadlineAt": 1753270001,
"rentExpiresAt": 1753356401,
"createdAt": 1753268201,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
}
],
"partial": {
"requested": 5,
"delivered": 2,
"missing": 3,
"reason": {
"code": "NO_MAILS_AVAILABLE",
"message": "No inboxes available for this service right now, please try again later",
"retryable": true,
"retryAfterSec": 5
}
}
}UNAUTHORIZED, VALIDATION_ERROR, SERVICE_DISABLED, INSUFFICIENT_BALANCE, OUT_OF_STOCK, DOMAIN_UNAVAILABLE, WAREHOUSE_UNAVAILABLE, ALL_WAREHOUSES_EXHAUSTED, NO_MAILS_AVAILABLE, QUANTITY_EXCEEDED, WAITING_LIMIT_REACHED, RATE_LIMITED, DUPLICATE_REQUEST, PROVIDER_ERROR, MAINTENANCEGET/v1/orders/{id}
按 ID 返回订单,包括目前已收到的验证码(otp[])。订单处于 waiting_code 状态时,请每 3–5 秒轮询此接口以获取验证码。
otp[] 数组按到达顺序排列:最早的在前,最新的在最后,请用 otp[otp.length - 1] 读取最新的验证码。每次调用时,系统都会按各仓库自己的节奏(每隔几秒)向服务商检查仍处于开启状态的订单,调用得更频繁只会返回已存储的数据。收到第一个验证码后请继续轮询:后续的验证码(如果服务发送了多个)会在订单有效期内追加到 otp[] 的末尾,不另收费;数量上限和时间窗口会在租用前显示(offers[] 中的 maxCodes 和 fee);如需确保系统正在等待新验证码,请调用 POST /v1/orders/{id}/next-code(适用于所有仓库;只有超出免费时段的仓库 2 才会收费)。如果服务每个邮箱只允许一个验证码,otp[] 会停留在该验证码,响应保持正常,不会报错。warehouse 字段说明由哪个仓库(1–4)提供了该邮箱;nextCodeFee 是获取更多验证码的费用(仓库 2 和仓库 3 的订单;仓库 1 和 4 为 null),参见“仓库”。 自 2026-10-02 起,订单还带有 nextCodeBlocked(true = 该邮箱无法再接收验证码)和 nextCodeBlockReason(原因):"email_banned"(邮箱已被对应服务锁定,请租用新邮箱)、"activation_gone"(服务商已关闭该邮箱)、"hard_expired"(验证码时段已结束)、"max_codes"(已收满验证码数量上限)、null(未被阻止 / 没有记录原因的旧订单)。这些都是新增字段,现有客户端无需修改。 要轮询很多订单?不要每个订单单独调用一次 GET,而是每次最多把 100 个 ID 发送到 POST /v1/orders/status——订单对象完全相同,速率限制只算一次请求。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 订单 ID(创建订单时返回的 orderId) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/orders/ORDER_ID" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"orderId": "abc123xyz",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "completed",
"price": 1060,
"otp": [
{
"code": "123456",
"receivedAt": 1753268500
},
{
"code": "789012",
"receivedAt": 1753269000
}
],
"codeDeadlineAt": 1753270000,
"rentExpiresAt": 1753356400,
"createdAt": 1753268200,
"warehouse": 1,
"nextCodeFee": null,
"listening": true,
"listenUntil": 1753270500,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
}
}可能的错误: UNAUTHORIZED, NOT_FOUNDPOST/v1/orders/{id}/cancel
取消订单并退还 100% 的价格。仅当订单处于 waiting_code 状态且尚未收到任何验证码时才允许取消。
已收到验证码的订单(哪怕只有一个)不能取消或退款,网站和 API 都一样。 rules.cancellable = false 的订单(自有库存的服务,如 Cointiply)无法取消:邮箱一经分配,就不能取消或退款,取消调用会返回 ORDER_NOT_CANCELLABLE。常规服务的仓库 3 订单(rules.cancellable = true)在验证码到达前可以像其他订单一样取消。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 要取消的订单 ID |
curl
Python
Node.js
curl -X POST "https://otpgmail.net/v1/orders/ORDER_ID/cancel" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json"{
"success": true,
"data": {
"orderId": "abc123xyz",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "cancelled",
"price": 1060,
"otp": [],
"codeDeadlineAt": 1753270000,
"rentExpiresAt": 1753356400,
"createdAt": 1753268200,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, ORDER_NOT_CANCELLABLE, PROVIDER_ERRORPOST/v1/orders/{id}/rerent
为自有库存服务(provider = "inventory",例如 Cointiply)的订单再次租用同一个邮箱:在该邮箱上创建一个全新接收时段的新订单,价格为旧订单的 rerentPriceVnd。
仅当旧订单的 canRerent = true 时有效(该邮箱的最新订单,且其上一个时段已结束或已收完所有邮件)。成功后立即扣费,请携带 Idempotency-Key 并在重试时复用,以免被重复扣费。响应为 { order, message };order 的结构与 GET /v1/orders/{id} 相同。 仓库 2(自 2026-10-09 起):已取消、已超时或已过期(邮箱已关闭)的订单,当
参数canRerent = true 时可调用(该邮箱的最新订单,且该邮箱没有其他进行中的订单)。系统会把原来的那个邮箱重新打开为一个新订单(status = waiting_code),价格为 rerentPriceVnd(目前为你价格的 100%),并像任何仓库 2 订单一样等待邮件 20 分钟,没有邮件则取消并 100% 退款。邮箱被对应服务锁定 ⇒ RERENT_EMAIL_BANNED;服务商已无法重新打开 ⇒ RERENT_MAIL_GONE;仓库 2 暂时不可用 ⇒ PROVIDER_ERROR,这三种情况都不会扣费。| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 该邮箱上一个订单的 ID(canRerent = true 的那个) |
curl
Python
Node.js
# 超时后重试时复用同一个密钥(防止重复下单)
curl -X POST "https://otpgmail.net/v1/orders/ORDER_ID/rerent" \
-H "Authorization: Bearer og_xxx..." \
-H "Idempotency-Key: $(uuidgen)"{
"success": true,
"data": {
"order": {
"orderId": "def456uvw",
"service": "ctp",
"domain": "icloud.com",
"email": "[email protected]",
"status": "waiting_code",
"price": 3500,
"otp": [],
"codeDeadlineAt": 1753273500,
"rentExpiresAt": 1753273500,
"createdAt": 1753269000,
"provider": "inventory",
"rules": {
"windowMinutes": 75,
"maxCodes": 4,
"cancellable": false,
"refundOnTimeout": false,
"rerentRatio": 0.5
},
"windowEndsAt": 1753273500,
"canRerent": false,
"rerentPriceVnd": 3500,
"warehouse": 3,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
},
"message": "Inbox rented again — a new 75-minute message window has started"
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, RERENT_NOT_AVAILABLE, RERENT_EMAIL_BANNED, RERENT_MAIL_GONE, INSUFFICIENT_BALANCE, WAITING_LIMIT_REACHED, RATE_LIMITED, DUPLICATE_REQUEST, PROVIDER_ERROR, MAINTENANCEPOST/v1/orders/{id}/next-code
请求在同一个邮箱上获取下一个验证码,适用于所有仓库(仓库 1–4)中已收到第一个验证码(status = completed)的订单。免费,但免费时段已结束的仓库 2 订单除外(nextCodeFee.feePriceVnd > 0):此时需发送 confirmFee: true 来支付费用并开启新的验证码时段(最长不超过硬性期限 nextCodeFee.hardUntil)。你的机器人无需知道是哪个仓库:继续轮询 GET /v1/orders/{id},新验证码会追加到 otp[] 中;想让系统等待新验证码时,再调用此接口。
此调用在各仓库中的作用
- 仓库 1:在订单有效期(
rentExpiresAt)内,每收到一个验证码后系统都会自动等待下一个,此调用返回 200,不做任何改动;一旦该邮箱无法再接收验证码(已达验证码上限、邮箱已关闭、已过期)⇒ 返回 409,并带有nextCodeBlockReason。 - 仓库 2:在服务商处重新打开邮箱(
reordered= true),并等待邮件 20 分钟;免费时段内免费(nextCodeFee.feePriceVnd= 0),超出后必须发送confirmFee= true(缺少 ⇒ 409NEXT_CODE_FEE_CONFIRM,不扣费)。只有收到新验证码时才会保留这笔费用,20 分钟内没有验证码 ⇒ 自动退款。每收到一个验证码后,系统会再继续等待下一个 25 分钟(listening= true,listenUntil)。 - 仓库 3(常规服务):重新开启一个短暂的监听时段,免费时段内免费(
nextCodeFee.freeUntil);之后 ⇒ 409,请使用POST /v1/orders/{id}/rerent。Cointiply 等自有库存服务(rules.cancellable= false)没有“下一个验证码”(409):每封邮件都会在时段内自动出现。 - 仓库 4:在服务商处重新读取订单并等待新验证码,在
rentExpiresAt之前免费。
200 = 系统正在为此订单等待新验证码(listening = true,listenUntil = 停止等待的时间;刚重新打开邮箱时 reordered = true;扣费时有 chargedVnd),或在你调用时恰好有新验证码到达(已在 order.otp 中)。409 = 无法开启新的等待时段,请查看 error.code;邮箱无法再接收验证码的原因见 order.nextCodeBlockReason(GET /v1/orders/{id})。仍在等待第一个验证码(waiting_code)的订单 ⇒ 409,暂时无需调用。付费调用请携带 Idempotency-Key(重试时复用,以免被重复扣费)。服务商无法连接 ⇒ 502 PROVIDER_ERROR,请稍后再试。 此调用的 409 错误码:NEXT_CODE_FEE_CONFIRM(仓库 2 需要付费,请带上 confirmFee: true 再次调用)、EMAIL_BANNED(邮箱已被对应服务锁定,请租用新邮箱;error.details.blockReason = "email_banned")、ORDER_EXPIRED(已过期、邮箱已关闭、已达验证码上限,或该服务没有“下一个验证码”,原因见 order.nextCodeBlockReason)、ORDER_NOT_CANCELLABLE(订单尚未收到第一个验证码)。把此调用返回的任何 409 都当作“没有更多验证码”的机器人无需修改即可继续使用。 refresh: true(仓库 2):先检查是否有新邮件(刚到的直接交付,不重新打开),再在服务商处重新打开邮箱——免费时段内等待重新计时 20 分钟;免费时段外等待截止时间(listenUntil)不变,也不扣费。响应额外包含 refreshed(true = 刚刚重新打开)和 refreshAvailableAt(unix 秒;在此之前再次刷新只会检查邮件;null = 现在即可刷新)。其他仓库:无效果(refreshed = false)。不发送 refresh 时,调用与以前完全一样。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 订单 ID(orderId),任何仓库均可 |
confirmFee | boolean | 否 | true = 你同意支付 nextCodeFee.feePriceVnd 以开启新的验证码时段。feePriceVnd = 0 时无需此参数。 |
refresh | boolean | 否 | true = 仓库 2:订单正在等待新验证码(listening = true)却迟迟不到时,立即在服务商处重新打开邮箱(刷新等待)——旧的等待在服务商那边可能已不再收信。免费;每个订单每 5 秒最多一次。订单不在等待中 ⇒ 与普通调用相同。 |
curl
Python
Node.js
# 免费(nextCodeFee 为 null 或 feePriceVnd = 0,即除免费时段结束后的仓库 2 之外的所有仓库):无需 confirmFee
curl -X POST "https://otpgmail.net/v1/orders/ORDER_ID/next-code" \
-H "Authorization: Bearer og_xxx..."
# 免费时段结束后的仓库 2:发送 confirmFee = true 以支付 nextCodeFee.feePriceVnd
curl -X POST "https://otpgmail.net/v1/orders/ORDER_ID/next-code" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"confirmFee":true}'{
"success": true,
"data": {
"order": {
"orderId": "ghi789rst",
"service": "dr",
"domain": "gmail.com",
"email": "[email protected]",
"status": "completed",
"price": 350,
"otp": [
{
"code": "482913",
"receivedAt": 1753268300
}
],
"codeDeadlineAt": 1753269700,
"rentExpiresAt": 1753354200,
"createdAt": 1753267800,
"warehouse": 2,
"nextCodeFee": {
"feePriceVnd": 0,
"freeUntil": 1753311000,
"hardUntil": 1753354200,
"paidPct": 50,
"freeHours": 6
},
"listening": true,
"listenUntil": 1753269500,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
},
"message": "Another code window is open — waiting for a new code",
"listening": true,
"listenUntil": 1753269500,
"reordered": true
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, NEXT_CODE_FEE_CONFIRM, INSUFFICIENT_BALANCE, EMAIL_BANNED, ORDER_EXPIRED, ORDER_NOT_CANCELLABLE, PROVIDER_ERROR, DUPLICATE_REQUEST, MAINTENANCE单次调用查询多个订单状态
租用大量邮箱的代理商和机器人不应该为每个订单单独发送
GET /v1/orders/{id}:POST /v1/orders/status 用相同的订单对象每次调用最多读取 100 个订单,GET /v1/orders 则从已存储的数据列出你的订单。- 用
POST /v1/orders/status每 3–5 秒轮询一次,每次调用最多 100 个订单 ID——每次调用在速率限制中只算一次请求。 - 每次调用,系统最多为列表中 20 个未结束的订单向服务商检查(先检查等待第一个验证码的订单,再检查最久未检查的订单),其余订单按已存储的数据返回;它们会在之后的调用中检查。一次调用约 3 秒内返回。
- 订单一旦结束就从列表中移除:cancelled(已退款),或在不再需要验证码时的 completed。
- 不存在或属于其他账号的 ID 会出现在
missing中(不返回 404)——也把它们移除。 - 重启后,用
GET /v1/orders?status=waiting_code取回所有未结束的订单(沿nextCursor翻页),然后继续用POST /v1/orders/status轮询。
POST/v1/orders/status
一次调用最多返回你的 100 个订单。每个元素与该订单的 GET /v1/orders/{id} 返回内容完全一致(status、包含所有已收到验证码的 otp[]、warehouse…)。用于同时轮询多个未结束的订单。
data 保持 ids 的顺序;重复的 ID 会被忽略。不存在或属于其他账号的 ID 会列在 missing 中(不返回 404)。对于未结束的订单,系统会像 GET /v1/orders/{id} 一样向服务商检查,但每次调用最多检查 20 个订单——先检查等待第一个验证码的订单,再检查最久未检查的订单——其余订单按已存储的数据返回(会在之后的调用中检查);一次调用约 3 秒内返回。无论包含多少个 ID,一次调用在速率限制中都只算一次请求。超过 100 个 ID、空列表或非字符串 ID ⇒ 400 VALIDATION_ERROR。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
ids | string[] | 是 | 订单 ID(orderId)——每次调用 1 到 100 个 |
curl
Python
Node.js
# 你在等待的订单 ID(POST /v1/orders 返回的 orderId)——每次调用最多 100 个
curl -X POST "https://otpgmail.net/v1/orders/status" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-d '{"ids":["ORDER_ID_1","ORDER_ID_2"]}'{
"success": true,
"data": [
{
"orderId": "abc123xyz",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "completed",
"price": 1060,
"otp": [
{
"code": "123456",
"receivedAt": 1753268500
}
],
"codeDeadlineAt": 1753270000,
"rentExpiresAt": 1753356400,
"createdAt": 1753268200,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
},
{
"orderId": "jkl012mno",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "waiting_code",
"price": 1060,
"otp": [],
"codeDeadlineAt": 1753270001,
"rentExpiresAt": 1753356401,
"createdAt": 1753268201,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
}
],
"missing": [
"zzz999qqq"
]
}可能的错误: UNAUTHORIZED, VALIDATION_ERROR, RATE_LIMITEDGET/v1/orders
按从新到旧列出你的订单,只读取已存储的数据——不会向服务商获取新验证码(这由 POST /v1/orders/status 完成)。可按状态和创建时间筛选,用 nextCursor 翻页。
把 nextCursor 作为 cursor 传入即可获取下一页;nextCursor = null ⇒ 最后一页。即使同一秒内创建了很多订单,翻页也绝不会重复或遗漏订单。内部状态永远不会列出(只有 waiting_code、completed、cancelled 和 queued)。机器人重启后:GET /v1/orders?status=waiting_code&limit=200,沿 nextCursor 翻页,再用 POST /v1/orders/status 轮询这些订单。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
status | string | 否 | 只返回该状态:waiting_code | completed | cancelled | queued(不传 ⇒ 全部) |
since | number | 否 | 只返回在此 unix 时间(秒)或之后创建的订单 |
limit | number | 否 | 每页订单数:1–200(默认 50) |
cursor | string | 否 | 上一页的 nextCursor,原样传入 |
curl
Python
Node.js
# 重启后:逐页收集所有仍在等待验证码的订单
curl "https://otpgmail.net/v1/orders?status=waiting_code&limit=200" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": [
{
"orderId": "jkl012mno",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "waiting_code",
"price": 1060,
"otp": [],
"codeDeadlineAt": 1753270001,
"rentExpiresAt": 1753356401,
"createdAt": 1753268201,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
},
{
"orderId": "abc123xyz",
"service": "ig",
"domain": "gmail.com",
"email": "[email protected]",
"status": "waiting_code",
"price": 1060,
"otp": [],
"codeDeadlineAt": 1753270000,
"rentExpiresAt": 1753356400,
"createdAt": 1753268200,
"warehouse": 1,
"nextCodeFee": null,
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
}
],
"nextCursor": "1753268200:abc123xyz"
}可能的错误: UNAUTHORIZED, VALIDATION_ERROR, RATE_LIMITED仓库
每个
offers[] 条目是一个报价 =(服务、仓库、邮箱类型)。只列出当前在售的仓库;暂时缺货的仓库仍会留在列表中,stock = 0。仓库编号是固定的:1、2、3、4,创建订单时在 warehouse 参数中传入相同的编号即可。现有客户端无需修改。省略
warehouse ⇒ 行为与以前相同(仓库 1:等待 25 分钟,如果服务发送多个验证码,额外的验证码会在 24 小时订单有效期内自动到达);发送 0 ⇒ 有库存的最便宜仓库;发送 99 ⇒ 自动选择仓库,某仓库缺货时自动换下一个(见下文);所有现有字段保持不变,只新增了字段(offers[]、warehouse、nextCodeFee、listening、nextCodeBlocked 等)。自动仓库:warehouse = 99(推荐用于经常缺货的服务)
offers[] 字段发送
"warehouse": 99,系统会从有库存的最便宜仓库租用;如果该仓库缺货或失败,会在同一次调用中自动换下一个。非常适合 Facebook 和 Instagram 等经常缺货的服务,你的机器人不必再读取 offers[] 并逐个仓库重试。- 尝试顺序:价格从低到高(例如 1,500 → 1,600 → 1,700 VND);价格相同 ⇒ 仓库编号小的优先。
- 库存为零的仓库(
offers[]中stock= 0)永远不会被调用,不浪费时间。 - 当前仓库缺货或失败时换下一个(
OUT_OF_STOCK、NO_MAILS_AVAILABLE、WAREHOUSE_UNAVAILABLE、PROVIDER_ERROR)。 - 其他错误会立即返回,不会尝试下一个仓库:
INSUFFICIENT_BALANCE、WAITING_LIMIT_REACHED、QUANTITY_EXCEEDED、MAINTENANCE、SERVICE_DISABLED、DOMAIN_UNAVAILABLE等。 quantity> 1:如果第一个仓库数量不足,其余部分由下一个仓库补足,因此订单可能来自多个仓库;每个订单都带有发货仓库的warehouse和price(并遵循该仓库的验证码规则)。- 如果所有仓库都尝试过仍无法租到邮箱 ⇒
ALL_WAREHOUSES_EXHAUSTED(HTTP 409,不扣费),请稍后再试。如果只租到了部分邮箱,仍会返回 200 和这些订单。
curl
Python
Node.js
# warehouse: 99,选择有库存的最便宜仓库;缺货或失败 ⇒ 自动换下一个仓库
curl -X POST "https://otpgmail.net/v1/orders" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"service":"fb","quantity":1,"domain":"gmail.com","warehouse":99}'¹ 仅从自有库存出售的服务,例如 Cointiply(
nextCodeFee:获取更多验证码的费用(仓库 2 和仓库 3 的订单)provider = "inventory",rules.cancellable = false),遵循 rules:在 rules.windowMinutes 分钟内最多接收 rules.maxCodes 封邮件,不可取消,时间用完仍未收到邮件也不退款。包含在
GET /v1/orders/{id} 和每个订单响应中;仓库 1 和 4 的订单为 null(更多验证码免费)。仓库 2:需要再来一个验证码时调用 POST /v1/orders/{id}/next-code,如果 feePriceVnd 大于 0,必须发送 confirmFee = true。仓库 3(常规服务):免费时段结束后,feePriceVnd 是再次租用同一个邮箱(rerent)的价格。curl
Python
Node.js
# warehouse: 0 | 1–4 | 99(可选,省略则与以前一样使用仓库 1;0 = 有库存的最便宜仓库;99 = 自动,仓库缺货时自动换下一个;见 GET /v1/services 中的 offers[])
curl -X POST "https://otpgmail.net/v1/orders" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"service":"dr","quantity":1,"domain":"gmail.com","warehouse":2}'{
"success": true,
"data": {
"orderId": "ghi789rst",
"service": "dr",
"domain": "gmail.com",
"email": "[email protected]",
"status": "completed",
"price": 350,
"otp": [
{
"code": "482913",
"receivedAt": 1753268300
}
],
"codeDeadlineAt": 1753269700,
"rentExpiresAt": 1753354200,
"createdAt": 1753267800,
"warehouse": 2,
"nextCodeFee": {
"feePriceVnd": 175,
"freeUntil": 1753289400,
"hardUntil": 1753354200,
"paidPct": 50,
"freeHours": 6
},
"listening": false,
"listenUntil": null,
"nextCodeBlocked": false,
"nextCodeBlockReason": null
}
}批量租用(批次)
批次需在网页端购买(批量租用页面),API 可用于列出批次和读取验证码。邮箱域名决定仓库:Gmail = 仓库 1,iCloud = 仓库 3(私有邮箱)。
- 批次自购买起保留 24 小时,在此期间可免费读取验证码,验证码数量不限。
- 没有收到验证码的邮箱不予退款;只有数量短缺的部分才会(自动)退款。
- 仓库 1(Gmail):每个邮箱都有一个
proxyUrl,对其发起GET会返回{"status":1,"code":"482913","all_codes":[...]},无需 API 密钥(链接本身就是密钥,请妥善保密)。下面的验证码接口同样可用。 - 仓库 3(iCloud):
proxyUrl=null,请用下面的接口读取验证码,或打开readUrl(网页)。每个邮箱在该服务下绑定到你的账号;过期后你仍可在网页端再次租用。 - 无法提取验证码的邮件(欢迎邮件、新闻邮件等)会原样放在
code中返回,并带有isRaw: true(HTML 或文本,最多 20,000 个字符)。
GET/v1/batches
你的批次,最新的在前(用 page / pageSize 分页,最多 100 条)。每个批次包含 warehouse(1 = Gmail,3 = iCloud)、status(active | expired | failed)、expiresAt、codeCount 和 readUrl。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
page | number | 否 | 页码(默认 1) |
pageSize | number | 否 | 每页批次数(默认 20,最大 100) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/batches?page=1&pageSize=20" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"items": [
{
"id": "k3v9m2x7q1w8e5r4",
"serviceCode": "lf",
"serviceName": "Tiktok",
"domain": "icloud.com",
"warehouse": 3,
"status": "active",
"countRequested": 3,
"countReceived": 3,
"countDuplicate": 0,
"countCharged": 3,
"unitPriceVnd": 600,
"quotedVnd": 1800,
"chargedVnd": 1800,
"refundVnd": 0,
"holdHours": 24,
"expiresAt": 1753354200,
"createdAt": 1753267800,
"readUrl": "https://otpgmail.net/app/batch/k3v9m2x7q1w8e5r4",
"codeCount": 1,
"rerentPriceVnd": null
}
],
"total": 1,
"page": 1,
"pageSize": 20
}
}可能的错误: UNAUTHORIZEDGET/v1/batches/{id}
单个批次及其 mails[],取每个邮箱的 id 即可读取其验证码。
Gmail 批次(仓库 1):每个邮箱的 proxyUrl 在你的脚本中与以前完全一样可用。iCloud 批次(仓库 3):proxyUrl = null,请用下面的接口读取验证码。已过期的 iCloud 批次带有 rerentPriceVnd,即单独再次租用一个邮箱的价格(在网页端操作)。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 批次 ID(显示在批量租用页面和 readUrl 中) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/batches/BATCH_ID" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"id": "k3v9m2x7q1w8e5r4",
"serviceCode": "lf",
"serviceName": "Tiktok",
"domain": "icloud.com",
"warehouse": 3,
"status": "active",
"countRequested": 3,
"countReceived": 3,
"countDuplicate": 0,
"countCharged": 3,
"unitPriceVnd": 600,
"quotedVnd": 1800,
"chargedVnd": 1800,
"refundVnd": 0,
"holdHours": 24,
"expiresAt": 1753354200,
"createdAt": 1753267800,
"readUrl": "https://otpgmail.net/app/batch/k3v9m2x7q1w8e5r4",
"codeCount": 1,
"rerentPriceVnd": null,
"mails": [
{
"id": 812,
"email": "[email protected]",
"url": "https://otpgmail.net/app/batch/k3v9m2x7q1w8e5r4",
"proxyUrl": null,
"readUrl": "https://otpgmail.net/app/batch/k3v9m2x7q1w8e5r4",
"fresh": true,
"codeCount": 1,
"lastCode": "482913",
"lastCodeAt": 1753268500,
"lastIsRaw": false,
"checkedAt": 1753268507
}
]
}
}可能的错误: UNAUTHORIZED, NOT_FOUNDGET/v1/batches/{id}/mails/{mailId}/codes
读取批次中某个邮箱的验证码:如果该邮箱到了检查时间,系统会向服务商查询,然后返回已收到的全部验证码,最早的在前,最新的验证码位于 codes[] 的末尾。
等待期间请每 5–10 秒轮询一次(调用得更频繁只会返回已存储的数据,polledAt 不会变化)。无法提取验证码的邮件会原样放在 code 中返回,并带有 isRaw: true。已过期的批次仍会返回已存储的验证码,但不再检查。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | 批次 ID |
mailId | number | 是 | 邮箱 ID(mails[].id) |
poll | 0 | 1 | 否 | 0 = 只读取已存储的验证码,不向服务商检查(默认 1) |
curl
Python
Node.js
# 等待期间每 5–10 秒再调用一次;最新的验证码位于 codes[] 的末尾
curl "https://otpgmail.net/v1/batches/BATCH_ID/mails/MAIL_ID/codes" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"mailId": 812,
"codes": [
{
"code": "482913",
"receivedAt": 1753268500,
"isRaw": false
}
],
"polledAt": 1753268507,
"mail": {
"id": 812,
"email": "[email protected]",
"url": "https://otpgmail.net/app/batch/k3v9m2x7q1w8e5r4",
"proxyUrl": null,
"readUrl": "https://otpgmail.net/app/batch/k3v9m2x7q1w8e5r4",
"fresh": true,
"codeCount": 1,
"lastCode": "482913",
"lastCodeAt": 1753268500,
"lastIsRaw": false,
"checkedAt": 1753268507
}
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, RATE_LIMITEDBuy live Gmail accounts (2 warehouses)
Buy short-lived Gmail accounts outright: you are charged when you order and the accounts arrive within seconds. There are two mail-shop warehouses (numbered separately from the OTP-rental warehouses 1–4), each with ONE price for any quantity. Prices returned by the API are YOUR account's prices (custom price if the admin set one) — exactly what you are charged.
Warehouses- 仓库 1(
gm_cookie)的账号不带辅助邮箱——你拿到的是邮箱、密码、Cookie 和收件箱链接(inboxUrl/inboxJsonUrl),没有别的。 - 推荐的登录方式:导入 Cookie(
cookies,JSON)到浏览器——用 Cookie 导入扩展,或用指纹浏览器(antidetect)的 Cookie 导入功能——然后打开 Gmail:会话已处于登录状态,通常不会要求输入密码或验证。 - 只需要验证码或邮件?通过
inboxUrl(网页)或inboxJsonUrl(JSON)读取即可——不需要 API 密钥,也不需要登录 Google。 - 在新设备或新 IP 上用密码登录,Google 可能要求验证(辅助邮箱、手机号或验证码)——这些账号无法通过该验证,所以请改用 Cookie 或收件箱链接。
- 转售?请把 Cookie 和收件箱链接一起交给你的客户——.txt 下载(
GET /v1/mail-shop/orders/{id}/download)每行就是email|password|cookies|inboxUrl——并告诉他们用 Cookie 登录,不要用密码。 - 仓库 2(
gm_login)的账号则附带快速登录链接(loginUrl)——请在电脑浏览器中打开它登录。
- See warehouses & prices:
GET /v1/mail-shop/products⇒products[](per warehouse:priceVnd,stock,available,maxQty) +auto(warehouse 99:order,priceVnd). To see the total first, callGET /v1/mail-shop/quote. - Buy:
POST /v1/mail-shop/orderswith{"warehouse": 1 | 2 | 99, "quantity": N}+ headerIdempotency-Key(a new UUID per purchase). The response is the order, usually alreadycompletedwithitems[]. - Get the accounts: read
items[](table below) or download the TXTGET /v1/mail-shop/orders/{id}/download.status=pending⇒ pollGET /v1/mail-shop/orders/{id}every 5 seconds untilcompleted/partial/failed.
curl
Python
Node.js
# 1) warehouses, your prices, stock
curl "https://otpgmail.net/v1/mail-shop/products" -H "Authorization: Bearer og_xxx..."
# 2) buy (warehouse 1 | 2 | 99) — a NEW Idempotency-Key per purchase
curl -X POST "https://otpgmail.net/v1/mail-shop/orders" \
-H "Authorization: Bearer og_xxx..." -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" --max-time 60 \
-d '{"warehouse":99,"quantity":2}'
# 3) status "pending"? read the order again (or download the .txt)
curl "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID" -H "Authorization: Bearer og_xxx..."GET {inboxJsonUrl} (= inboxUrl + ?json) returns the same JSON as GET /api/mailbox/{code}: email, letters[] (≤ max latest emails: from, subject, receivedAt, code — extracted code / link, content — sanitized HTML), total, etag, nextAt. No API key needed (the code in the link is the key — keep it private), CORS is open. Add &max=20|50|100|200 for more emails (default 10), &etag={previous etag} ⇒ unchanged emails come back as empty letters + unchanged: true. Reading one link faster than every 10 seconds ⇒ 429 (retryAfter); link off / wrong / expired ⇒ 404; account locked ⇒ 410.curl "https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d?json&max=20"{
"success": true,
"data": {
"email": "[email protected]",
"letters": [
{
"key": "h:9f2c4e1a7b3d5c80",
"from": "Instagram [email protected]",
"subject": "350038 is your Instagram code",
"receivedAt": 1760000520,
"code": "350038",
"content": "<html><body><p>Enter this confirmation code in the app: 350038</p></body></html>"
}
],
"max": 10,
"total": 8,
"fetchedAt": 1760000530,
"cached": false,
"etag": "f1c2a9d4e0b37a65",
"unchanged": false,
"nextAt": 1760000540,
"expiresAt": 1762592400,
"serverTime": 1760000530
}
}Read the Vinted OTP code (otpUrl)A warehouse-2 account with
otpUrl ⇒ open it in a browser to see the latest Vinted code (the page refreshes itself); GET {otpUrl}?json returns {status, code, service, email, checkedAt, retryAfter}: status = waiting (no code yet — ask again after retryAfter seconds) · ok (code = the latest code) · dead (codes can no longer be read for this account) · error (temporarily unreadable — ask again). No API key needed (the code in the link is the key — keep it private), CORS is open; the server reads the source at most every few seconds per account; more than 120 calls / minute / IP ⇒ 429.curl "https://otpgmail.net/api/mail-shop/otp/p3x9k2m7q1w8e5r4.2.Zx8Kq2Lm4Np6Rt0Vw3Yb5c?json"{
"success": true,
"data": {
"status": "ok",
"code": "4829",
"service": "vinted",
"email": "[email protected]",
"checkedAt": 1760000530,
"retryAfter": 5
}
}The 2FA code (twoFaUrl)Some warehouse-1 accounts have 2-step verification (2FA) on — a password sign-in makes Google ask for a 6-digit code.
GET {twoFaUrl} (or open it in a browser) returns text/plain with JUST the current 6 digits (no newline), a Refresh header = seconds until the next code + 1 (browsers reload by themselves), Cache-Control: no-store, open CORS — you can hand the link to your customers. Account without 2FA ⇒ 404 no 2fa; wrong / off / expired link ⇒ 404 not found; locked account ⇒ 410 gone; too fast / source hiccup ⇒ 429 / 502 retry (Retry-After header). With an API key: GET /v1/mail-shop/orders/{id}/items/{lineNo}/2fa ⇒ {none, code, expiresIn}. The server caches the code until it expires. The .txt file (GET /v1/mail-shop/orders/{id}/download): add ?twofa=1 ⇒ a last twoFaUrl column (off by default — the old layout is unchanged).curl "https://otpgmail.net/2fa/Q7xK2mP9vL4tR8wN3cB6yH1d"
# 424819Order statuses- Money:
chargedVnd= Σroute[].quantity×unitPriceVnd(a 99 order split across warehouses ⇒ two legs inroute). Shortfalls are refunded automatically (refundVnd). Delivered accounts are not refunded — the only warranty is the warehouse'sterms(read them first). - Idempotency-Key is required (400 without it): retrying with the SAME key after a timeout returns the same order and charges nothing more; a new key buys again. Use a client timeout of at least 60 seconds.
- Limits: at most 500 accounts per order (and the warehouse's
maxQty), 10 purchases per minute for your account (web + API share it), 180 inbox reads per hour. Above ⇒ 429RATE_LIMITED. - Common errors:
MAILSHOP_OUT_OF_STOCK409 (details.stock= how many you can buy now) ·MAILSHOP_QTY_LIMIT400 (details.max) ·INSUFFICIENT_BALANCE402 ·MAILSHOP_DISABLED503 (warehouse / section paused) ·MAILSHOP_SUPPLIER_ERROR502 (the source refused at once — a failed order was recorded and refunded,details.orderId) ·VALIDATION_ERROR400 (warehouse not 1 | 2 | 99, warehouse and product disagree, missing Idempotency-Key…). - Terms (warranty only for the first sign-in within 2 hours, accounts last ~6–24 hours) are in each warehouse's
terms; the API does not ask you to accept them (the web asks once). API orders show up in your web history (channel=api) and vice versa.
GET/v1/mail-shop/products
The two mail-shop warehouses + automatic warehouse 99. products[]: warehouse (1 | 2), code (gm_cookie | gm_login), delivers (fields of every account), priceVnd (YOUR price), listPriceVnd (list price), stock, available, maxQty (most one order can take now). auto: order (the order warehouse 99 buys in), priceVnd (cheapest warehouse), available.
Warehouse 1 stock = what the source publishes (refreshed every minute; the source checks accounts are live at purchase time, so you may get fewer — the shortfall is refunded). Warehouse 2 = a reference number from a source sharing the same stock, may be off by a few (null = could not be read — just buy); a shortfall becomes a partial order with the rest refunded. available = false ⇒ do not buy from that warehouse (out of stock / paused). Prices can change — read the catalog again before a large purchase.
示例curl
Python
Node.js
curl "https://otpgmail.net/v1/mail-shop/products" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"products": [
{
"warehouse": 1,
"code": "gm_cookie",
"name": "Short-lived live Gmail — with cookies + inbox link",
"description": "Short-lived personal Gmail account. You get the email · password · login cookies · an inbox link (add ?json to read the emails as JSON).",
"terms": "• Warranty covers ONLY the first successful sign-in, within 2 hours of purchase.\n• These Gmail accounts typically last 6–24 hours: use them right after purchase.",
"delivers": [
"email",
"password",
"cookies",
"inboxUrl",
"inboxJsonUrl"
],
"priceVnd": 3300,
"listPriceVnd": 3300,
"stock": 1840,
"available": true,
"maxQty": 500
},
{
"warehouse": 2,
"code": "gm_login",
"name": "Short-lived live Gmail — with a quick login link",
"description": "Short-lived personal Gmail account that has been used on a few services. You get the email · password · a quick login link.",
"terms": "• Warranty covers ONLY the first successful sign-in, within 2 hours of purchase.\n• Sign in with the link in a DESKTOP BROWSER.",
"delivers": [
"email",
"password",
"loginUrl"
],
"priceVnd": 6000,
"listPriceVnd": 6000,
"stock": 141,
"available": true,
"maxQty": 100
}
],
"auto": {
"warehouse": 99,
"priceVnd": 3300,
"available": true,
"order": [
1,
2
]
}
}
}可能的错误: UNAUTHORIZEDGET/v1/mail-shop/quote
Call quote to see the total first (nothing is reserved). Warehouse 99 ⇒ route[] = planned split by known stock; available = false ⇒ that quantity cannot be bought right now.
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
warehouse | 1 | 2 | 99 | 否 | Mail-shop warehouse: 1 = email + password + cookies + inbox link · 2 = email + password + login link · 99 = automatic, cheapest first. Send warehouse OR product |
product | string | 否 | Instead of warehouse: gm_cookie (= warehouse 1) | gm_login (= warehouse 2) |
quantity | number | 否 | Number of accounts (default 1) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/mail-shop/quote?warehouse=99&quantity=10" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"warehouse": 99,
"quantity": 10,
"unitPriceVnd": 3300,
"totalVnd": 38400,
"available": true,
"stock": null,
"maxQty": 500,
"route": [
{
"warehouse": 1,
"quantity": 8,
"unitPriceVnd": 3300
},
{
"warehouse": 2,
"quantity": 2,
"unitPriceVnd": 6000
}
]
}
}可能的错误: UNAUTHORIZED, VALIDATION_ERROR, MAILSHOP_PRODUCT_NOT_FOUNDPOST/v1/mail-shop/orders
Buy: you are charged at once, then the system buys from the source and delivers. The response is the order — usually already completed with items[]; status = pending ⇒ poll GET /v1/mail-shop/orders/{id} every 5 seconds. The call waits up to about 16 seconds for the delivery.
Idempotency-Key is required (400 without it). Not enough stock ⇒ MAILSHOP_OUT_OF_STOCK (error.details.stock = how many you can buy now, nothing charged); over the per-order limit ⇒ MAILSHOP_QTY_LIMIT (error.details.max). 502 MAILSHOP_SUPPLIER_ERROR = the source refused at once: a failed order was recorded and fully refunded (error.details.orderId) — retry with a new key.
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
warehouse | 1 | 2 | 99 | 否 | Mail-shop warehouse: 1 = email + password + cookies + inbox link · 2 = email + password + login link · 99 = automatic, cheapest first. Send warehouse OR product |
product | string | 否 | Instead of warehouse: gm_cookie (= warehouse 1) | gm_login (= warehouse 2) |
quantity | number | 否 | Number of accounts (default 1) |
Idempotency-Key | string | 是 | Header: a new UUID for every new purchase — reuse it only to retry that same purchase |
curl
Python
Node.js
# 必须携带 Idempotency-Key:每次购买都用新的 UUID——只有在超时后重试时才复用同一个密钥
# warehouse: 1 = cookies + inbox link · 2 = login link · 99 = cheapest first
curl -X POST "https://otpgmail.net/v1/mail-shop/orders" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--max-time 60 \
-d '{"warehouse":99,"quantity":5}'{
"success": true,
"data": {
"id": "p3x9k2m7q1w8e5r4",
"requestedWarehouse": 99,
"warehouse": 1,
"productCode": "gm_cookie",
"quantity": 2,
"deliveredCount": 2,
"unitPriceVnd": 3300,
"chargedVnd": 9300,
"refundVnd": 0,
"status": "completed",
"channel": "api",
"route": [
{
"warehouse": 1,
"quantity": 1,
"unitPriceVnd": 3300
},
{
"warehouse": 2,
"quantity": 1,
"unitPriceVnd": 6000
}
],
"message": null,
"createdAt": 1760000400,
"completedAt": 1760000409,
"items": [
{
"lineNo": 1,
"warehouse": 1,
"email": "[email protected]",
"password": "Pw7Xq!k3d",
"rawLine": "[email protected]|Pw7Xq!k3d||[{\"domain\":\".google.com\",\"name\":\"SID\",\"value\":\"g.a000…\"}]",
"cookies": "[{\"domain\":\".google.com\",\"name\":\"SID\",\"value\":\"g.a000…\"}]",
"inboxUrl": "https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d",
"inboxJsonUrl": "https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d?json",
"twoFaUrl": "https://otpgmail.net/2fa/Q7xK2mP9vL4tR8wN3cB6yH1d",
"inboxLinkExpiresAt": 1762592400
},
{
"lineNo": 2,
"warehouse": 2,
"email": "[email protected]",
"password": "Qp9!rT2",
"rawLine": "[email protected]!rT2----no----https://login.example/google/login?uid=s2c38a3b",
"loginUrl": "https://login.example/google/login?uid=s2c38a3b",
"otpUrl": "https://otpgmail.net/api/mail-shop/otp/p3x9k2m7q1w8e5r4.2.Zx8Kq2Lm4Np6Rt0Vw3Yb5c",
"otpService": "vinted"
}
]
}
}可能的错误: UNAUTHORIZED, VALIDATION_ERROR, MAILSHOP_PRODUCT_NOT_FOUND, MAILSHOP_DISABLED, MAILSHOP_QTY_LIMIT, MAILSHOP_OUT_OF_STOCK, INSUFFICIENT_BALANCE, MAILSHOP_SUPPLIER_ERROR, RATE_LIMITED, DUPLICATE_REQUEST, MAINTENANCEGET/v1/mail-shop/orders
Your mail-shop orders, newest first (page / pageSize, up to 100 per page) — without items[]: fetch an order to get its accounts. Orders bought on the web are listed too (channel = web).
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
page | number | 否 | Page (default 1) |
pageSize | number | 否 | Orders per page (default 20, max 100) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/mail-shop/orders?page=1&pageSize=20" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"items": [
{
"id": "p3x9k2m7q1w8e5r4",
"requestedWarehouse": 99,
"warehouse": 1,
"productCode": "gm_cookie",
"quantity": 2,
"deliveredCount": 2,
"unitPriceVnd": 3300,
"chargedVnd": 9300,
"refundVnd": 0,
"status": "completed",
"channel": "api",
"route": [
{
"warehouse": 1,
"quantity": 1,
"unitPriceVnd": 3300
},
{
"warehouse": 2,
"quantity": 1,
"unitPriceVnd": 6000
}
],
"message": null,
"createdAt": 1760000400,
"completedAt": 1760000409
}
],
"total": 1,
"page": 1,
"pageSize": 20
}
}可能的错误: UNAUTHORIZEDGET/v1/mail-shop/orders/{id}
One order with every account delivered so far (an order still delivering already shows what has arrived). route[] = which warehouse, how many, at what price.
Poll every 5 seconds while status = pending; completed / partial / failed / refunded are final. An order of many warehouse-1 accounts (cookies) can weigh several MB — stop polling once it is final.
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Order ID (data.id returned by the purchase) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"id": "p3x9k2m7q1w8e5r4",
"requestedWarehouse": 99,
"warehouse": 1,
"productCode": "gm_cookie",
"quantity": 2,
"deliveredCount": 2,
"unitPriceVnd": 3300,
"chargedVnd": 9300,
"refundVnd": 0,
"status": "completed",
"channel": "api",
"route": [
{
"warehouse": 1,
"quantity": 1,
"unitPriceVnd": 3300
},
{
"warehouse": 2,
"quantity": 1,
"unitPriceVnd": 6000
}
],
"message": null,
"createdAt": 1760000400,
"completedAt": 1760000409,
"items": [
{
"lineNo": 1,
"warehouse": 1,
"email": "[email protected]",
"password": "Pw7Xq!k3d",
"rawLine": "[email protected]|Pw7Xq!k3d||[{\"domain\":\".google.com\",\"name\":\"SID\",\"value\":\"g.a000…\"}]",
"cookies": "[{\"domain\":\".google.com\",\"name\":\"SID\",\"value\":\"g.a000…\"}]",
"inboxUrl": "https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d",
"inboxJsonUrl": "https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d?json",
"twoFaUrl": "https://otpgmail.net/2fa/Q7xK2mP9vL4tR8wN3cB6yH1d",
"inboxLinkExpiresAt": 1762592400
},
{
"lineNo": 2,
"warehouse": 2,
"email": "[email protected]",
"password": "Qp9!rT2",
"rawLine": "[email protected]!rT2----no----https://login.example/google/login?uid=s2c38a3b",
"loginUrl": "https://login.example/google/login?uid=s2c38a3b",
"otpUrl": "https://otpgmail.net/api/mail-shop/otp/p3x9k2m7q1w8e5r4.2.Zx8Kq2Lm4Np6Rt0Vw3Yb5c",
"otpService": "vinted"
}
]
}
}可能的错误: UNAUTHORIZED, NOT_FOUNDGET/v1/mail-shop/orders/{id}/download
The delivered accounts as .txt (text/plain), separated by |. The FIRST LINE is the column header: warehouse 1 email|password|cookies|inboxUrl · warehouse 2 email|password|loginUrl · a 99 order with both warehouses ⇒ email|password|cookies|inboxUrl|loginUrl (missing cells empty).
cookies is one-line JSON and may contain | — split email and password at the first two |, the link at the last | (with extra columns ⇒ the links after cookies at the last | separators — links never contain |; read the header line for their order). Nothing delivered yet ⇒ 409 VALIDATION_ERROR. Add <c>?otp=1</c> ⇒ warehouse 2 gets a LAST column <c>otpUrl</c> (the Vinted OTP link — empty when the account has none): email|password|loginUrl|otpUrl. Without it ⇒ exactly the columns above. Add <c>?twofa=1</c> ⇒ warehouse 1 gets a LAST column <c>twoFaUrl</c> (the 2FA link — same code as inboxUrl; GET ⇒ just the 6 digits; empty when the link is off / expired): email|password|cookies|inboxUrl|twoFaUrl. Combines with <c>otp=1</c> — fixed column order: …|loginUrl|otpUrl|twoFaUrl (a 99 order with both warehouses). Without it ⇒ the default layout above.
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Order ID (data.id returned by the purchase) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID/download" \
-H "Authorization: Bearer og_xxx..." \
-o mail-ORDER_ID.txtemail|password|cookies|inboxUrl
[email protected]|Pw7Xq!k3d|[{"domain":".google.com","name":"SID","value":"g.a000…"}]|https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d可能的错误: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERRORGET/v1/mail-shop/orders/{id}/items/{lineNo}/inbox
Latest emails of a WAREHOUSE-1 account (sender, subject, time, extracted code / link, sanitized full email). Without an API key: GET the account's inboxJsonUrl (same emails, JSON).
max = 10 (default) | 20 | 50 | 100 | 200 — more emails take longer (up to about a minute). Cached for 15 seconds; every real read counts toward the hourly limit. 409 VALIDATION_ERROR = a warehouse-2 account (no inbox reading); 409 PROVIDER_ERROR with details.dead = the account was locked or removed.
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Order ID (data.id returned by the purchase) |
lineNo | number | 是 | Account number within the order (items[].lineNo) |
max | number | 否 | Number of latest emails: 10 (default), 20, 50, 100 or 200 |
curl
Python
Node.js
# Warehouse 1 accounts only; max = 10 | 20 | 50 | 100 | 200
curl "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID/items/1/inbox?max=20" \
-H "Authorization: Bearer og_xxx..." \
--max-time 70
# Same emails without an API key: GET the account's inboxJsonUrl
curl "https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d?json&max=20"{
"success": true,
"data": {
"email": "[email protected]",
"letters": [
{
"key": "h:9f2c4e1a7b3d5c80",
"from": "Instagram [email protected]",
"subject": "350038 is your Instagram code",
"receivedAt": 1760000520,
"code": "350038",
"content": "<html><body><p>Enter this confirmation code in the app: 350038</p></body></html>"
}
],
"max": 10,
"total": 8,
"fetchedAt": 1760000530,
"cached": false
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED, PROVIDER_ERRORPOST/v1/mail-shop/orders/{id}/items/{lineNo}/inbox-link
Manage the inbox link (inboxUrl + inboxJsonUrl + twoFaUrl — one shared code) of a warehouse-1 account: action "regenerate" = NEW link (the old one stops working immediately; also re-enables a link you turned off), "off" = turn it off (inboxUrl / inboxJsonUrl / twoFaUrl become null). Returns the account (an element of items[]).
At most 60 changes per 10 minutes for your account.
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Order ID (data.id returned by the purchase) |
lineNo | number | 是 | Account number within the order (items[].lineNo) |
action | string | 是 | "regenerate" = new link (the old one stops working immediately) · "off" = turn the link off |
curl
Python
Node.js
# 生成新链接(旧链接立即失效);发送 {"action":"off"} 即可关闭链接
curl -X POST "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID/items/1/inbox-link" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-d '{"action":"regenerate"}'{
"success": true,
"data": {
"lineNo": 1,
"warehouse": 1,
"email": "[email protected]",
"password": "Pw7Xq!k3d",
"rawLine": "[email protected]|Pw7Xq!k3d||[{\"domain\":\".google.com\",\"name\":\"SID\",\"value\":\"g.a000…\"}]",
"cookies": "[{\"domain\":\".google.com\",\"name\":\"SID\",\"value\":\"g.a000…\"}]",
"inboxUrl": "https://otpgmail.net/mail/Hn4Vw8Zr2Lk6Tq9Pm3Xs7Bd1",
"inboxJsonUrl": "https://otpgmail.net/mail/Hn4Vw8Zr2Lk6Tq9Pm3Xs7Bd1?json",
"twoFaUrl": "https://otpgmail.net/2fa/Hn4Vw8Zr2Lk6Tq9Pm3Xs7Bd1",
"inboxLinkExpiresAt": 1762592400
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, RATE_LIMITEDGET/v1/mail-shop/orders/{id}/items/{lineNo}/status
Is a bought warehouse-1 account still alive: live = true / false, or null when it cannot be determined. Counts toward the hourly inbox-read limit.
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Order ID (data.id returned by the purchase) |
lineNo | number | 是 | Account number within the order (items[].lineNo) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID/items/1/status" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"email": "[email protected]",
"live": true,
"reason": null,
"checkedAt": 1760000600
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED, PROVIDER_ERRORGET/v1/mail-shop/orders/{id}/items/{lineNo}/2fa
The current 2FA code (6-digit TOTP, changes every 30 seconds) of a warehouse-1 account — some warehouse-1 accounts have 2-step verification on (Google asks for a code at a password sign-in). none = true ⇒ the account has no 2FA (code / expiresIn null). expiresIn = seconds the code stays valid — ask again after that. Without an API key: GET the account's twoFaUrl (the body is just the 6 digits).
The server caches the code until it expires (every call within those 30 seconds gets the same code) — it does NOT count toward the hourly inbox-read limit. Calling too fast ⇒ 429 (retryAfterSec); a warehouse-2 account ⇒ 409; a locked account ⇒ 409 (details.dead).
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 是 | Order ID (data.id returned by the purchase) |
lineNo | number | 是 | Account number within the order (items[].lineNo) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID/items/1/2fa" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"none": false,
"code": "424819",
"expiresIn": 24
}
}可能的错误: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED, PROVIDER_ERROR订单生命周期
重要说明:
- 订单处于 waiting_code 时,请每 3–5 秒轮询一次
GET /v1/orders/{id},以便及时拿到验证码。 - 第一个验证码一到达,状态就会立即变为 completed。
- 到
codeDeadlineAt仍无验证码(最多 25 分钟;仓库 2:20 分钟;部分仓库 4 的来源更短)⇒ 系统取消订单并 100% 退款(status = cancelled)。 otp[]数组最早的在前,最新的在最后。请用otp[otp.length - 1]读取最新的验证码(Python:otp[-1])。- completed 订单会自动继续接收验证码:继续轮询
GET /v1/orders/{id},每个新验证码都会出现在otp[]的末尾。如果服务发送多个验证码,订单有效期内额外验证码不收费;数量上限和时间窗口会在租用前显示(offers[]中的maxCodes和fee;在仓库 1,邮箱通常在最近一个验证码之后保持开启约 25 分钟)。如需明确请求再来一个验证码(例如注册后登录):POST /v1/orders/{id}/next-code,适用于所有仓库,只有超出免费时段的仓库 2 才会收费。 - 如果服务每个邮箱只允许一个验证码,
otp[]会停留在该验证码。响应保持正常,不会报错,由你决定何时停止轮询。 otp[].code通常是一个短验证码(数字 / 字母)或一个链接。无法提取验证码的邮件(欢迎邮件、新闻邮件、特殊格式的验证码等)会原样(HTML 或文本)放在otp[].code中。当它超过 80 个字符、包含换行或是 HTML 时,请把它当作完整邮件处理,并自行在其中查找验证码或链接。POST /v1/orders会等待服务商分配邮箱,通常约需 0.3 秒,高峰期(少见)最长可达 10 秒。请将客户端超时设为约 15 秒,不要在 5 秒后就放弃并提前重试;重试时请复用同一个Idempotency-Key。- 已收到验证码的订单无法取消或退款,取消调用会返回
ORDER_NOT_CANCELLABLE。 - 如需租用 @icloud.com 邮箱,请在创建订单时发送
"domain": "icloud.com"(Gmail 则省略;现有机器人无需修改)。如果服务不支持 iCloud,或 iCloud 已暂时停用,会返回DOMAIN_UNAVAILABLE,改用 Gmail 即可立即租用成功。订单生命周期、取消与退款规则以及读取验证码的方式与 Gmail 完全相同。 - 自有库存服务(
provider="inventory",例如 Cointiply,见GET /v1/services)仅限 iCloud;每封邮件以链接形式出现在otp[].code中,同时带有kind、label和完整的邮件正文text。一个订单在rules.windowMinutes分钟内(截至windowEndsAt)最多接收rules.maxCodes封邮件,不可取消(ORDER_NOT_CANCELLABLE),如果时间用完仍未收到邮件,订单会变为 cancelled,且不退款。如需在同一个邮箱上继续接收,请在canRerent为 true 时调用POST /v1/orders/{id}/rerent(价格:rerentPriceVnd)。其他所有服务 / 订单的provider均为"standard"——自 2026-10-24 起provider只有两个值:"standard"|"inventory"(实际仓库见warehouse);请只与"inventory"比较。 - 仓库 2 订单(
warehouse= 2):验证码在fee.freeHours小时内免费;免费时段结束后不再有新验证码到达,请调用POST /v1/orders/{id}/next-code并带上confirmFee= true(支付nextCodeFee.feePriceVnd)来开启新的时段,最长到nextCodeFee.hardUntil。常规服务的仓库 3 订单:24 小时内验证码免费,验证码到达前可取消,之后请使用rerent。仓库 4 订单:读取订单时新验证码会进入otp[],在rentExpiresAt之前免费;对于只发一个验证码的来源,收到第一个验证码后nextCodeBlockReason会变为"max_codes"。 - 同时有很多订单?用
POST /v1/orders/status每次最多轮询 100 个——请见单次调用查询多个订单状态一节。
速率限制
限制按 API 密钥计算(账号可能有单独的限制;没有时按网站默认限制):
- 100 次请求/秒:几秒内的短时突发
- 3000 次请求/分钟:每分钟的总流量
超出任一限制会返回 HTTP 429,带有
Retry-After: <seconds> 响应头和响应体 {"success":false,"error":{"code":"RATE_LIMITED",...}}。请先等待响应头中给出的秒数,再重试。 错误响应体中的 error.retryAfterSec 也给出相同的秒数(同时 error.retryable = true)。错误码
所有错误的结构相同:
{"success":false,"error":{"code":"...","message":"...","retryable":false,"retryAfterSec":null}}自 2026-10-17 起,每个 /v1 错误还包含
错误响应示例error.retryable 和 error.retryAfterSec(新增字段——现有客户端无需任何修改;details 保持不变)。retryable = true ⇒ 同一调用稍后可能成功:等待 retryAfterSec 秒(≥ 1 的整数)后再次发送——缺货 / 没有邮箱 / 仓库暂停 / 服务商错误 ⇒ 5 秒;429 RATE_LIMITED ⇒ 与 Retry-After 响应头相同;使用同一 Idempotency-Key 的第一个请求仍在处理时的 409 DUPLICATE_REQUEST ⇒ 2 秒。retryable = false(retryAfterSec = null)⇒ 在解决原因之前(余额、等待中订单上限、参数、API 密钥…)重试也没有用。POST /v1/orders 的 partial.reason 遵循同样的规则。 遇到表示缺货的 409 响应(OUT_OF_STOCK、WAREHOUSE_UNAVAILABLE、ALL_WAREHOUSES_EXHAUSTED)时,请等满 retryAfterSec 秒再重试——提前重试通常只会得到同样的回答——并在再次租用前查看 GET /v1/services 中的 stock / offers[].stock;已租的订单请用 webhook 跟踪,而不是频繁轮询。HTTP/1.1 409 Conflict
{
"success": false,
"error": {
"code": "OUT_OF_STOCK",
"message": "This service is currently out of stock",
"retryable": true,
"retryAfterSec": 5
}
}
HTTP/1.1 429 Too Many Requests
Retry-After: 1
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded, retry after 1s",
"retryable": true,
"retryAfterSec": 1
}
}
HTTP/1.1 402 Payment Required
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance to rent an inbox",
"retryable": false,
"retryAfterSec": null
}
}Webhook(推送通知)
每个账户注册一个 URL;一旦订单收到新验证码(
3 步完成设置order.code)或被取消 / 退款(order.cancelled),我们立即向该 URL POST 一条带签名的 JSON 事件。Webhook 为可选功能,覆盖账户的所有订单(网页和 API)。投递为尽力而为——请保留低频轮询作为兜底(见下文)。- 调用
PUT /v1/webhook并传入你的 URL(可选events)——保存data.secret(64 位十六进制字符):验证每次投递都需要它。再次调用 PUT 会保留同一个 secret。 POST /v1/webhook/test——我们立即发送一条ping事件,并返回你的服务器如何响应(ok、httpStatus、error)。- 你的端点必须在 5 秒内返回任意 2xx——先响应,再处理事件。其他任何结果(3xx、4xx、5xx、超时、连接错误)都算失败并会重试。
URL 必须以
请求格式http:// 或 https:// 开头(证书有效),且可从互联网访问:localhost、私有、链路本地、组播和保留地址(10.x、172.16–31.x、192.168.x、169.254.x、fc00::/7、fe80::/10…)以及 OTPGmail 自身的服务器都会以 VALIDATION_ERROR 拒绝。每次投递前都会重新解析主机名。不跟随重定向——请注册最终 URL。每次投递都是向你的 URL 发出的 HTTP POST,正文为 JSON(UTF-8),并带有以下请求头:
以你的 secret 为密钥,对
接收端示例(Python 和 Node 只用标准库;curl 命令向你自己的接收端发送一次签名正确的测试投递)X-OTPGmail-Timestamp + . + 原始请求正文计算 HMAC-SHA256(小写十六进制),并与 sha256= 之后的值做常量时间比较。密钥就是这串 64 个字符的 secret 本身(其 UTF-8 字节——不要按十六进制解码)。务必使用原始字节:解析后再序列化 JSON 会改变字节。拒绝与你的时钟相差超过 5 分钟的时间戳(防重放)。curl
Python
Node.js
# 本地测试:向你的接收端发送一条签名方式与我们完全相同的投递
SECRET="9f2c6d1e8b7a4f3c5e0d2b1a6c9e8f7d3b5a1c2e4f6a8b0d9c7e5f3a1b2c4d6e"
TS=$(date +%s)
BODY='{"event":"ping","deliveryId":"6f1d2c3b-8a9e-4f70-b1c2-d3e4f5a6b7c8","sentAt":'$TS'}'
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST "http://localhost:8080/" \
-H "Content-Type: application/json" \
-H "X-OTPGmail-Event: ping" \
-H "X-OTPGmail-Delivery: 6f1d2c3b-8a9e-4f70-b1c2-d3e4f5a6b7c8" \
-H "X-OTPGmail-Timestamp: $TS" \
-H "X-OTPGmail-Signature: sha256=$SIG" \
-d "$BODY"{
"event": "order.code",
"deliveryId": "6f1d2c3b-8a9e-4f70-b1c2-d3e4f5a6b7c8",
"sentAt": 1760700125,
"orderId": "k3m9x2p7q1w8e5r4",
"service": "fb",
"warehouse": 1,
"domain": "gmail.com",
"email": "[email protected]",
"code": "482913",
"receivedAt": 1760700124,
"status": "completed",
"otp": [
{
"code": "482913",
"receivedAt": 1760700124
}
]
}正文示例——order.cancelled{
"event": "order.cancelled",
"deliveryId": "0b7e4c2a-5d1f-4e8b-9a3c-2f6d8e1b4a7c",
"sentAt": 1760701630,
"orderId": "p8v2n5c1x7z3m9k4",
"service": "ig",
"warehouse": 2,
"domain": "gmail.com",
"email": "[email protected]",
"reason": "timeout",
"refundVnd": 1500,
"status": "cancelled"
}正文示例——ping{
"event": "ping",
"deliveryId": "3c9a1e7f-2b4d-4c6e-8f0a-5d7b9c1e3a2f",
"sentAt": 1760700000
}重试、暂停与历史- 投递失败后会在 10 秒、1 分钟、5 分钟、30 分钟和 2 小时后重试(共 5 次);最后一次仍失败则丢弃该事件(
failed)。 - 连续 50 次失败(所有事件合计)会暂停 webhook:
GET /v1/webhook显示pausedAt,不再发送任何内容,新事件也不会入队。 - 恢复方法:再次调用
PUT /v1/webhook(URL 不变也可以),或成功完成一次POST /v1/webhook/test——之后队列中等待的事件会被发送。 GET /v1/webhook/deliveries列出最近 100 次投递及其状态、尝试次数和最后一次错误;历史保留 7 天。
同一事件可能到达多次(例如你的 2xx 响应丢失时),事件也可能乱序到达(会并行发送多条)。保存已处理的
保留轮询作为兜底deliveryId 并跳过重复;对每个订单,以其最新数据(otp、status)为准,而不是到达顺序。Webhook 可能延迟(重试)或丢失(webhook 已暂停、你的服务器宕机)。请保留低频轮询——例如对仍处于
waiting_code 的订单每 30–60 秒调用一次 GET /v1/orders/{id}——并以 API 返回的订单对象为准。PUT/v1/webhook
注册或更新账户的 webhook。首次调用会创建并返回新的 secret;之后的调用保留 secret、重新启用 webhook,若已暂停则恢复。不传 events:新 webhook 接收两种事件,已有 webhook 保持不变。URL 必须是公网地址——否则返回 400 VALIDATION_ERROR(原因见 error.details.problem)。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 你的端点(http:// 或 https://,公网地址,最多 2048 个字符) |
events | string[] | 否 | 要发送的事件:["order.code", "order.cancelled"](默认两者都发) |
curl
Python
Node.js
# 保存 data.secret——验证每次投递都需要它
curl -X PUT "https://otpgmail.net/v1/webhook" \
-H "Authorization: Bearer og_xxx..." \
-H "Content-Type: application/json" \
-d '{"url":"https://your-server.example/otpgmail-webhook","events":["order.code","order.cancelled"]}'{
"success": true,
"data": {
"url": "https://your-server.example/otpgmail-webhook",
"events": [
"order.code",
"order.cancelled"
],
"enabled": true,
"secret": "9f2c6d1e8b7a4f3c5e0d2b1a6c9e8f7d3b5a1c2e4f6a8b0d9c7e5f3a1b2c4d6e",
"pausedAt": null,
"consecutiveFailures": 0,
"createdAt": 1760699000,
"updatedAt": 1760699000
}
}可能的错误: UNAUTHORIZED, VALIDATION_ERRORGET/v1/webhook
账户的 webhook,包括 secret、pausedAt 和 consecutiveFailures。未注册 webhook 时 data 为 null。
示例curl
Python
Node.js
curl "https://otpgmail.net/v1/webhook" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"url": "https://your-server.example/otpgmail-webhook",
"events": [
"order.code",
"order.cancelled"
],
"enabled": true,
"secret": "9f2c6d1e8b7a4f3c5e0d2b1a6c9e8f7d3b5a1c2e4f6a8b0d9c7e5f3a1b2c4d6e",
"pausedAt": 1760705400,
"consecutiveFailures": 50,
"createdAt": 1760699000,
"updatedAt": 1760705400
}
}可能的错误: UNAUTHORIZEDDELETE/v1/webhook
删除 webhook;队列中仍在等待的事件会被丢弃。始终返回 200——没有可删除的内容时 data.deleted 为 false。
示例curl
Python
Node.js
curl -X DELETE "https://otpgmail.net/v1/webhook" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"deleted": true
}
}可能的错误: UNAUTHORIZEDPOST/v1/webhook/test
立即向你的 URL 发送一条 ping 事件并报告结果:ok(你的服务器返回 2xx)、httpStatus、durationMs、error。测试成功会恢复已暂停的 webhook。每分钟最多 10 次测试;webhook 在全系统关闭期间返回 503 MAINTENANCE。
示例curl
Python
Node.js
curl -X POST "https://otpgmail.net/v1/webhook/test" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"ok": true,
"httpStatus": 200,
"durationMs": 143,
"error": null,
"deliveryId": "3c9a1e7f-2b4d-4c6e-8f0a-5d7b9c1e3a2f"
}
}你的服务器未返回 2xx 时的 200 响应
可能的错误: {
"success": true,
"data": {
"ok": false,
"httpStatus": 500,
"durationMs": 88,
"error": "HTTP 500",
"deliveryId": "7d2b9f4e-1c6a-4e3b-8d5f-0a9c2e7b1f4d"
}
}UNAUTHORIZED, NOT_FOUND, RATE_LIMITED, MAINTENANCEGET/v1/webhook/deliveries
最近的投递,最新的在前(保留 7 天):status = queued(下次尝试时间为 nextAt)| delivered | failed | paused,并附 attempts、lastHttpStatus 和 lastError。不返回事件正文。
参数| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
limit | number | 否 | 投递条数,1–100(默认 100) |
curl
Python
Node.js
curl "https://otpgmail.net/v1/webhook/deliveries?limit=20" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": [
{
"id": "0b7e4c2a-5d1f-4e8b-9a3c-2f6d8e1b4a7c",
"event": "order.cancelled",
"orderId": "p8v2n5c1x7z3m9k4",
"status": "queued",
"attempts": 1,
"lastHttpStatus": 503,
"lastError": "HTTP 503",
"nextAt": 1760701640,
"createdAt": 1760701630,
"deliveredAt": null
},
{
"id": "6f1d2c3b-8a9e-4f70-b1c2-d3e4f5a6b7c8",
"event": "order.code",
"orderId": "k3m9x2p7q1w8e5r4",
"status": "delivered",
"attempts": 1,
"lastHttpStatus": 200,
"lastError": null,
"nextAt": null,
"createdAt": 1760700124,
"deliveredAt": 1760700125
}
]
}可能的错误: UNAUTHORIZED