Применить персональную скидкуЕсли вы арендуете OTP Gmail более 2000 Gmail в день для любых сервисов (кроме Facebook, Instagram и Apple) и хотите получить персональную скидку, напишите мне! Скидка может достигать 50% от базовой цены для тех, кто регистрирует аккаунты в достаточно больших объемах.
New update 29/09I have just added 4 new warehouses for all services + short-live accounts shop (24h) with email + pass for you to log in, feel free to check them out when you have time

Документация 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, добавьте "domain": "icloud.com" в тело запроса POST /v1/orders. Если domain не указан, подразумевается gmail.com — существующие интеграции продолжают работать без изменений. iCloud доступен только для сервисов, у которых поле icloud в GET /v1/services не равно null; цена iCloud может отличаться от цены Gmail (см. icloud.price). Каждый ответ по заказу содержит поле domain — оно показывает, какой тип ящика используется. Некоторые сервисы также предлагают ящики @gmx.com (сейчас только Pokemon, код pkm, склад 2): передайте "domain": "gmx.com" — сервис предлагает его, если в его offers[] из GET /v1/services есть запись с domain = "gmx.com"; для любого другого сервиса ⇒ DOMAIN_UNAVAILABLE. gmx.com недоступен для пакетов.
Склады: один сервис, несколько источников ящиков
Один сервис может продаваться с нескольких складов, у каждого из которых своя цена, наличие и правила кодов (массив offers[] в GET /v1/services). Передайте "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)
Помимо аренды ящиков, можно покупать Gmail-аккаунты целиком с 2 складов — склад 1: e-mail, пароль, cookies + ссылка на ящик (есть и JSON); склад 2: e-mail, пароль + ссылка для быстрого входа; склад 99 сам берёт сначала самый дешёвый (страница «Купить Gmail-аккаунт на короткий срок» на сайте). API умеет всё то же, что эта страница: склады, цены и остатки, покупка (нужен Idempotency-Key), отслеживание выдачи, скачивание аккаунтов и чтение их писем. Подробнее: раздел Покупка Gmail-аккаунтов на короткий срок.
Пусть ваши клиенты сами смотрят коды (страница /otp)
Перепродаёте арендованные ящики (через сайт или API)? Включите код доступа на странице «Профиль» и отправьте клиенту ссылку https://otpgmail.net/ru/otp#k7m2q9:[email protected] (k7m2q9 — ваш код доступа): он увидит последние коды этого ящика без входа в аккаунт и никогда не увидит цены или ваш баланс; страница выполняет только бесплатные действия (никогда ничего не списывает с вас).
JSON-ссылка для программ и ботов (ваших или клиента): возьмите ссылку в виде пути https://otpgmail.net/ru/otp/k7m2q9:[email protected] (она открывает страницу /otp так же, как ссылка с #) и добавьте ?json ⇒ сервер вернёт JSON ровно с теми данными, что показывает страница: cards[] — по карточке на каждый сервис, арендованный на этом email (email, serviceName, state, items[] = до 5 последних кодов / ссылок, новые первыми), — и serverTime. state: waiting ждёт первый код · listening ждёт новый код · received коды получены · needs_owner продавец должен продлить · expired срок истёк · banned ящик заблокирован. Несколько email — через запятую (не больше 50). Без API-ключа и cookie, CORS открыт; только чтение («Загрузить новый код» / «Арендовать этот e-mail снова» остаются на странице /otp). Те же ограничения против подбора, что и у страницы: неверный код / для email нет заказов для показа ⇒ 404 NOT_FOUND; повторные промахи или больше 30 запросов в минуту для одного email ⇒ 429 RATE_LIMITED с retryAfter (секунды). Если добавить ?json к ссылке /otp#…, JSON не вернётся: браузер никогда не отправляет на сервер часть после #.
curl "https://otpgmail.net/ru/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).
Вебхуки
Вместо постоянного опроса каждого заказа зарегистрируйте один URL и получайте подписанный POST, как только заказ получит новый код или будет отменён / возмещён — см. Вебхуки.

Эндпоинты

GET/v1/balance
Возвращает баланс аккаунта (в VND) для ключа API, использованного в запросе.
Пример
curl "https://otpgmail.net/v1/balance" \
  -H "Authorization: Bearer og_xxx..."
Пример ответа
{
  "success": true,
  "data": {
    "balance": 25000
  }
}
Возможные ошибки: UNAUTHORIZED
GET/v1/services
Возвращает список включённых сервисов: код, название, цену Gmail (в VND) и текущее наличие Gmail, блок icloud с ценой и наличием ящиков @icloud.com, а также массив offers[] — все склады, продающие сервис, по одной записи на каждую пару «склад × тип ящика» (цена, наличие, число кодов, правила). Данные кэшируются: наличие обновляется примерно раз в 30–60 секунд, цены — реже.
icloud = null означает, что сервис пока не поддерживает iCloud (поставщик его не предлагает или он временно отключён) — арендовать можно только Gmail. Для сервисов с блоком icloud передайте "domain": "icloud.com" при создании заказа, чтобы получить ящик @icloud.com по цене icloud.price. price / stock / icloud.price описывают склад 1 — туда попадают заказы без warehouse (для сервисов без склада 1 ⇒ самый дешёвый склад с наличием); цены и наличие по каждому складу — в offers[] (см. «Склады»). stock — число, которое публикует источник (склад 1: обновляется примерно раз в 30 секунд), и это не обещание, что один вызов арендует столько же: ящики выдаются по одному, и источник может закончиться на середине — тогда POST /v1/orders вернёт меньше заказов и поле partial. Склады 2 и 4 так же показывают числа своих источников.
Пример
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
    }
  ]
}
Возможные ошибки: UNAUTHORIZED
POST/v1/orders
Создаёт заказ на ящик для приёма кодов подтверждения — по умолчанию Gmail или iCloud, если передать domain: "icloud.com". Деньги списываются с баланса, как только поставщик выдаёт ящик. Возвращает список заказов (один или несколько, если quantity > 1).
Частичная выдача: quantity — это максимум
  • Ящики выдаются по одному. Если у поставщика они закончатся на середине (например, на складе 1 в этот момент нет ящиков для этого сервиса), вызов останавливается и возвращает HTTP 200 с уже созданными заказами — data может оказаться короче quantity, а списание идёт только за заказы из data. Значение stock в GET /v1/services не гарантирует, сколько получит один вызов.
  • Тогда в ответе рядом с 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 с) и запросите недостающие ящики (missing) с НОВЫМ Idempotency-Key (старый ключ лишь повторит этот же ответ); повторяйте, пока не получите все.
  • Сразу после частичной выдачи следующий вызов может вернуть 409 WAREHOUSE_UNAVAILABLE / OUT_OF_STOCK или 503 NO_MAILS_AVAILABLE — это нормально: продолжайте делать паузы по 3–5 с между вызовами и сдавайтесь, только когда много вызовов подряд неудачны (например, 10 попыток — около минуты); тогда повторите позже или отправьте "warehouse": 99, чтобы система перешла на другие склады. retryable = false (INSUFFICIENT_BALANCE, WAITING_LIMIT_REACHED…) ⇒ сначала устраните причину (пополните баланс или дождитесь завершения открытых заказов).
  • В ответах с ошибкой эти же два поля есть внутри error: error.retryable и error.retryAfterSec (например, 409 OUT_OF_STOCK ⇒ true, 5; 402 INSUFFICIENT_BALANCE ⇒ false, null) — см. «Коды ошибок».
Заголовок Idempotency-Key (UUID) обязателен. Для каждой новой аренды генерируйте новый UUID; при повторе после таймаута используйте ТОТ ЖЕ ключ, чтобы не создать дубль заказа. Этот вызов ждёт, пока поставщик выдаст ящик, поэтому обычно занимает около 0,3 секунды, но в пиковые часы (редко) может занять до 10 секунд — установите таймаут клиента около 15 секунд, а не сдавайтесь через 5 секунд и не повторяйте запрос раньше времени.
Параметры
ИмяТипОбязательныйОписание
servicestringДаКод сервиса (из GET /v1/services, например "ig", "tg", "fb")
quantitynumberНетМАКСИМАЛЬНОЕ число арендуемых ящиков (по умолчанию 1; не больше 200 за запрос) — если склад закончится на середине, вы получите меньше: считайте data[] и читайте partial
domainstringНетТип ящика: "gmail.com" (по умолчанию), "icloud.com" или "gmx.com". Если не указан — gmail.com, существующим интеграциям ничего менять не нужно. iCloud можно арендовать только для сервисов, у которых поле icloud в GET /v1/services не равно null; "gmx.com" — только для сервисов с offers[].domain = "gmx.com" (сейчас только Pokemon, код pkm); иначе вызов завершится ошибкой DOMAIN_UNAVAILABLE. Любое другое значение ⇒ VALIDATION_ERROR.
warehousenumberНетСклад, с которого арендовать. Если не указан = СКЛАД 1, ровно как до появления нескольких складов (для сервисов без склада 1 — запасной вариант: самый дешёвый) — существующим клиентам ничего менять не нужно. 0 = самый дешёвый склад с наличием (пробуется только один склад). 1–4 = именно этот склад (из offers[].warehouse в GET /v1/services); если он не продаёт этот сервис/тип ящика, приостановлен или товар в нём закончился ⇒ WAREHOUSE_UNAVAILABLE. 99 = АВТОМАТИЧЕСКИ: склады с наличием пробуются от самого дешёвого к самому дорогому, при отсутствии товара или сбое происходит переход к следующему; если не осталось ни одного ⇒ ALL_WAREHOUSES_EXHAUSTED — рекомендуется для сервисов, которые часто заканчиваются (Facebook, Instagram…). См. «Склады».
Пример
# Для каждого НОВОГО заказа генерируйте случайный Idempotency-Key
# При повторе после таймаута используйте ТОТ ЖЕ ключ (защита от дублей заказов)
# domain: "gmail.com" (по умолчанию, можно не указывать) | "icloud.com" (аренда ящика @icloud.com) | "gmx.com" (только сервисы, где он есть, например Pokemon)
# warehouse: 0 | 1–4 | 99 (необязательно — без него склад 1, как раньше; 0 = самый дешёвый склад с наличием; 99 = автоматически, при исчерпании переходит к следующему складу; см. offers[] в 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
    }
  ]
}
Пример ответа — частичная выдача (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, MAINTENANCE
GET/v1/orders/{id}
Возвращает заказ по ID вместе с уже полученными кодами (otp[]). Опрашивайте этот эндпоинт раз в 3–5 секунд, чтобы получить код, пока заказ в статусе waiting_code.
Массив otp[] упорядочен по времени прихода: сначала самые старые, САМЫЙ НОВЫЙ — ПОСЛЕДНИЙ; последний код читается как otp[otp.length - 1]. При каждом вызове система проверяет поставщика по ещё открытым заказам в собственном ритме каждого склада (раз в несколько секунд) — более частые вызовы просто возвращают сохранённые данные. После первого кода продолжайте опрос: следующие коды (если сервис присылает больше одного) добавляются в конец otp[] без доплаты в течение срока заказа — лимит и окно времени показываются до аренды (maxCodes и fee в offers[]); чтобы точно знать, что система ждёт новый код, вызовите POST /v1/orders/{id}/next-code (любой склад; плату берёт только склад 2 вне окна без доплаты). Если сервис допускает только один код на ящик, otp[] останавливается на этом коде — ответы остаются обычными, без ошибки. Поле warehouse показывает, какой склад выдал ящик (1–4); nextCodeFee — плата за дополнительные коды (заказы складов 2 и 3; null для складов 1 и 4) — см. «Склады». С 2026-10-02 в заказах также есть nextCodeBlocked (true = ящик больше не может получать коды) и nextCodeBlockReason — причина: "email_banned" (ящик заблокирован сервисом — арендуйте новый), "activation_gone" (поставщик закрыл ящик), "hard_expired" (окно приёма кодов закрыто), "max_codes" (получено максимальное число кодов), null (не заблокирован / более старый заказ без записанной причины). Это добавленные поля — существующим клиентам ничего менять не нужно. Опрашиваете много заказов? Отправляйте до 100 ID за вызов в POST /v1/orders/status вместо отдельного GET на каждый заказ — те же объекты заказов, один запрос в счёт лимита.
Параметры
ИмяТипОбязательныйОписание
idstringДаID заказа (orderId, возвращённый при создании заказа)
Пример
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_FOUND
POST/v1/orders/{id}/cancel
Отменяет заказ и возвращает 100 % цены. Допускается только пока заказ в статусе waiting_code И ни один код не получен.
Заказ, получивший код (даже один), нельзя отменить или вернуть — это относится и к сайту, и к API. Заказы с rules.cancellable = false (сервисы с собственным запасом ящиков, например Cointiply) отменить нельзя: после выдачи ящика ни отмены, ни возврата нет — вызов отмены возвращает ORDER_NOT_CANCELLABLE. Заказы со склада 3 для обычных сервисов (rules.cancellable = true) можно отменить, как и любые другие, до прихода кода.
Параметры
ИмяТипОбязательныйОписание
idstringДаID отменяемого заказа
Пример
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_ERROR
POST/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 (последний заказ этого e-mail, и на нём нет других активных заказов). Система заново открывает ТОТ ЖЕ e-mail как НОВЫЙ заказ (status = waiting_code) по цене rerentPriceVnd (сейчас 100 % вашей цены) и ждёт письмо 20 минут, как любой заказ склада 2, — нет письма ⇒ заказ отменяется с возвратом 100 %. Ящик заблокирован сервисом ⇒ RERENT_EMAIL_BANNED; поставщик больше не может его открыть ⇒ RERENT_MAIL_GONE; склад 2 временно недоступен ⇒ PROVIDER_ERROR — ни в одном из трёх случаев деньги не списываются.
Параметры
ИмяТипОбязательныйОписание
idstringДаID прежнего заказа на этом ящике (с canRerent = true)
Пример
# При повторе после таймаута используйте ТОТ ЖЕ ключ (защита от дублей заказов)
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, MAINTENANCE
POST/v1/orders/{id}/next-code
Запрашивает следующий код на ТОМ ЖЕ ящике — работает для заказов ЛЮБОГО склада (warehouse 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 (без него ⇒ 409 NEXT_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 вызов работает в точности как раньше.
Параметры
ИмяТипОбязательныйОписание
idstringДаID заказа (orderId) — любой склад
confirmFeebooleanНетtrue = вы согласны оплатить nextCodeFee.feePriceVnd, чтобы открыть ещё одно окно кодов. Не нужен, пока feePriceVnd = 0.
refreshbooleanНетtrue = склад 2, заказ ждёт новый код (listening = true), но код не приходит: сразу заново открыть ящик у поставщика (обновить ожидание) — прежнее ожидание могло перестать получать письма на стороне поставщика. Бесплатно; не чаще одного раза в 5 с на заказ. Заказ не ждёт ⇒ как обычный вызов.
Пример
# Бесплатно (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 выводит список ваших заказов из сохранённых данных.
  • Опрашивайте каждые 3–5 секунд через POST /v1/orders/status, до 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.
Параметры
ИмяТипОбязательныйОписание
idsstring[]ДаID заказов (orderId) — от 1 до 100 за вызов
Пример
# ID заказов, которые вы ждёте (orderId из POST /v1/orders), — до 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_LIMITED
GET/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.
Параметры
ИмяТипОбязательныйОписание
statusstringНетТолько этот статус: waiting_code | completed | cancelled | queued (не указан ⇒ все)
sincenumberНетТолько заказы, созданные начиная с этого unix-времени (в секундах)
limitnumberНетЗаказов на странице: 1–200 (по умолчанию 50)
cursorstringНетnextCursor предыдущей страницы, без изменений
Пример
# После перезапуска: соберите все заказы, которые ещё ждут код, страница за страницей
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 (рекомендуется для сервисов, которые часто заканчиваются)
Передайте "warehouse": 99 — система арендует с самого дешёвого склада с наличием; если на этом складе нет товара или он даёт сбой, она переходит к следующему в рамках того же вызова. Удобно для сервисов, которые часто заканчиваются, например Facebook и Instagram — вашему боту больше не нужно читать offers[] и перебирать склады по очереди.
  • Порядок перебора: цена от низкой к высокой (например, 1 500 → 1 600 → 1 700 VND); при равных ценах ⇒ сначала склад с меньшим номером.
  • Склады с нулевым наличием (stock = 0 в offers[]) не вызываются никогда — время не тратится.
  • Переход к следующему происходит, когда на текущем складе нет товара или он даёт сбой (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 с этими заказами.
Пример: аренда Gmail для Facebook с warehouse = 99
# 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}'
Поля offers[]
ПолеТипОписание
warehouse1 | 2 | 3 | 4Номер склада: 1, 2, 3 или 4.
domainstringТип ящика в предложении: gmail.com, icloud.com или gmx.com (дополнительный домен — только у некоторых сервисов, сейчас Pokemon).
pricenumberЦена одного ящика с этого склада (VND).
stocknumberОстаток ящиков. 0 = нет в наличии — аренда со склада 1 или 2 вернёт WAREHOUSE_UNAVAILABLE; склад 4 всё равно пробуется (его источники часто выдают почту, хотя сообщают 0) и вернёт OUT_OF_STOCK, если ничего нет.
maxCodesnumber | nullМаксимум кодов, которые может получить один ящик; null = без ограничений / неизвестно (см. fee).
rulesobject | nullПравила окна для склада 3 (windowMinutes, maxCodes, cancellable, refundOnTimeout, rerentRatio); null для складов 1, 2 и 4.
feeobject | nullОкно дополнительных кодов: freeHours часов без доплаты с момента аренды, затем каждое продление стоит paidPct % стоимости аренды, максимум maxHours часов с момента аренды (null = без ограничений). fee = null: платы нет — дополнительные коды не требуют доплаты в течение срока заказа, если сервис присылает больше одного (до maxCodes), как на складе 1.
isDefaultbooleantrue для самого дешёвого склада с наличием (при равенстве ⇒ меньший номер склада) — именно он используется при warehouse = 0 и пробуется первым при 99.
Правила по складам
ПравилоСклад 1Склад 2Склад 3Склад 4
Ожидание первого кода25 мин20 мин25 мин ¹До 25 минут (у некоторых источников меньше — точный срок в codeDeadlineAt заказа)
Нет кодаАвтоотмена, возврат 100 %Автоотмена, возврат 100 %Автоотмена, возврат 100 % ¹Автоотмена, возврат 100 %
Отмена до прихода кодаДаДаДа ¹Да
Кодов на ящикПо maxCodesБез ограниченийБез ограничений ¹По maxCodes (обычно null = без ограничений; у некоторых предложений — 1)
Дополнительные кодыБез доплаты в течение 24-часового срока заказа, если сервис присылает больше кодов; автоматически (дополнительный вызов не нужен). Ящик обычно остаётся открытым около 25 минут после последнего кода.Без доплаты fee.freeHours ч (сейчас Gmail — 6 ч, iCloud — 12 ч); затем каждое продление стоит fee.paidPct % цены через next-code, максимум fee.maxHours (Gmail — 24 ч, iCloud — 10 дней)Без доплаты 24 часа; затем можно снова арендовать тот же ящик (rerent) за 50 % ценыБез доплаты в течение 24-часового срока заказа, как на складе 1 — новые коды появляются при чтении заказа; next-code сразу перечитывает его.
¹ Сервисы, которые продаются только из собственного запаса ящиков, например Cointiply (provider = "inventory", rules.cancellable = false), подчиняются rules: до rules.maxCodes писем в течение rules.windowMinutes мин, без отмены и без возврата, если время вышло, а письма не пришло.
nextCodeFee — плата за дополнительные коды (заказы складов 2 и 3)
Входит в GET /v1/orders/{id} и в каждый ответ по заказу; null для заказов складов 1 и 4 (дополнительные коды бесплатны). Склад 2: вызывайте POST /v1/orders/{id}/next-code, когда нужен ещё один код, — если feePriceVnd больше 0, нужно передать confirmFee = true. Склад 3 (обычные сервисы): после окна без доплаты feePriceVnd — цена повторной аренды того же ящика (rerent).
ПолеТипОписание
feePriceVndnumberСумма, которая будет списана, если вызвать next-code сейчас; 0 = окно без доплаты ещё открыто.
freeUntilnumber | nullКонец текущего окна без доплаты (unix-секунды).
hardUntilnumber | nullЖёсткий предел (unix-секунды) — после этого времени кодов больше не будет.
paidPctnumber% стоимости аренды, списываемый за каждое дополнительное окно.
freeHoursnumberЧисло часов без доплаты в одном окне кодов (склад 2: Gmail 6, iCloud 12; склад 3: 24).
Пример: получить список складов, затем арендовать с одного из них
# warehouse: 0 | 1–4 | 99 (необязательно — без него склад 1, как раньше; 0 = самый дешёвый склад с наличием; 99 = автоматически, при исчерпании переходит к следующему складу; см. offers[] в 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}'
Пример заказа склада 2 (GET /v1/orders/{id})
{
  "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 символов).
Поля ящика в пакете (mails[])
ПолеТипОписание
idnumberID ящика — параметр {mailId} эндпоинта чтения кодов
emailstringАдрес e-mail
urlstringСклад 1: = proxyUrl · склад 3: = readUrl (оставлено для старых скриптов / файлов экспорта)
proxyUrlstring | nullСклад 1: ссылка на код, не требующая входа · склад 3: null
readUrlstringВеб-страница для чтения кодов пакета
codeCountnumberЧисло полученных кодов
lastCodestring | nullСамый новый код (необработанное письмо: первые 300 символов — полный текст в эндпоинте чтения кодов)
lastIsRawbooleantrue = самый новый код — необработанное письмо
checkedAtnumber | nullКогда система в последний раз проверяла поставщика по этому ящику (unix-секунды; null = никогда)
GET/v1/batches
Ваши пакеты, сначала новые (постраничный вывод через page / pageSize, до 100). У каждого пакета есть warehouse (1 = Gmail, 3 = iCloud), status (active | expired | failed), expiresAt, codeCount и readUrl.
Параметры
ИмяТипОбязательныйОписание
pagenumberНетСтраница (по умолчанию 1)
pageSizenumberНетПакетов на странице (по умолчанию 20, максимум 100)
Пример
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
  }
}
Возможные ошибки: UNAUTHORIZED
GET/v1/batches/{id}
Один пакет с его mails[] — берите id каждого ящика, чтобы читать его коды.
Пакеты Gmail (склад 1): proxyUrl каждого ящика работает в ваших скриптах так же, как раньше. Пакеты iCloud (склад 3): proxyUrl = null, читайте коды через эндпоинт ниже. У истёкших пакетов iCloud есть rerentPriceVnd — цена повторной аренды одного ящика по отдельности (выполняется на сайте).
Параметры
ИмяТипОбязательныйОписание
idstringДаID пакета (показан на странице «Пакетная аренда» и в readUrl)
Пример
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_FOUND
GET/v1/batches/{id}/mails/{mailId}/codes
Читает коды одного ящика из пакета: система проверяет поставщика, если подошло время проверки ящика, а затем возвращает ВСЕ полученные коды, сначала самые старые — самый новый код последний в codes[].
Пока ждёте, опрашивайте раз в 5–10 секунд (более частые вызовы просто возвращают сохранённые данные — polledAt не меняется). Письма, из которых не удалось извлечь код, возвращаются дословно в code с isRaw: true. Истёкшие пакеты по-прежнему возвращают сохранённые коды, но больше не проверяются.
Параметры
ИмяТипОбязательныйОписание
idstringДаID пакета
mailIdnumberДаID ящика (mails[].id)
poll0 | 1Нет0 = только прочитать сохранённые коды, поставщика не проверять (по умолчанию 1)
Пример
# Пока ждёте, вызывайте снова каждые 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_LIMITED

Buy 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
WarehouseEach account deliversDefault priceStock
1gm_cookieemail · password · cookies · inboxUrl (inbox link) · inboxJsonUrl (emails as JSON) · twoFaUrl (2FA link — just the 6 digits)3,300 VNDA number — published by the source, refreshed every minute. The source checks that accounts are live at purchase time, so you may get fewer — the shortfall is refunded (a partial order; warehouse 99: the rest goes to the next warehouse)
2gm_loginemail · password · loginUrl (quick login link — open it in a desktop browser) · otpUrl (Vinted OTP link — when delivers lists it)6,000 VNDA REFERENCE number from a source sharing the same stock (refreshed every minute; null when it cannot be read) — may be off by a few, never blocks a purchase; a shortfall becomes a partial order with the rest refunded
99As the warehouse that supplied each account (items[].warehouse)Price of the cheapest warehouse on sale (auto.priceVnd)Buys the cheapest warehouse first (auto.order); if it runs out midway the rest goes to the next warehouse at that warehouse's price (the difference is charged — refunded instead if your balance is short); every warehouse out ⇒ MAILSHOP_OUT_OF_STOCK, nothing charged
Вход в аккаунты склада 1 (без резервной почты)
  • Аккаунты склада 1 (gm_cookie) идут без резервной почты — вы получаете e-mail, пароль, cookies и ссылку на ящик (inboxUrl / inboxJsonUrl), и больше ничего.
  • Рекомендуемый способ входа — импорт cookies (cookies, JSON) в браузер: расширением для импорта cookies или через импорт cookies в вашем антидетект-браузере, — затем откройте Gmail: сессия уже авторизована, обычно без запроса пароля и без проверки.
  • Нужны только коды или письма? Читайте их по inboxUrl (веб-страница) или inboxJsonUrl (JSON) — без API-ключа и без входа в Google.
  • Вход по паролю с нового устройства или IP может вызвать проверку Google (резервная почта, телефон или код) — пройти её этим аккаунтам нечем, поэтому используйте cookies или ссылку на ящик.
  • Перепродаёте? Отдавайте своим клиентам cookies и ссылку на ящик — в .txt-выгрузке (GET /v1/mail-shop/orders/{id}/download) всё это есть одной строкой: email|password|cookies|inboxUrl — и предупреждайте, что входить нужно через cookies, а не по паролю.
  • Аккаунты склада 2 (gm_login) вместо этого идут со ссылкой для быстрого входа (loginUrl) — откройте её в браузере на компьютере, чтобы войти.
3 steps
  1. 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, call GET /v1/mail-shop/quote.
  2. Buy: POST /v1/mail-shop/orders with {"warehouse": 1 | 2 | 99, "quantity": N} + header Idempotency-Key (a new UUID per purchase). The response is the order, usually already completed with items[].
  3. Get the accounts: read items[] (table below) or download the TXT GET /v1/mail-shop/orders/{id}/download. status = pending ⇒ poll GET /v1/mail-shop/orders/{id} every 5 seconds until completed / partial / failed.
# 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..."
Fields of an account (items[])
ПолеТипОписание
lineNonumberPosition in the order (1, 2, …) — the {lineNo} of the inbox endpoints
warehouse1 | 2Warehouse that supplied this account
emailstringGmail address
passwordstringPassword
recoverystring?Recovery email — only when the source provides one; warehouse 1 (gm_cookie) accounts have NONE (sign in with the cookies, see the sign-in notes above)
cookiesstring | nullWarehouse 1: login cookies (JSON)
inboxUrlstring | nullWarehouse 1: inbox page without an API key (https://otpgmail.net/mail/{code}); null = link turned off
inboxJsonUrlstring | nullWarehouse 1: the same link + ?json ⇒ emails as JSON (section below)
twoFaUrlstring | nullWarehouse 1: the 2FA LINK (https://otpgmail.net/2fa/{code} — same code + expiry as inboxUrl): GET / open it in a browser ⇒ JUST the 6-digit 2FA code (section below). null = link turned off. The .txt file has this column when downloaded with ?twofa=1
inboxLinkExpiresAtnumber | nullWarehouse 1: when the link expires (unix seconds); null = no limit
loginUrlstring | nullWarehouse 2: quick login link
otpUrlstring | nullWarehouse 2: the account's Vinted OTP link — open it in a browser to see the code, ?json ⇒ JSON (section below). null = this account has no link
otpService"vinted" | nullWarehouse 2: the service of otpUrl
rawLinestringThe whole account on one line (cookies compacted) — copy it as is
Read the inbox as JSON (inboxJsonUrl)
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"
# 424819
Order statuses
СтатусОписание
pendingBeing delivered — accounts that arrived are in items[]; poll again in 5 seconds
completedFully delivered. Final.
partialDelivered short — the shortfall was refunded (refundVnd; message = the reason + the source's own words when it gave any). Final.
failedNothing delivered — fully refunded (message as above). Final.
refundedRefunded by an admin (refundVnd). Final.
Rules
  • Money: chargedVnd = Σ route[].quantity × unitPriceVnd (a 99 order split across warehouses ⇒ two legs in route). Shortfalls are refunded automatically (refundVnd). Delivered accounts are not refunded — the only warranty is the warehouse's terms (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 ⇒ 429 RATE_LIMITED.
  • Common errors: MAILSHOP_OUT_OF_STOCK 409 (details.stock = how many you can buy now) · MAILSHOP_QTY_LIMIT 400 (details.max) · INSUFFICIENT_BALANCE 402 · MAILSHOP_DISABLED 503 (warehouse / section paused) · MAILSHOP_SUPPLIER_ERROR 502 (the source refused at once — a failed order was recorded and refunded, details.orderId) · VALIDATION_ERROR 400 (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 "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
      ]
    }
  }
}
Возможные ошибки: UNAUTHORIZED
GET/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.
Параметры
ИмяТипОбязательныйОписание
warehouse1 | 2 | 99НетMail-shop warehouse: 1 = email + password + cookies + inbox link · 2 = email + password + login link · 99 = automatic, cheapest first. Send warehouse OR product
productstringНетInstead of warehouse: gm_cookie (= warehouse 1) | gm_login (= warehouse 2)
quantitynumberНетNumber of accounts (default 1)
Пример
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_FOUND
POST/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.
Параметры
ИмяТипОбязательныйОписание
warehouse1 | 2 | 99НетMail-shop warehouse: 1 = email + password + cookies + inbox link · 2 = email + password + login link · 99 = automatic, cheapest first. Send warehouse OR product
productstringНетInstead of warehouse: gm_cookie (= warehouse 1) | gm_login (= warehouse 2)
quantitynumberНетNumber of accounts (default 1)
Idempotency-KeystringДаHeader: a new UUID for every new purchase — reuse it only to retry that same purchase
Пример
# 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, MAINTENANCE
GET/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).
Параметры
ИмяТипОбязательныйОписание
pagenumberНетPage (default 1)
pageSizenumberНетOrders per page (default 20, max 100)
Пример
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
  }
}
Возможные ошибки: UNAUTHORIZED
GET/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.
Параметры
ИмяТипОбязательныйОписание
idstringДаOrder ID (data.id returned by the purchase)
Пример
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_FOUND
GET/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.
Параметры
ИмяТипОбязательныйОписание
idstringДаOrder ID (data.id returned by the purchase)
Пример
curl "https://otpgmail.net/v1/mail-shop/orders/ORDER_ID/download" \
  -H "Authorization: Bearer og_xxx..." \
  -o mail-ORDER_ID.txt
Пример ответа
email|password|cookies|inboxUrl
[email protected]|Pw7Xq!k3d|[{"domain":".google.com","name":"SID","value":"g.a000…"}]|https://otpgmail.net/mail/Q7xK2mP9vL4tR8wN3cB6yH1d
Возможные ошибки: UNAUTHORIZED, NOT_FOUND, VALIDATION_ERROR
GET/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.
Параметры
ИмяТипОбязательныйОписание
idstringДаOrder ID (data.id returned by the purchase)
lineNonumberДаAccount number within the order (items[].lineNo)
maxnumberНетNumber of latest emails: 10 (default), 20, 50, 100 or 200
Пример
# 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_ERROR
GET/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.
Параметры
ИмяТипОбязательныйОписание
idstringДаOrder ID (data.id returned by the purchase)
lineNonumberДаAccount number within the order (items[].lineNo)
Пример
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_ERROR
GET/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).
Параметры
ИмяТипОбязательныйОписание
idstringДаOrder ID (data.id returned by the purchase)
lineNonumberДаAccount number within the order (items[].lineNo)
Пример
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

Жизненный цикл заказа

СтатусОписание
Ждём кодНовый заказ, ждёт первый код. Опрашивайте GET /v1/orders/{id} раз в 3–5 секунд.
ВыполненПервый код пришёл — статус меняется сразу. Новые коды всё ещё могут прийти, если сервис присылает больше одного (без доплаты в течение срока заказа; лимит и окно зависят от склада); продолжайте опрос, и каждый новый код появится в конце otp[].
ОтменёнВозврат 100 %. Происходит, если вы вызвали /cancel (до прихода кода) или к codeDeadlineAt (не более 25 минут) код так и не пришёл.
Важные замечания:
  • Опрашивайте GET /v1/orders/{id} каждые 3–5 секунд, пока заказ в статусе waiting_code, чтобы быстро получить код.
  • Как только приходит первый код, статус сразу меняется на completed.
  • Если к codeDeadlineAt кода нет (не более 25 минут; склад 2: 20 минут; у некоторых источников склада 4 меньше) ⇒ система отменяет заказ и возвращает 100 % (status = cancelled).
  • Массив otp[] упорядочен от старых к новым: самый новый — последний. Последний код читается как otp[otp.length - 1] (Python: otp[-1]).
  • Заказ в статусе completed продолжает получать коды автоматически: продолжайте опрашивать GET /v1/orders/{id}, каждый новый код появляется в конце otp[]. Дополнительные коды не требуют доплаты в течение срока заказа, если сервис присылает больше одного; лимит и окно времени показываются до аренды (maxCodes и fee в offers[]; на складе 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.maxCodes писем в течение rules.windowMinutes мин (до windowEndsAt), не отменяется (ORDER_NOT_CANCELLABLE), а если время вышло и писем не пришло, он становится cancelled без возврата. Чтобы продолжить приём на том же ящике, вызовите POST /v1/orders/{id}/rerent, пока canRerent равно true (цена: 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".
  • Много заказов сразу? Опрашивайте до 100 за вызов через POST /v1/orders/status — см. раздел Статус многих заказов за один вызов.

Лимиты запросов

Лимиты действуют на каждый ключ 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; 409 DUPLICATE_REQUEST, пока первый запрос с тем же Idempotency-Key ещё выполняется ⇒ 2 с. retryable = false (retryAfterSec = null) ⇒ повторный вызов не поможет, пока вы не устраните причину (баланс, лимит ожидающих заказов, параметры, ключ API…). partial.reason в POST /v1/orders следует тем же правилам. При ответе 409 «нет в наличии» (OUT_OF_STOCK, WAREHOUSE_UNAVAILABLE, ALL_WAREHOUSES_EXHAUSTED) выждите полные retryAfterSec секунд перед повтором — более ранний повтор обычно возвращает тот же ответ — и перед новой арендой проверьте stock / offers[].stock в GET /v1/services; за уже арендованными заказами следите через вебхуки, а не частым опросом.
Примеры ответов с ошибкой
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
  }
}
Код ошибкиHTTPretryable · retryAfterSecЗначение
UNAUTHORIZED401false · nullКлюч API недействителен или отсутствует.
ACCOUNT_LOCKED403false · nullАккаунт заблокирован — все запросы с его ключом отклоняются. Обратитесь в поддержку.
FORBIDDEN403false · nullВо время обработки аренды аккаунт уже был неактивен (его только что заблокировали).
NOT_FOUND404false · nullВ вашем аккаунте нет заказа / пакета / ящика с таким ID (заказы другого аккаунта тоже дают 404).
VALIDATION_ERROR400false · nullПараметры запроса недопустимы (неверный тип, отсутствует обязательное поле и т. п.).
INSUFFICIENT_BALANCE402false · nullБаланса аккаунта недостаточно для оплаты заказа.
SERVICE_DISABLED409false · nullТакого кода сервиса нет, или сервис временно отключён. Берите точный код из GET /v1/services.
OUT_OF_STOCK409true · 5На момент запроса у сервиса нет доступных ящиков (для выбранного типа ящика).
DOMAIN_UNAVAILABLE409false · nullСервис не предлагает выбранный тип ящика — iCloud пока не поддерживается / временно отключён, либо у сервиса нет ящиков gmx.com. Передайте domain "gmail.com" (или не указывайте поле), чтобы арендовать Gmail.
WAREHOUSE_UNAVAILABLE409true · 5 / falseВыбранный склад не продаёт этот сервис/тип ящика, приостановлен или в нём только что закончился товар. Выберите другой склад из offers[] или передайте warehouse = 99, чтобы система сама выбрала склад с наличием. Если этот склад (или все склады, продающие этот сервис) недоступен для вашего аккаунта, retryable = false — повторный вызов не поможет, используйте другой склад.
ALL_WAREHOUSES_EXHAUSTED409true · 5Только при warehouse = 99: перебраны все склады с наличием (сначала самые дешёвые), и везде нет товара или сбой — либо все склады сообщают нулевое наличие. Ничего не списано; повторите позже.
NO_MAILS_AVAILABLE503true · 5У поставщика не осталось ящиков для этого сервиса (временно закончились).
QUANTITY_EXCEEDED400false · nullquantity превышает максимум на один запрос (сейчас 200).
WAITING_LIMIT_REACHED429false · nullСлишком много заказов одновременно ждут код (превышен лимит одновременных заказов waiting_code для аккаунта) — дождитесь завершения части из них и арендуйте снова.
ORDER_NOT_CANCELLABLE409false · nullЗаказ нельзя отменить — по нему мог уже прийти код, или он больше не в статусе waiting_code. next-code тоже возвращает этот код, если по заказу ещё не пришёл первый код.
RERENT_NOT_AVAILABLE409false · nullЗаказ нельзя арендовать снова. Склад 3: это не последний заказ на ящике, ящик больше не закреплён за вашим аккаунтом или предыдущее окно приёма ещё открыто. Склад 2: заказ ещё не закрыт (может получать коды — используйте next-code), он слишком старый для аренды снова, на e-mail есть другой активный заказ или сервис приостановлен. На складах 1 и 4 аренды снова нет.
RERENT_EMAIL_BANNED409false · nullАренда снова, склад 2: поставщик сообщает, что ящик заблокирован сервисом (email banned) — арендовать снова нельзя; деньги не списываются.
RERENT_MAIL_GONE409false · nullАренда снова, склад 2: поставщик больше не может открыть этот ящик — арендуйте новый; деньги не списываются.
NEXT_CODE_FEE_CONFIRM409false · nullОкно без доплаты у заказа склада 2 закончилось — вызовите next-code снова с confirmFee: true, чтобы оплатить плату (nextCodeFee.feePriceVnd). Ничего не списано.
EMAIL_BANNED409false · nullЯщик заблокирован сервисом (поставщик сообщает email banned) — новые коды получать он не может; арендуйте новый. С 2026-10-02 (раньше — ORDER_EXPIRED, тот же HTTP 409).
ORDER_EXPIRED409false · nullЗаказ больше не может получать коды — его срок истёк (rentExpiresAt / nextCodeFee.hardUntil), поставщик закрыл ящик, достигнут лимит кодов (maxCodes) или у сервиса нет «следующего кода». Причина указана в order.nextCodeBlockReason.
RATE_LIMITED429true · = Retry-AfterБольше 100 запросов в секунду или 3000 в минуту на ваш ключ API (у аккаунта могут быть собственные лимиты). Заголовок Retry-After сообщает, сколько секунд ждать. На складе 2 POST /v1/orders и /rerent возвращают этот код и тогда, когда аккаунт за последние 60 минут отменил много заказов склада 2 без кода, а код получили лишь немногие, — аренда со склада 2 приостанавливается, пока доля отмен не снизится ("warehouse": 99 тоже останавливается на складе 2 с этой ошибкой — на это время выберите другой склад). Facebook / Instagram (fb, fbb, ig) — исключение.
DUPLICATE_REQUEST409true · 2 / falseIdempotency-Key уже использован с другим телом запроса или другим эндпоинтом, либо первый запрос с этим ключом ещё выполняется (повторите чуть позже). Чтобы создать другой заказ, сгенерируйте новый ключ.
PROVIDER_ERROR502true · 5Ошибка поставщика ящиков (временная — повторите через несколько секунд).
MAINTENANCE503false · nullСистема на техническом обслуживании — создание и отмена заказов приостановлены.
MAILSHOP_DISABLED503false · nullПокупка Gmail-аккаунтов на короткий срок приостановлена или этот товар отключён — повторите позже.
MAILSHOP_PRODUCT_NOT_FOUND404false · nullТовара с таким кодом нет — берите точный код из GET /v1/mail-shop/products.
MAILSHOP_OUT_OF_STOCK409false · nullДля этого количества по текущей цене не хватает наличия — error.details.stock = сколько аккаунтов можно купить сейчас.
MAILSHOP_QTY_LIMIT400false · nullquantity превышает максимум на один заказ (сейчас 500) — error.details.max.
MAILSHOP_SUPPLIER_ERROR502false · nullИсточник сразу отказал в покупке: записан заказ в статусе failed, деньги возвращены (error.details.orderId). Повторите позже с новым Idempotency-Key.
INTERNAL500false · nullНепредвиденная ошибка сервера — повторите позже; если повторяется, обратитесь в поддержку.

Вебхуки (push-уведомления)

Зарегистрируйте один URL на аккаунт, и мы отправим на него подписанное JSON-событие методом POST, как только заказ получит новый код (order.code) или будет отменён / возмещён (order.cancelled). Вебхуки необязательны и охватывают все заказы аккаунта (веб и API). Доставка выполняется по принципу «лучшей попытки» — продолжайте редкий опрос как запасной вариант (см. ниже).
Настройка в 3 шага
  1. PUT /v1/webhook с вашим URL (и при желании events) — сохраните data.secret (64 шестнадцатеричных символа): он нужен для проверки каждой доставки. Повторный вызов PUT сохраняет тот же secret.
  2. POST /v1/webhook/test — мы сразу отправляем событие ping и возвращаем ответ вашего сервера (ok, httpStatus, error).
  3. Ваш эндпоинт должен ответить любым 2xx в течение 5 секунд — сначала ответьте, затем обрабатывайте событие. Всё остальное (3xx, 4xx, 5xx, тайм-аут, ошибка соединения) считается неудачей и повторяется.
URL должен начинаться с http:// или https:// (с действительным сертификатом) и быть доступен из интернета: localhost, частные, link-local, multicast и зарезервированные адреса (10.x, 172.16–31.x, 192.168.x, 169.254.x, fc00::/7, fe80::/10…) и собственные серверы OTPGmail отклоняются с VALIDATION_ERROR. Имя хоста заново разрешается перед каждой доставкой. Перенаправления не выполняются — регистрируйте конечный URL.
Формат запроса
Каждая доставка — это HTTP-запрос POST на ваш URL с телом JSON (UTF-8) и такими заголовками:
ЗаголовокЗначение
Content-Typeapplication/json
User-AgentOTPGmail-Webhook/1
X-OTPGmail-EventИмя события: order.code, order.cancelled или ping
X-OTPGmail-DeliveryУникальный ID события (UUID) — совпадает с deliveryId в теле и не меняется при повторах
X-OTPGmail-TimestampUnix-время (секунды) подписи этой попытки — совпадает с sentAt в теле
X-OTPGmail-Signaturesha256= + hex в нижнем регистре от HMAC-SHA256 над timestamp + "." + сырое тело с ключом — вашим secret
Проверка подписи
Вычислите HMAC-SHA256 с вашим secret в качестве ключа над X-OTPGmail-Timestamp + . + сырым телом запроса в виде hex в нижнем регистре и сравните со значением после sha256= за постоянное время. Ключ — сама строка secret из 64 символов (её байты UTF-8, не декодируйте hex). Используйте сырые байты: разбор и повторная сериализация JSON меняют их. Отклоняйте метки времени, отличающиеся от ваших часов более чем на 5 минут (защита от повторов).
Примеры приёмника (Python и Node используют только стандартную библиотеку; команда curl отправляет на ваш приёмник правильно подписанную тестовую доставку)
# Локальный тест: отправьте своему приёмнику доставку, подписанную точно так же, как наша
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"
События
СобытиеКогда отправляется
order.codeЗаказ получил новый код — одно событие на каждый код, включая следующие коды на том же ящике. code — новый код; otp перечисляет все коды заказа на этот момент, от старых к новым (как в GET /v1/orders/{id}; только новый код, если тело превысило бы 64 КБ).
order.cancelledЗаказ отменён или возмещён — один раз на заказ: код не пришёл до срока (timeout), отменён вами (user) или администратором (admin), либо возмещён из-за сбоя склада (provider). refundVnd = сумма в VND, возвращённая на баланс (0, если правила сервиса не предусматривают возврат).
pingТолько от POST /v1/webhook/test.
Поля события
ПолеТипОписание
eventstringИмя события (также в заголовке X-OTPGmail-Event)
deliveryIdstringУникальный ID события (UUID, = X-OTPGmail-Delivery) — сохраните, чтобы пропускать дубликаты
sentAtnumberUnix-время этой попытки (= X-OTPGmail-Timestamp)
orderIdstringID заказа — тот же, что в GET /v1/orders/{id}
servicestringКод сервиса
warehouse1 | 2 | 3 | 4 | nullСклад 1–4, выдавший ящик; null, если заказ так и не получил ящик
domainstringДомен ящика: gmail.com, icloud.com или gmx.com
emailstring | nullАдрес арендованного ящика
codestringТолько order.code: новый код — может быть и ссылкой или полным письмом, как otp[].code
receivedAtnumberТолько order.code: Unix-время прихода кода
statusstringСтатус заказа на этот момент: completed для order.code, cancelled для order.cancelled (если администратор возмещает заказ, у которого уже есть коды, статус остаётся completed)
otparrayТолько order.code: все коды на этот момент, [{code, receivedAt}], от старых к новым
reasonstringТолько order.cancelled: timeout | user | admin | provider
refundVndnumberТолько order.cancelled: VND, возвращённые на баланс (0 = без возврата)
Пример тела — order.code
{
  "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 неудачных попыток подряд (по всем событиям) ставят вебхук на паузу: GET /v1/webhook показывает pausedAt, ничего не отправляется, новые события не ставятся в очередь.
  • Чтобы возобновить: снова вызовите PUT /v1/webhook (можно с тем же URL) или выполните успешный POST /v1/webhook/test — после этого события из очереди будут отправлены.
  • GET /v1/webhook/deliveries показывает последние 100 доставок со статусом, числом попыток и последней ошибкой; история хранится 7 дней.
Дубликаты и порядок
Одно и то же событие может прийти несколько раз (например, если ваш ответ 2xx потерялся), а события могут приходить не по порядку (несколько отправляются параллельно). Сохраняйте обработанные deliveryId и пропускайте повторы; для заказа опирайтесь на его самые свежие данные (otp, status), а не на порядок прихода.
Опрос как запасной вариант
Вебхуки могут задерживаться (повторы) или теряться (вебхук на паузе, ваш сервер недоступен). Продолжайте редкий опрос — например, GET /v1/orders/{id} каждые 30–60 с для заказов, которые ещё в waiting_code, — и считайте объект заказа из API источником истины.
PUT/v1/webhook
Регистрирует или обновляет вебхук аккаунта. Первый вызов создаёт его и возвращает новый secret; последующие вызовы сохраняют secret, снова включают вебхук и возобновляют его, если он был на паузе. Без events: оба события для нового вебхука, без изменений для существующего. URL должен быть публичным адресом — иначе 400 VALIDATION_ERROR (причина в error.details.problem).
Параметры
ИмяТипОбязательныйОписание
urlstringДаВаш эндпоинт (http:// или https://, публичный адрес, не более 2048 символов)
eventsstring[]НетКакие события отправлять: ["order.code", "order.cancelled"] (по умолчанию оба)
Пример
# Сохраните 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_ERROR
GET/v1/webhook
Вебхук вашего аккаунта, включая secret, pausedAt и consecutiveFailures. data равно null, если вебхук не зарегистрирован.
Пример
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
  }
}
Возможные ошибки: UNAUTHORIZED
DELETE/v1/webhook
Удаляет вебхук; события, ожидающие в очереди, отбрасываются. Всегда 200 — data.deleted равно false, если удалять было нечего.
Пример
curl -X DELETE "https://otpgmail.net/v1/webhook" \
  -H "Authorization: Bearer og_xxx..."
Пример ответа
{
  "success": true,
  "data": {
    "deleted": true
  }
}
Возможные ошибки: UNAUTHORIZED
POST/v1/webhook/test
Сразу отправляет событие ping на ваш URL и сообщает результат: ok (ваш сервер ответил 2xx), httpStatus, durationMs, error. Успешный тест возобновляет вебхук на паузе. Не более 10 тестов в минуту; 503 MAINTENANCE, пока вебхуки отключены во всей системе.
Пример
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"
  }
}
Ответ 200, когда ваш сервер не отвечает 2xx
{
  "success": true,
  "data": {
    "ok": false,
    "httpStatus": 500,
    "durationMs": 88,
    "error": "HTTP 500",
    "deliveryId": "7d2b9f4e-1c6a-4e3b-8d5f-0a9c2e7b1f4d"
  }
}
Возможные ошибки: UNAUTHORIZED, NOT_FOUND, RATE_LIMITED, MAINTENANCE
GET/v1/webhook/deliveries
Последние доставки, сначала новые (хранятся 7 дней): status = queued (следующая попытка в nextAt) | delivered | failed | paused, с attempts, lastHttpStatus и lastError. Тела событий не возвращаются.
Параметры
ИмяТипОбязательныйОписание
limitnumberНетКоличество доставок, 1–100 (по умолчанию 100)
Пример
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