Documentación de la API
API pública /v1: autentícate con una clave de API y automatiza el alquiler de buzones desde tu propio software
Tu clave de API
No has iniciado sesión: inicia sesión para obtener tu propia clave de API.
Envía esta clave en el encabezado Authorization: Bearer <key> o en el parámetro de consulta ?api_key=<key>. Pulsa el icono del ojo para ver la clave completa; pulsa Generar nueva clave cuando necesites reemplazarla (la clave anterior deja de funcionar al instante).Resumen
URL base
https://otpgmail.netAutenticación
Añade tu clave de API a cada solicitud, ya sea en el encabezado
Authorization: Bearer og_xxx... (recomendado) o en la cadena de consulta ?api_key=og_xxx....Idempotency-Key (evita pedidos duplicados)
Toda solicitud
POST /v1/orders debería incluir el encabezado Idempotency-Key: <UUID nuevo por pedido>. Si la solicitud agota el tiempo de espera o se corta la conexión, envíala de nuevo con la misma clave: el servidor devuelve la respuesta original en lugar de crear otro pedido. Genera un UUID nuevo para cada pedido nuevo y reutiliza un UUID solo al reintentar.Muchos pedidos a la vez: estado en una sola llamada
¿Alquilas decenas o cientos de buzones? Consulta su estado en bloque:
POST /v1/orders/status devuelve hasta 100 pedidos por llamada (cuenta como una sola solicitud en tu límite) y GET /v1/orders lista tus pedidos; tras un reinicio, ?status=waiting_code te devuelve todos los pedidos que siguen esperando un código. Cada error indica además si conviene reintentar (error.retryable, error.retryAfterSec). Detalles: la sección Estado de muchos pedidos.Tipo de buzón: Gmail o iCloud
De forma predeterminada obtienes un buzón
@gmail.com. Para alquilar un buzón @icloud.com, añade "domain": "icloud.com" al cuerpo de POST /v1/orders. Si omites domain, se entiende gmail.com: las integraciones existentes siguen funcionando sin cambios. iCloud solo está disponible para los servicios cuyo campo icloud no sea null en GET /v1/services; el precio de iCloud puede ser distinto del de Gmail (consulta icloud.price). Cada respuesta de un pedido incluye un campo domain que indica qué tipo de buzón usa el pedido. Algunos servicios también ofrecen buzones @gmx.com (por ahora solo Pokemon, código pkm, almacén 2): envía "domain": "gmx.com"; un servicio lo ofrece cuando su offers[] en GET /v1/services tiene una entrada con domain = "gmx.com"; con cualquier otro servicio ⇒ DOMAIN_UNAVAILABLE. gmx.com no está disponible para lotes.Almacenes: un servicio, varias fuentes de buzones
Un servicio puede venderse desde varios almacenes, cada uno con su propio precio, stock y reglas de códigos (el array
offers[] de GET /v1/services). Envía "warehouse": 2 al crear un pedido para elegir uno; "warehouse": 99 ⇒ el sistema elige el almacén más barato con stock y pasa al siguiente si se agota (ideal para servicios que se agotan a menudo, como Facebook e Instagram); 0 ⇒ el almacén más barato con stock. Los clientes existentes no necesitan cambios: si omites warehouse ⇒ almacén 1 como antes, y price, stock e icloud describen el almacén 1 (servicios sin almacén 1 ⇒ el almacén más barato con stock). Detalles: consulta Almacenes.Alquiler por lotes
Los lotes se compran en la web (la página Alquiler por lotes); la API permite listar lotes y leer códigos. Gmail (Almacén 1): cada buzón tiene un
proxyUrl, un enlace de código que no necesita clave de API. iCloud (Almacén 3): buzones privados; lee los códigos con GET /v1/batches/{id}/mails/{mailId}/codes o abre readUrl en la web. Detalles: sección Alquiler por lotes.Comprar cuentas de Gmail de vida corta (mail shop)
Además de alquilar buzones, puedes comprar cuentas de Gmail completas de 2 almacenes — almacén 1: correo, contraseña, cookies + enlace del buzón (también en JSON); almacén 2: correo, contraseña + enlace de inicio de sesión rápido; el almacén 99 elige primero el más barato (la página Comprar Gmail de vida corta en la web). La API hace todo lo que hace esa página: ver almacenes, precios y stock, comprar (con
Idempotency-Key obligatoria), seguir la entrega, descargar las cuentas y leer sus correos. Detalles: sección Comprar Gmail de vida corta.Que tus clientes vean los códigos por sí mismos (página /otp)
¿Revendes buzones que alquilaste (en la web o por la API)? Activa un código para compartir en tu página Cuenta y envía a tu cliente el enlace
https://otpgmail.net/es/otp#k7m2q9:[email protected] (k7m2q9 = tu código para compartir): verá los últimos códigos de ese correo sin iniciar sesión y nunca verá precios ni tu saldo; la página solo realiza acciones gratuitas (nunca te cobra).Enlace JSON para herramientas / bots (tuyos o de tu cliente): toma el enlace con forma de ruta
https://otpgmail.net/es/otp/k7m2q9:[email protected] (abre la página /otp igual que el enlace con #) y añade ?json ⇒ el servidor devuelve un JSON con exactamente lo que muestra la página: cards[] — una tarjeta por cada servicio alquilado en ese correo (email, serviceName, state, items[] = hasta 5 códigos / enlaces más recientes, primero el más nuevo) — y serverTime. state: waiting espera el primer código · listening espera un código nuevo · received ya tiene códigos · needs_owner el vendedor debe ampliarlo · expired caducado · banned buzón bloqueado. Varios correos: sepáralos con comas (hasta 50). Sin clave de API ni cookies, CORS abierto; solo lectura (“Cargar nuevo OTP” / “Alquilar este correo de nuevo” siguen en la página /otp). Mismos límites antiadivinanza que la página: código incorrecto / el correo no tiene pedidos que mostrar ⇒ 404 NOT_FOUND; fallos repetidos o más de 30 lecturas por minuto para un correo ⇒ 429 RATE_LIMITED con retryAfter (segundos). Añadir ?json a un enlace /otp#… NO devuelve JSON: el navegador nunca envía al servidor lo que va después de #.curl "https://otpgmail.net/es/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
}
}Código de ejemplo completo en GitHub
Clientes listos para ejecutar en Python, Node.js, PHP, C# (.NET) y curl: alquilar un buzón, esperar el código, cancelar automáticamente para obtener el reembolso y alternar entre iCloud y Gmail cuando se agota el stock: github.com/letoan0902/otpgmail-email-otp-api (inglés) · thue-otp-gmail (vietnamita). Nota sobre Python: usa
requests en lugar de urllib a secas (su User-Agent predeterminado recibe un 403 de la CDN).Webhooks
En lugar de consultar cada pedido sin parar, registra una URL y recibe un POST firmado en cuanto un pedido recibe un código nuevo o se cancela / reembolsa — consulta Webhooks.
Endpoints
GET/v1/balance
Devuelve el saldo de la cuenta (en VND) correspondiente a la clave de API usada en la solicitud.
Ejemplocurl
Python
Node.js
curl "https://otpgmail.net/v1/balance" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"balance": 25000
}
}Errores posibles: UNAUTHORIZEDGET/v1/services
Lista los servicios habilitados: código, nombre, precio de Gmail (en VND) y stock actual de Gmail, un bloque icloud con el precio y el stock de los buzones @icloud.com, y un array offers[]: cada almacén que vende el servicio, con una entrada por almacén × tipo de buzón (precio, stock, número de códigos, reglas). Los datos se guardan en caché: el stock se actualiza cada 30–60 segundos aproximadamente y los precios con menos frecuencia.
icloud = null significa que el servicio aún no admite iCloud (el proveedor no lo ofrece o está desactivado temporalmente): solo se puede alquilar Gmail. Para los servicios con un bloque icloud, envía "domain": "icloud.com" al crear el pedido para obtener un buzón @icloud.com al precio icloud.price. price / stock / icloud.price describen el almacén 1, adonde van los pedidos sin warehouse (servicios sin almacén 1 ⇒ el almacén más barato con stock); los precios y el stock de cada almacén están en offers[] (consulta Almacenes). stock es la cifra que publica la fuente (almacén 1: se actualiza aproximadamente cada 30 segundos), no una promesa de que una sola llamada pueda alquilar tantos: los buzones se asignan de uno en uno y la fuente puede agotarse a mitad; entonces POST /v1/orders devuelve menos pedidos y un campo partial. Los almacenes 2 y 4 también muestran las cifras de sus fuentes.
Ejemplocurl
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
}
]
}Errores posibles: UNAUTHORIZEDPOST/v1/orders
Crea un pedido de un buzón que recibe códigos de verificación: Gmail de forma predeterminada, o iCloud si envías domain: "icloud.com". Tu saldo se cobra en cuanto el proveedor asigna el buzón. Devuelve una lista de pedidos (uno, o varios cuando quantity > 1).
Entrega parcial: quantity es un máximo
- Los buzones se asignan de uno en uno. Si el proveedor se queda sin buzones a mitad (por ejemplo, el almacén 1 no tiene más buzones para ese servicio en ese momento), la llamada se detiene y devuelve HTTP 200 con los pedidos creados hasta entonces:
datapuede ser más corto que quantity y solo se cobran los pedidos dedata. ElstockdeGET /v1/servicesno garantiza cuántos obtendrá una llamada. - En ese caso la respuesta incluye además un campo de primer nivel
partialjunto adata(no aparece si se entregó todo, así que los bots que solo leendatasiguen funcionando):requested,delivered,missingyreason={code, message, retryable, retryAfterSec}, dondecodees el error que detuvo la llamada (NO_MAILS_AVAILABLE,OUT_OF_STOCK,ALL_WAREHOUSES_EXHAUSTED,INSUFFICIENT_BALANCE,WAITING_LIMIT_REACHED…). - Quedarse sin buzones suele ser temporal: en el almacén 1 el proveedor a menudo vuelve a tener buzones a los pocos segundos. Si
reason.retryable= true, esperareason.retryAfterSecsegundos (3–5 s) y vuelve a pedir los buzones que faltan (missing) con unaIdempotency-KeyNUEVA (la clave anterior solo repite esta misma respuesta); repite hasta tenerlos todos. - Justo después de una entrega parcial, la siguiente llamada puede devolver 409
WAREHOUSE_UNAVAILABLE/OUT_OF_STOCKo 503NO_MAILS_AVAILABLE: es normal; sigue esperando 3–5 s entre llamadas y abandona solo cuando fallen muchas llamadas seguidas (por ejemplo 10 intentos, alrededor de un minuto); entonces inténtalo más tarde o envía"warehouse": 99para que el sistema pase a otros almacenes.retryable= false (INSUFFICIENT_BALANCE,WAITING_LIMIT_REACHED…) ⇒ resuelve primero la causa (recarga saldo o espera a que terminen los pedidos abiertos). - Las respuestas de error llevan los mismos dos campos dentro de
error:error.retryableyerror.retryAfterSec(por ejemplo, 409OUT_OF_STOCK⇒true, 5; 402INSUFFICIENT_BALANCE⇒false,null); consulta Códigos de error.
El encabezado Idempotency-Key (un UUID) es obligatorio. Genera un UUID nuevo para cada alquiler nuevo; al reintentar tras un timeout, reutiliza la MISMA clave para no crear un pedido duplicado. Esta llamada espera a que el proveedor asigne un buzón, por lo que normalmente tarda unos 0,3 segundos pero puede tardar hasta 10 segundos en horas punta (poco frecuente): configura el timeout de tu cliente en unos 15 segundos en lugar de rendirte a los 5 segundos y reintentar demasiado pronto.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
service | string | Sí | Código del servicio (de GET /v1/services, p. ej. "ig", "tg", "fb") |
quantity | number | No | Número MÁXIMO de buzones que alquilar (1 por defecto; como mucho 200 por solicitud): si el almacén se agota a mitad, recibes menos; cuenta data[] y lee partial |
domain | string | No | Tipo de buzón: "gmail.com" (predeterminado), "icloud.com" o "gmx.com". Si se omite = gmail.com: las integraciones existentes no necesitan cambios. iCloud solo se puede alquilar para servicios cuyo campo icloud no sea null en GET /v1/services; "gmx.com" solo para servicios con offers[].domain = "gmx.com" (por ahora solo Pokemon, código pkm); en caso contrario la llamada falla con DOMAIN_UNAVAILABLE. Cualquier otro valor ⇒ VALIDATION_ERROR. |
warehouse | number | No | Almacén donde alquilar. Si se omite = ALMACÉN 1, exactamente como antes de los múltiples almacenes (los servicios sin almacén 1 usan el más barato): los clientes existentes no necesitan cambios. 0 = el almacén más barato con stock (solo se prueba un almacén). 1–4 = ese almacén concreto (de offers[].warehouse en GET /v1/services); si no vende este servicio/tipo de buzón, está en pausa o se ha agotado ⇒ WAREHOUSE_UNAVAILABLE. 99 = AUTOMÁTICO: se prueban los almacenes con stock del más barato al más caro, pasando al siguiente cuando uno se agota o falla; si no queda ninguno ⇒ ALL_WAREHOUSES_EXHAUSTED. Recomendado para servicios que se agotan a menudo (Facebook, Instagram…). Consulta Almacenes. |
curl
Python
Node.js
# Genera una Idempotency-Key aleatoria para cada pedido NUEVO
# REUTILIZA la misma clave al reintentar tras un timeout (evita pedidos duplicados)
# domain: "gmail.com" (predeterminado, opcional) | "icloud.com" (alquila un buzón @icloud.com) | "gmx.com" (solo servicios que lo ofrecen, p. ej. Pokemon)
# warehouse: 0 | 1–4 | 99 (opcional: si lo omites, se usa el almacén 1 como antes; 0 = almacén más barato con stock; 99 = automático, pasa al siguiente cuando un almacén se agota; consulta offers[] en GET /v1/services)
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
}
]
}Respuesta de ejemplo: entrega parcial (quantity 5, 2 buzones entregados)
Errores posibles: {
"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}
Devuelve un pedido por su ID, incluidos los códigos recibidos hasta ahora (otp[]). Consulta este endpoint cada 3–5 segundos para recoger el código mientras el pedido está en estado waiting_code.
El array otp[] está ordenado por llegada: el más antiguo primero, el MÁS RECIENTE AL FINAL: lee el último código con otp[otp.length - 1]. En cada llamada el sistema consulta al proveedor por los pedidos aún abiertos, al ritmo propio de cada almacén (cada pocos segundos); llamar más a menudo solo devuelve los datos guardados. Tras el primer código sigue consultando: los códigos siguientes (si el servicio envía más de uno) se añaden al final de otp[] sin costo extra dentro del periodo del pedido; el límite y la ventana de tiempo se muestran antes de alquilar (maxCodes y fee en offers[]). Para asegurarte de que el sistema espera un código nuevo, llama a POST /v1/orders/{id}/next-code (cualquier almacén; solo el almacén 2 fuera de su ventana sin costo cobra un cargo). Si el servicio solo permite un código por buzón, otp[] se detiene en ese código: las respuestas siguen siendo normales, sin error. El campo warehouse indica qué almacén suministró el buzón (1–4); nextCodeFee es el cargo por más códigos (pedidos de los almacenes 2 y 3; null en los almacenes 1 y 4): consulta Almacenes. Desde el 2026-10-02 los pedidos también incluyen nextCodeBlocked (true = el buzón no puede recibir más códigos) y nextCodeBlockReason, el motivo: "email_banned" (el servicio bloqueó el correo: alquila uno nuevo), "activation_gone" (el proveedor cerró el buzón), "hard_expired" (terminó la ventana de códigos), "max_codes" (se alcanzó el número máximo de códigos), null (sin bloqueo / pedido antiguo sin motivo registrado). Son campos añadidos: los clientes existentes no necesitan cambios. ¿Consultas muchos pedidos? Envía hasta 100 ID por llamada a POST /v1/orders/status en lugar de un GET por pedido: los mismos objetos de pedido y una sola solicitud en tu límite.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | ID del pedido (el orderId devuelto al crear el pedido) |
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
}
}Errores posibles: UNAUTHORIZED, NOT_FOUNDPOST/v1/orders/{id}/cancel
Cancela el pedido y reembolsa el 100 % del precio. Solo se permite mientras el pedido está en estado waiting_code Y no se ha recibido ningún código.
Un pedido que ha recibido un código (aunque sea uno solo) no se puede cancelar ni reembolsar: esto se aplica tanto al sitio web como a la API. Los pedidos con rules.cancellable = false (servicios de inventario propio como Cointiply) no se pueden cancelar: una vez asignado el buzón no hay cancelación ni reembolso; la llamada de cancelación devuelve ORDER_NOT_CANCELLABLE. Los pedidos del almacén 3 de servicios normales (rules.cancellable = true) se pueden cancelar como cualquier otro pedido antes de que llegue un código.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | ID del pedido que se va a cancelar |
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
}
}Errores posibles: UNAUTHORIZED, NOT_FOUND, ORDER_NOT_CANCELLABLE, PROVIDER_ERRORPOST/v1/orders/{id}/rerent
Alquila de NUEVO el MISMO buzón para un pedido de un servicio de inventario propio (provider = "inventory", p. ej. Cointiply): crea un pedido NUEVO en ese buzón con una ventana de recepción nueva, al precio rerentPriceVnd del pedido anterior.
Solo funciona cuando el pedido anterior tiene canRerent = true (el último pedido de ese buzón, cuya ventana anterior ya terminó o recibió todos sus correos). El cobro se hace en cuanto la operación se completa: envía una Idempotency-Key y reutilízala al reintentar para que nunca se te cobre dos veces. La respuesta es { order, message }; order tiene la misma forma que GET /v1/orders/{id}. Almacén 2 (desde el 2026-10-09): un pedido cancelado, agotado por tiempo o vencido (buzón cerrado); llámalo cuando
ParámetroscanRerent = true (el último pedido de ese correo, sin otro pedido activo en él). El sistema reabre EXACTAMENTE ese correo como un pedido NUEVO (status = waiting_code) al precio rerentPriceVnd (ahora el 100% de tu precio) y espera un correo 20 minutos como cualquier pedido del almacén 2; si no llega nada, se cancela y se reembolsa el 100%. Correo bloqueado por el servicio ⇒ RERENT_EMAIL_BANNED; el proveedor ya no puede reabrirlo ⇒ RERENT_MAIL_GONE; almacén 2 caído temporalmente ⇒ PROVIDER_ERROR; en ninguno de los tres casos se cobra.| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | ID del pedido anterior del buzón (el que tiene canRerent = true) |
curl
Python
Node.js
# REUTILIZA la misma clave al reintentar tras un timeout (evita pedidos duplicados)
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"
}
}Errores posibles: 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
Pide el siguiente código en el MISMO buzón: funciona para pedidos de CUALQUIER almacén (warehouse 1–4) que ya recibieron su primer código (status = completed). Es gratis, salvo en los pedidos del almacén 2 cuya ventana sin costo ya terminó (nextCodeFee.feePriceVnd > 0): en ese caso envía confirmFee: true para pagar el cargo y abrir otra ventana de códigos (hasta el límite máximo nextCodeFee.hardUntil). Tu bot no necesita saber el almacén: sigue consultando GET /v1/orders/{id} (los códigos nuevos se añaden al final de otp[]) y llama a este endpoint cuando quieras que el sistema espere un código nuevo.
Qué hace esta llamada en cada almacén
- Almacén 1: el sistema espera solo el siguiente código después de cada código dentro del periodo del pedido (
rentExpiresAt); la llamada devuelve 200 y no cambia nada; si el buzón ya no puede recibir más códigos (límite alcanzado, buzón cerrado, caducado) ⇒ 409 connextCodeBlockReason. - Almacén 2: reabre el buzón en el proveedor (
reordered= true) y espera un correo durante 20 minutos; es gratis dentro de la ventana sin costo (nextCodeFee.feePriceVnd= 0) y fuera de ella debes enviarconfirmFee= true (si falta ⇒ 409NEXT_CODE_FEE_CONFIRM, sin cobro). El cargo solo se conserva si llega un código nuevo: sin código en 20 minutos ⇒ reembolso automático. Después de cada código, el sistema sigue esperando el siguiente 25 minutos más (listening= true,listenUntil). - Almacén 3 (servicios normales): vuelve a abrir una escucha breve, gratis dentro de la ventana sin costo (
nextCodeFee.freeUntil); pasada la ventana ⇒ 409: usaPOST /v1/orders/{id}/rerent. Los servicios de inventario propio como Cointiply (rules.cancellable= false) no admiten esta llamada (409): cada correo aparece solo durante la ventana. - Almacén 4: vuelve a leer el pedido en el proveedor y espera un código nuevo, gratis hasta
rentExpiresAt.
200 = el sistema está esperando un código nuevo para este pedido (listening = true, listenUntil = cuándo deja de esperar; reordered = true si el buzón se acaba de reabrir; chargedVnd si se cobró un cargo), o llegó un código nuevo justo al llamar (ya está en order.otp). 409 = no se pudo abrir una nueva espera: lee error.code; el motivo por el que el buzón no admite más códigos está en order.nextCodeBlockReason (GET /v1/orders/{id}). Un pedido que aún espera su primer código (waiting_code) ⇒ 409, todavía no hace falta llamar. Envía un Idempotency-Key en las llamadas de pago (reutilízalo al reintentar para que nunca se cobre dos veces). Proveedor inaccesible ⇒ 502 PROVIDER_ERROR, inténtalo más tarde. Códigos de error 409 de esta llamada: NEXT_CODE_FEE_CONFIRM (el almacén 2 requiere el cargo: vuelve a llamar con confirmFee: true), EMAIL_BANNED (el servicio bloqueó el correo: alquila otro; error.details.blockReason = "email_banned"), ORDER_EXPIRED (caducado, buzón cerrado, límite de códigos alcanzado o el servicio no tiene «siguiente código»; el motivo está en order.nextCodeBlockReason), ORDER_NOT_CANCELLABLE (el pedido aún no recibió su primer código). Los bots que tratan cualquier 409 de esta llamada como «no hay más códigos» siguen funcionando sin cambios. refresh: true (almacén 2): primero se comprueba si hay un correo nuevo (si acaba de llegar se entrega y no se reabre), luego se reabre el buzón en el proveedor — dentro de la ventana sin costo la espera se reinicia por 20 minutos; fuera de ella la espera conserva su fin (listenUntil) y no se cobra nada. La respuesta añade refreshed (true = reabierto ahora) y refreshAvailableAt (segundos unix; antes de ese momento otra actualización solo comprueba el correo; null = ya se puede). Otros almacenes: sin efecto (refreshed = false). Sin refresh la llamada funciona exactamente como antes.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | ID del pedido (orderId), de cualquier almacén |
confirmFee | boolean | No | true = aceptas pagar nextCodeFee.feePriceVnd para abrir otra ventana de códigos. No hace falta mientras feePriceVnd = 0. |
refresh | boolean | No | true = almacén 2, el pedido espera un código nuevo (listening = true) pero no llega: reabre ya el buzón en el proveedor (actualiza la espera) — la espera anterior puede haber dejado de recibir correos del lado del proveedor. Gratis; como máximo una vez cada 5 s por pedido. Si el pedido no está esperando ⇒ igual que una llamada normal. |
curl
Python
Node.js
# Gratis (nextCodeFee es null o feePriceVnd = 0: cualquier almacén salvo el 2 tras su ventana sin costo): no hace falta confirmFee
curl -X POST "https://otpgmail.net/v1/orders/ORDER_ID/next-code" \
-H "Authorization: Bearer og_xxx..."
# Almacén 2 tras su ventana sin costo: envía confirmFee = true para pagar 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
}
}Errores posibles: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, NEXT_CODE_FEE_CONFIRM, INSUFFICIENT_BALANCE, EMAIL_BANNED, ORDER_EXPIRED, ORDER_NOT_CANCELLABLE, PROVIDER_ERROR, DUPLICATE_REQUEST, MAINTENANCEEstado de muchos pedidos en una sola llamada
Los revendedores y bots que alquilan muchos buzones no deberían enviar un
GET /v1/orders/{id} por pedido: POST /v1/orders/status lee hasta 100 pedidos por llamada con los mismos objetos de pedido, y GET /v1/orders lista tus pedidos a partir de los datos guardados.- Consulta cada 3–5 segundos con
POST /v1/orders/status, hasta 100 ID de pedido por llamada; cada llamada cuenta como una sola solicitud en tu límite. - En cada llamada el sistema consulta al proveedor como máximo 20 pedidos abiertos de la lista (primero los que esperan su primer código, después los que llevan más tiempo sin consultarse) y devuelve el resto con los datos guardados; esos se consultan en las llamadas siguientes. Una llamada responde en unos 3 segundos.
- Quita un pedido de tu lista cuando sea definitivo: cancelled (reembolsado) o completed si ya no necesitas más códigos.
- Los ID que no existen o pertenecen a otra cuenta vuelven en
missing(sin 404): quítalos también. - Tras un reinicio, recupera todos los pedidos abiertos con
GET /v1/orders?status=waiting_code(siguenextCursor) y retoma las consultas conPOST /v1/orders/status.
POST/v1/orders/status
Devuelve hasta 100 de tus pedidos en una llamada. Cada elemento es exactamente lo que GET /v1/orders/{id} devuelve para ese pedido (status, otp[] con todos los códigos recibidos, warehouse…). Úsalo para consultar muchos pedidos abiertos a la vez.
data respeta el orden de ids; los ID repetidos se ignoran. Los ID que no existen o pertenecen a otra cuenta aparecen en missing (sin 404). Para los pedidos abiertos, el sistema consulta al proveedor igual que GET /v1/orders/{id}, pero como máximo para 20 pedidos por llamada (primero los que esperan su primer código y después los que llevan más tiempo sin consultarse) y devuelve el resto con los datos guardados (se consultan en las llamadas siguientes); una llamada responde en unos 3 segundos. Cada llamada cuenta como una sola solicitud en tu límite, lleve los ID que lleve. Más de 100 ID, una lista vacía o ID que no sean cadenas ⇒ 400 VALIDATION_ERROR.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
ids | string[] | Sí | ID de pedido (orderId): de 1 a 100 por llamada |
curl
Python
Node.js
# ID de los pedidos que esperas (orderId de POST /v1/orders): hasta 100 por llamada
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"
]
}Errores posibles: UNAUTHORIZED, VALIDATION_ERROR, RATE_LIMITEDGET/v1/orders
Lista tus pedidos, del más reciente al más antiguo, solo con los datos guardados: no pide códigos nuevos al proveedor (eso lo hace POST /v1/orders/status). Filtra por estado y fecha de creación; pagina con nextCursor.
Pasa nextCursor como cursor para obtener la página siguiente; nextCursor = null ⇒ última página. Las páginas nunca repiten ni se saltan un pedido, aunque se hayan creado muchos en el mismo segundo. Los estados internos nunca se listan (solo waiting_code, completed, cancelled y queued). Tras reiniciar tu bot: GET /v1/orders?status=waiting_code&limit=200, sigue nextCursor y luego consulta esos pedidos con POST /v1/orders/status.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
status | string | No | Solo este estado: waiting_code | completed | cancelled | queued (sin indicar ⇒ todos) |
since | number | No | Solo los pedidos creados en esta hora unix (segundos) o después |
limit | number | No | Pedidos por página: 1–200 (50 por defecto) |
cursor | string | No | nextCursor de la página anterior, sin modificar |
curl
Python
Node.js
# Tras un reinicio: reúne todos los pedidos que siguen esperando un código, página a página
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"
}Errores posibles: UNAUTHORIZED, VALIDATION_ERROR, RATE_LIMITEDAlmacenes
Cada entrada de
offers[] es una oferta = (servicio, almacén, tipo de buzón). Solo se listan los almacenes que venden en este momento; un almacén sin stock temporalmente permanece en la lista con stock = 0. Los números de almacén son fijos: 1, 2, 3, 4; pasa el mismo número en el parámetro warehouse al crear un pedido.Los clientes existentes no necesitan cambios. Si omites
warehouse ⇒ el mismo comportamiento de antes (almacén 1: espera de 25 minutos, los códigos adicionales llegan automáticamente dentro del periodo del pedido de 24 horas cuando el servicio envía más de uno); si envías 0 ⇒ almacén más barato con stock; si envías 99 ⇒ almacén automático, que pasa al siguiente cuando uno se agota (más abajo); todos los campos existentes se mantienen: solo se añaden campos nuevos (offers[], warehouse, nextCodeFee, listening, nextCodeBlocked…).Almacén automático: warehouse = 99 (recomendado para servicios que se agotan a menudo)
Campos de offers[]Envía
"warehouse": 99 y el sistema alquila en el almacén más barato con stock; si ese almacén se agota o falla, pasa al siguiente dentro de la misma llamada. Ideal para servicios que se agotan a menudo, como Facebook e Instagram: tu bot ya no tiene que leer offers[] ni reintentar almacén por almacén.- Orden de prueba: precio de menor a mayor (p. ej. 1500 → 1600 → 1700 VND); a igual precio ⇒ primero el almacén de número menor.
- Los almacenes que informan de stock cero (
stock= 0 enoffers[]) nunca se llaman: no se pierde tiempo. - Pasa al siguiente cuando el almacén actual está agotado o falla (
OUT_OF_STOCK,NO_MAILS_AVAILABLE,WAREHOUSE_UNAVAILABLE,PROVIDER_ERROR). - Los demás errores se devuelven de inmediato, sin probar el siguiente almacén:
INSUFFICIENT_BALANCE,WAITING_LIMIT_REACHED,QUANTITY_EXCEEDED,MAINTENANCE,SERVICE_DISABLED,DOMAIN_UNAVAILABLE… quantity> 1: si el primer almacén se queda corto, el resto sale del siguiente: los pedidos pueden abarcar varios almacenes; cada pedido lleva elwarehousey elpricedel almacén que lo emitió (y sigue las reglas de códigos de ese almacén).- Si se probaron todos los almacenes y no se pudo alquilar ningún buzón ⇒
ALL_WAREHOUSES_EXHAUSTED(HTTP 409, sin cobro): inténtalo más tarde. Si solo se alquilaron algunos buzones, recibes igualmente 200 con esos pedidos.
curl
Python
Node.js
# warehouse: 99: almacén más barato con stock; si se agota o falla ⇒ pasa automáticamente al siguiente
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}'¹ Los servicios que se venden solo desde el inventario propio, como Cointiply (
nextCodeFee: cargo por más códigos (pedidos de los almacenes 2 y 3)provider = "inventory", rules.cancellable = false), siguen rules: hasta rules.maxCodes correos en rules.windowMinutes minutos, sin cancelación y sin reembolso si se acaba el tiempo sin recibir ningún correo.Se incluye en
GET /v1/orders/{id} y en cada respuesta de pedido; null en los pedidos de los almacenes 1 y 4 (los códigos adicionales son gratis). Almacén 2: llama a POST /v1/orders/{id}/next-code cuando necesites otro código; si feePriceVnd es mayor que 0, debes enviar confirmFee = true. Almacén 3 (servicios normales): terminada la ventana sin costo, feePriceVnd es el precio de volver a alquilar el mismo buzón (rerent).curl
Python
Node.js
# warehouse: 0 | 1–4 | 99 (opcional: si lo omites, se usa el almacén 1 como antes; 0 = almacén más barato con stock; 99 = automático, pasa al siguiente cuando un almacén se agota; consulta offers[] en GET /v1/services)
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
}
}Alquiler por lotes
Los lotes se compran en la web (la página Alquiler por lotes); la API permite listar lotes y leer códigos. El dominio del correo determina el almacén: Gmail = Almacén 1, iCloud = Almacén 3 (buzones privados).
- Un lote se conserva durante 24 horas desde la compra: lee los códigos sin costo durante ese tiempo, sin límite en el número de códigos.
- Los buzones que no reciben ningún código no se reembolsan; solo se reembolsa (automáticamente) una diferencia en la cantidad.
- Almacén 1 (Gmail): cada buzón tiene un
proxyUrl: unGETdevuelve{"status":1,"code":"482913","all_codes":[...]}sin clave de API (el enlace es el secreto: mantenlo en privado). El endpoint de códigos de abajo también funciona. - Almacén 3 (iCloud):
proxyUrl=null: lee los códigos con el endpoint de abajo o abrereadUrl(página web). Cada buzón queda vinculado a tu cuenta para ese servicio; tras la caducidad, aún puedes alquilarlo de nuevo en la web. - Los correos sin código extraíble (correos de bienvenida, boletines…) se devuelven tal cual en
codeconisRaw: true(HTML o texto, hasta 20 000 caracteres).
GET/v1/batches
Tus lotes, del más reciente al más antiguo (paginados con page / pageSize, hasta 100). Cada lote tiene warehouse (1 = Gmail, 3 = iCloud), status (active | expired | failed), expiresAt, codeCount y readUrl.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
page | number | No | Página (1 por defecto) |
pageSize | number | No | Lotes por página (20 por defecto, máx. 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
}
}Errores posibles: UNAUTHORIZEDGET/v1/batches/{id}
Un lote con su mails[]: toma el id de cada buzón para leer sus códigos.
Lotes de Gmail (almacén 1): el proxyUrl de cada buzón funciona en tus scripts exactamente como antes. Lotes de iCloud (almacén 3): proxyUrl = null, lee los códigos con el endpoint de abajo. Los lotes de iCloud caducados incluyen rerentPriceVnd: el precio para alquilar de nuevo un buzón de forma individual (se hace en la web).
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | ID del lote (se muestra en la página Alquiler por lotes y en 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
}
]
}
}Errores posibles: UNAUTHORIZED, NOT_FOUNDGET/v1/batches/{id}/mails/{mailId}/codes
Lee los códigos de un buzón de un lote: el sistema consulta al proveedor si toca revisar el buzón y luego devuelve TODOS los códigos recibidos, del más antiguo al más reciente: el código más reciente es el último de codes[].
Consulta cada 5–10 segundos mientras esperas (llamar con más frecuencia solo devuelve los datos almacenados: polledAt no cambia). Los correos sin código extraíble se devuelven tal cual en code con isRaw: true. Los lotes caducados siguen devolviendo sus códigos almacenados, pero ya no se comprueban.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | ID del lote |
mailId | number | Sí | ID del buzón (mails[].id) |
poll | 0 | 1 | No | 0 = solo lee los códigos almacenados, no consulta al proveedor (1 por defecto) |
curl
Python
Node.js
# Vuelve a llamar cada 5–10 segundos mientras esperas; el código más reciente es el último de 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
}
}
}Errores posibles: 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- Las cuentas del almacén 1 (
gm_cookie) vienen sin correo de recuperación: recibes el correo, la contraseña, las cookies y el enlace del buzón (inboxUrl/inboxJsonUrl), nada más. - Forma recomendada de iniciar sesión: importar las cookies (
cookies, JSON) en un navegador, con una extensión de importación de cookies o con la importación de cookies de tu navegador antidetect, y luego abrir Gmail: la sesión ya está iniciada, normalmente sin pedir contraseña ni verificación. - ¿Solo necesitas los códigos o los correos? Léelos con
inboxUrl(página web) oinboxJsonUrl(JSON), sin clave de API y sin iniciar sesión en Google. - Iniciar sesión con la contraseña en un dispositivo o una IP nuevos puede hacer que Google pida una verificación (correo de recuperación, teléfono o un código); estas cuentas no tienen con qué superarla, así que usa las cookies o el enlace del buzón.
- ¿Revendes? Entrega a tus clientes las cookies y el enlace del buzón (la descarga .txt,
GET /v1/mail-shop/orders/{id}/download, traeemail|password|cookies|inboxUrlen una sola línea) y diles que inicien sesión con las cookies, no con la contraseña. - Las cuentas del almacén 2 (
gm_login) traen en su lugar un enlace de inicio de sesión rápido (loginUrl): ábrelo en un navegador de escritorio para entrar.
- 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.
Ejemplocurl
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
]
}
}
}Errores posibles: 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.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
warehouse | 1 | 2 | 99 | No | Mail-shop warehouse: 1 = email + password + cookies + inbox link · 2 = email + password + login link · 99 = automatic, cheapest first. Send warehouse OR product |
product | string | No | Instead of warehouse: gm_cookie (= warehouse 1) | gm_login (= warehouse 2) |
quantity | number | No | 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
}
]
}
}Errores posibles: 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.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
warehouse | 1 | 2 | 99 | No | Mail-shop warehouse: 1 = email + password + cookies + inbox link · 2 = email + password + login link · 99 = automatic, cheapest first. Send warehouse OR product |
product | string | No | Instead of warehouse: gm_cookie (= warehouse 1) | gm_login (= warehouse 2) |
quantity | number | No | Number of accounts (default 1) |
Idempotency-Key | string | Sí | Header: a new UUID for every new purchase — reuse it only to retry that same purchase |
curl
Python
Node.js
# Idempotency-Key obligatoria: un UUID nuevo por compra — reutiliza la misma clave solo para reintentar tras un timeout
# 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"
}
]
}
}Errores posibles: 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).
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
page | number | No | Page (default 1) |
pageSize | number | No | 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
}
}Errores posibles: 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.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | 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"
}
]
}
}Errores posibles: 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.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | 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/Q7xK2mP9vL4tR8wN3cB6yH1dErrores posibles: 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.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | Order ID (data.id returned by the purchase) |
lineNo | number | Sí | Account number within the order (items[].lineNo) |
max | number | No | 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
}
}Errores posibles: 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.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | Order ID (data.id returned by the purchase) |
lineNo | number | Sí | Account number within the order (items[].lineNo) |
action | string | Sí | "regenerate" = new link (the old one stops working immediately) · "off" = turn the link off |
curl
Python
Node.js
# Enlace nuevo (el anterior deja de funcionar al instante); envía {"action":"off"} para desactivarlo
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
}
}Errores posibles: 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.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | Order ID (data.id returned by the purchase) |
lineNo | number | Sí | 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
}
}Errores posibles: 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).
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | Sí | Order ID (data.id returned by the purchase) |
lineNo | number | Sí | 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
}
}Errores posibles: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED, PROVIDER_ERRORCiclo de vida del pedido
Notas importantes:
- Consulta
GET /v1/orders/{id}cada 3–5 segundos mientras el pedido esté en waiting_code para recoger el código sin demora. - En cuanto llega el primer código, el estado cambia a completed de inmediato.
- Sin código al llegar
codeDeadlineAt(como máximo 25 minutos; almacén 2: 20 minutos; algunas fuentes del almacén 4 tienen menos) ⇒ el sistema cancela el pedido y reembolsa el 100% (status = cancelled). - El array
otp[]está ordenado del más antiguo al más reciente. Lee el último código conotp[otp.length - 1](Python:otp[-1]). - Un pedido completed sigue recibiendo códigos automáticamente: sigue consultando
GET /v1/orders/{id}y cada código nuevo aparece al final deotp[]. Los códigos adicionales no cuestan nada extra dentro del periodo del pedido cuando el servicio envía más de uno; el límite y la ventana de tiempo se muestran antes de alquilar (maxCodesyfeeenoffers[]; en el almacén 1 el buzón suele permanecer abierto unos 25 minutos después del último código). Para pedir otro código de forma explícita (p. ej. para iniciar sesión tras registrarte):POST /v1/orders/{id}/next-code, que funciona en cualquier almacén; solo el almacén 2 fuera de su ventana sin costo cobra un cargo. - Si el servicio permite solo un código por buzón,
otp[]se detiene en ese código. Las respuestas siguen siendo normales, sin error: tú decides cuándo dejar de consultar. otp[].codesuele ser un código corto (dígitos / letras) o un enlace. Un correo del que no se pudo extraer el código (correos de bienvenida, boletines, formatos de código poco comunes…) se entrega íntegro (HTML o texto) enotp[].code: trátalo como un correo completo si tiene más de 80 caracteres, contiene un salto de línea o es HTML, y busca tú mismo el código o el enlace.POST /v1/ordersespera a que el proveedor asigne un buzón, por lo que normalmente tarda ~0,3 segundos pero puede tardar hasta 10 segundos en horas punta (poco frecuente). Configura un timeout de cliente de ≈ 15 segundos en lugar de rendirte a los 5 segundos y reintentar demasiado pronto; y cuando reintentes, reutiliza la mismaIdempotency-Key.- Un pedido que ha recibido un código no se puede cancelar ni reembolsar: la llamada de cancelación devuelve
ORDER_NOT_CANCELLABLE. - Para alquilar un buzón @icloud.com, envía
"domain": "icloud.com"al crear el pedido (omítelo para Gmail; los bots existentes no necesitan cambios). Si el servicio no admite iCloud, o iCloud está desactivado temporalmente, recibesDOMAIN_UNAVAILABLE: cambia a Gmail y el alquiler se completa de inmediato. El ciclo de vida, las reglas de cancelación y reembolso y la forma de leer los códigos son exactamente los mismos que con Gmail. - Los servicios de inventario propio (
provider="inventory", p. ej. Cointiply; consultaGET /v1/services) son solo de iCloud; cada correo llega como un enlace enotp[].code, junto conkind,labely el texto completo del correo entext. Un pedido recibe hastarules.maxCodescorreos enrules.windowMinutesminutos (hastawindowEndsAt), no se puede cancelar (ORDER_NOT_CANCELLABLE) y, si se acaba el tiempo sin recibir ningún correo, pasa a cancelled sin reembolso. Para seguir recibiendo en el mismo buzón, llama aPOST /v1/orders/{id}/rerentmientrascanRerentsea true (precio:rerentPriceVnd). Todos los demás servicios / pedidos tienenprovider="standard": desde el 2026-10-24providersolo tiene dos valores,"standard"|"inventory"(el almacén real está enwarehouse); compara solo con"inventory". - Pedidos del almacén 2 (
warehouse= 2): los códigos no tienen costo durantefee.freeHourshoras; cuando se cierra la ventana sin costo, dejan de llegar códigos nuevos: llama aPOST /v1/orders/{id}/next-codeconconfirmFee= true (pagandonextCodeFee.feePriceVnd) para abrir otra ventana, hastanextCodeFee.hardUntil. Pedidos del almacén 3 de servicios normales: códigos sin costo durante 24 horas, cancelables antes de que llegue un código y, después, usarerent. Pedidos del almacén 4: los códigos nuevos entran enotp[]al leer el pedido, gratis hastarentExpiresAt; con una fuente de un solo código, tras el primer códigonextCodeBlockReason="max_codes". - ¿Muchos pedidos a la vez? Consulta hasta 100 por llamada con
POST /v1/orders/status: mira la sección Estado de muchos pedidos en una sola llamada.
Límites de uso
Los límites se aplican por clave de API (una cuenta puede tener límites propios; si no, rige el límite predeterminado del sitio):
- 100 solicitudes/segundo: ráfagas cortas de unos segundos
- 3000 solicitudes/minuto: tráfico total por minuto
Si superas un límite, se devuelve HTTP 429 con un encabezado
Retry-After: <seconds> y el cuerpo {"success":false,"error":{"code":"RATE_LIMITED",...}}. Espera el número de segundos indicado en el encabezado antes de reintentar. El cuerpo del error lleva el mismo número de segundos en error.retryAfterSec (con error.retryable = true).Códigos de error
Todos los errores tienen la misma estructura:
{"success":false,"error":{"code":"...","message":"...","retryable":false,"retryAfterSec":null}}Desde el 2026-10-17 todo error de /v1 incluye además
Respuestas de error de ejemploerror.retryable y error.retryAfterSec (campos añadidos: los clientes existentes no necesitan cambios; details no cambia). retryable = true ⇒ la misma llamada puede funcionar más tarde: espera retryAfterSec segundos (un entero ≥ 1) y vuelve a enviarla; sin existencias / sin buzones / almacén en pausa / error del proveedor ⇒ 5 s; 429 RATE_LIMITED ⇒ el encabezado Retry-After; 409 DUPLICATE_REQUEST mientras la primera solicitud con el mismo Idempotency-Key sigue en curso ⇒ 2 s. retryable = false (retryAfterSec = null) ⇒ volver a llamar no servirá hasta que corrijas la causa (saldo, límite de pedidos en espera, parámetros, clave API…). partial.reason de POST /v1/orders sigue las mismas reglas. Ante una respuesta 409 de falta de stock (OUT_OF_STOCK, WAREHOUSE_UNAVAILABLE, ALL_WAREHOUSES_EXHAUSTED), espere los retryAfterSec segundos completos antes de reintentar — reintentar antes suele devolver la misma respuesta — y consulte stock / offers[].stock en GET /v1/services antes de volver a alquilar; siga los pedidos que ya tiene con webhooks en lugar de consultar sin parar.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
}
}Webhooks (notificaciones push)
Registra una URL por cuenta y le enviaremos por POST un evento JSON firmado en cuanto un pedido reciba un código nuevo (
Configuración en 3 pasosorder.code) o se cancele / reembolse (order.cancelled). Los webhooks son opcionales y cubren todos los pedidos de tu cuenta (web y API). La entrega es de mejor esfuerzo: mantén una consulta espaciada como respaldo (ver abajo).PUT /v1/webhookcon tu URL (y opcionalmenteevents) — guardadata.secret(64 caracteres hexadecimales): lo necesitas para verificar cada entrega. Volver a llamar a PUT conserva el mismo secret.POST /v1/webhook/test— enviamos al instante un eventopingy devolvemos lo que respondió tu servidor (ok,httpStatus,error).- Tu endpoint debe responder cualquier 2xx en menos de 5 segundos: responde primero y procesa el evento después. Cualquier otra cosa (3xx, 4xx, 5xx, tiempo de espera agotado, error de conexión) cuenta como fallo y se reintenta.
La URL debe empezar por
Formato de la peticiónhttp:// o https:// (con certificado válido) y ser accesible desde internet: localhost, las direcciones privadas, link-local, multicast y reservadas (10.x, 172.16–31.x, 192.168.x, 169.254.x, fc00::/7, fe80::/10…) y los propios servidores de OTPGmail se rechazan con VALIDATION_ERROR. El nombre de host se vuelve a resolver antes de cada entrega. No se siguen redirecciones: registra la URL final.Cada entrega es un POST HTTP a tu URL con cuerpo JSON (UTF-8) y estas cabeceras:
Calcula el HMAC-SHA256 con tu secret como clave sobre
Ejemplos de receptor (Python y Node solo usan la biblioteca estándar; el comando curl envía a tu propio receptor una entrega de prueba firmada correctamente)X-OTPGmail-Timestamp + . + el cuerpo sin procesar de la petición, en hex en minúsculas, y compáralo en tiempo constante con el valor que sigue a sha256=. La clave es la propia cadena secret de 64 caracteres (sus bytes UTF-8; no la decodifiques como hex). Usa los bytes sin procesar: analizar y volver a serializar el JSON los cambia. Rechaza las marcas de tiempo que se alejen más de 5 minutos de tu reloj (protección contra repeticiones).curl
Python
Node.js
# Prueba local: envía a tu receptor una entrega firmada exactamente como las nuestras
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
}
]
}Cuerpo de ejemplo — 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"
}Cuerpo de ejemplo — ping{
"event": "ping",
"deliveryId": "3c9a1e7f-2b4d-4c6e-8f0a-5d7b9c1e3a2f",
"sentAt": 1760700000
}Reintentos, pausa e historial- Una entrega fallida se reintenta tras 10 s, 1 min, 5 min, 30 min y 2 h (5 reintentos); si el último también falla, el evento se descarta (
failed). - 50 intentos fallidos seguidos (sumando todos los eventos) pausan el webhook:
GET /v1/webhookmuestrapausedAt, no se envía nada y los eventos nuevos no se ponen en cola. - Para reanudar: vuelve a llamar a
PUT /v1/webhook(puede ser la misma URL) o logra unPOST /v1/webhook/testcorrecto; después se envían los eventos que esperan en la cola. GET /v1/webhook/deliveriesmuestra las últimas 100 entregas con su estado, intentos y último error; el historial se guarda 7 días.
El mismo evento puede llegar más de una vez (por ejemplo, si se perdió tu respuesta 2xx) y los eventos pueden llegar desordenados (se envían varios en paralelo). Guarda los
Mantén la consulta como respaldodeliveryId ya procesados y omite las repeticiones; para cada pedido, confía en sus datos más recientes (otp, status) y no en el orden de llegada.Los webhooks pueden retrasarse (reintentos) o perderse (webhook en pausa, tu servidor caído). Mantén una consulta espaciada —por ejemplo,
GET /v1/orders/{id} cada 30–60 s para los pedidos que siguen en waiting_code— y toma el objeto de pedido de la API como fuente de verdad.PUT/v1/webhook
Registra o actualiza el webhook de tu cuenta. La primera llamada lo crea y devuelve un secret nuevo; las siguientes conservan el secret, vuelven a activar el webhook y lo reanudan si estaba en pausa. Sin events: ambos eventos para un webhook nuevo, sin cambios para uno existente. La URL debe ser una dirección pública; si no, 400 VALIDATION_ERROR (error.details.problem indica el motivo).
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
url | string | Sí | Tu endpoint (http:// o https://, dirección pública, máximo 2048 caracteres) |
events | string[] | No | Qué eventos enviar: ["order.code", "order.cancelled"] (por defecto ambos) |
curl
Python
Node.js
# Guarda data.secret: lo necesitas para verificar cada entrega
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
}
}Errores posibles: UNAUTHORIZED, VALIDATION_ERRORGET/v1/webhook
El webhook de tu cuenta, con su secret, pausedAt y consecutiveFailures. data es null si no hay ningún webhook registrado.
Ejemplocurl
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
}
}Errores posibles: UNAUTHORIZEDDELETE/v1/webhook
Elimina el webhook; los eventos que esperan en la cola se descartan. Siempre 200: data.deleted es false si no había nada que eliminar.
Ejemplocurl
Python
Node.js
curl -X DELETE "https://otpgmail.net/v1/webhook" \
-H "Authorization: Bearer og_xxx..."{
"success": true,
"data": {
"deleted": true
}
}Errores posibles: UNAUTHORIZEDPOST/v1/webhook/test
Envía ahora mismo un evento ping a tu URL e informa del resultado: ok (tu servidor respondió 2xx), httpStatus, durationMs, error. Una prueba correcta reanuda un webhook en pausa. Como máximo 10 pruebas por minuto; 503 MAINTENANCE mientras los webhooks estén desactivados en todo el sistema.
Ejemplocurl
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"
}
}Respuesta 200 cuando tu servidor no responde 2xx
Errores posibles: {
"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
Últimas entregas, las más recientes primero (se guardan 7 días): status = queued (próximo intento en nextAt) | delivered | failed | paused, con attempts, lastHttpStatus y lastError. No se devuelve el cuerpo de los eventos.
Parámetros| Nombre | Tipo | Obligatorio | Descripción |
|---|---|---|---|
limit | number | No | Número de entregas, 1–100 (por defecto 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
}
]
}Errores posibles: UNAUTHORIZED