REST API v1

ДокументацияAPI APIdocumentation

Подключите автоматическую выдачу Robux к своему магазину, боту или CRM: баланс, каталог, создание заказов, статусы и подписанные вебхуки. Те же цены и скорость, что и в кабинете.

Connect automatic Robux delivery to your store, bot or CRM: balance, catalog, order creation, statuses and signed webhooks. Same prices and speed as in the dashboard.

Base URL
/api/v1
ФорматHTTPS · JSON · UTF-8
АвторизацияBearer mms_live_…
Лимит запросов240 в минуту на ключ
ВебхукиHMAC-SHA256, 5 попыток
ДеньгиUSD, списание при создании

ВведениеIntroduction

mmsRoblox API — это REST-интерфейс того же сервиса, что и кабинет: те же цены, комиссия и скорость выдачи. Заказы, созданные через API, видны в кабинете с меткой «API», а события по заказам из кабинета приходят в ваши вебхуки.

  • Формат. JSON поверх HTTPS. Деньги — числа в USD (priceUsd), Robux — целые числа (amount), время — Unix-время в миллисекундах (createdAt).
  • Два режима заказа. link — вы получаете loginUrl и отправляете его покупателю: он сам входит в Roblox на нашей защищённой странице, вы не касаетесь его пароля. direct — вы передаёте ник и пароль покупателя, выдача начинается сразу; пароль не хранится и не попадает в логи.
  • Оплата. Сумма заказа списывается с баланса при создании. Если заказ не выполнен, деньги, как правило, автоматически возвращаются на баланс (refundedUsd, событие order.refunded); если списание у сети доставки не подтверждено однозначно — после проверки.
  • Отслеживание. Вебхуки (рекомендуется), long-poll GET /orders/{id} или пакетный POST /orders/lookup.
  • Поддержка. Каждый ответ содержит заголовок X-Request-Id — укажите его, если пишете нам о проблеме.
  • Примеры. Суммы, цены, комиссия и лимиты в примерах ниже условные. Актуальные цены — в GET /catalog, комиссия и ваши лимиты — в GET /balance (tariff.feeUsd, maxActiveOrders, rateLimit).

The mmsRoblox API is a REST interface to the same service as the dashboard: same prices, fee and delivery speed. Orders created via the API show up in the dashboard with an “API” tag, and events for dashboard orders are delivered to your webhooks too.

  • Format. JSON over HTTPS. Money is a USD number (priceUsd), Robux are integers (amount), time is a Unix timestamp in milliseconds (createdAt).
  • Two order modes. link — you get a loginUrl and send it to the buyer, who signs in to Roblox on our secure page; you never touch their password. direct — you pass the buyer’s username and password and delivery starts at once; the password is never stored or logged.
  • Payment. The order total is debited from your balance at creation. If the order fails, the money is normally returned to your balance automatically (refundedUsd, event order.refunded) — after a check if the charge status is unclear.
  • Tracking. Webhooks (recommended), long-poll GET /orders/{id} or batch POST /orders/lookup.
  • Support. Every response carries an X-Request-Id header — include it when you contact us about an issue.
  • Examples. Amounts, prices, fees and limits in the examples below are illustrative. Current prices are in GET /catalog; the fee and your limits are in GET /balance (tariff.feeUsd, maxActiveOrders, rateLimit).

Быстрый стартQuick start

  1. Откройте кабинет → раздел API и скопируйте ключ mms_live_…. Сохраните его в переменной окружения MMS_API_KEY на сервере.
  2. Проверьте баланс: GET /balance.
  3. Создайте заказ: POST /orders с заголовком Idempotency-Key — номером заказа в вашем магазине.
  4. Отправьте покупателю loginUrl и дождитесь статуса delivered — через вебхук order.completed или long-poll.
  1. Open the dashboard → API section and copy your mms_live_… key. Keep it in the MMS_API_KEY environment variable on your server.
  2. Check your balance: GET /balance.
  3. Create an order: POST /orders with an Idempotency-Key header — your store’s order number.
  4. Send the loginUrl to the buyer and wait for delivered — via the order.completed webhook or long-poll.
# 1. Balance
curl {{BASE}}/balance \
  -H "Authorization: Bearer $MMS_API_KEY"

# 2. Order for 1000 Robux (link mode)
curl -X POST {{BASE}}/orders \
  -H "Authorization: Bearer $MMS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: shop-10482" \
  -d '{"amount": 1000, "target": "player_42"}'

# 3. Wait for a change of the order (long-poll, up to 10 s)
curl "{{BASE}}/orders/M-7KQ2ZD?from_revision=9f2c4a1b7e30d5c2&max_wait_ms=10000" \
  -H "Authorization: Bearer $MMS_API_KEY"
// Node.js 18+ (built-in fetch). Save as quickstart.mjs
const BASE = '{{BASE}}';
const KEY = process.env.MMS_API_KEY;

async function api(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json', ...headers },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json();
  if (!res.ok) throw Object.assign(new Error(`${res.status} ${data.error}: ${data.detail}`), { data });
  return data;
}

const balance = await api('GET', '/balance');
console.log('Balance:', balance.balanceUsd);

// Idempotency-Key = your own order id: safe to retry after a network error
const order = await api('POST', '/orders', { amount: 1000, target: 'player_42' }, { 'Idempotency-Key': 'shop-10482' });
console.log(order.id, order.status, order.loginUrl); // send loginUrl to the buyer

// Wait for a change (long-poll, up to 10 s)
const next = await api('GET', `/orders/${order.id}?from_revision=${order.revision}&max_wait_ms=10000`);
console.log(next.status);
# Python 3 + requests
import os, requests

BASE = "{{BASE}}"
S = requests.Session()
S.headers["Authorization"] = "Bearer " + os.environ["MMS_API_KEY"]

def api(method, path, **kw):
    r = S.request(method, BASE + path, timeout=30, **kw)
    data = r.json()
    if r.status_code >= 400:
        raise RuntimeError(f"{r.status_code} {data['error']}: {data.get('detail')}")
    return data

print("Balance:", api("GET", "/balance")["balanceUsd"])

# Idempotency-Key = your own order id: safe to retry after a network error
order = api("POST", "/orders",
            json={"amount": 1000, "target": "player_42"},
            headers={"Idempotency-Key": "shop-10482"})
print(order["id"], order["status"], order["loginUrl"])  # send loginUrl to the buyer

# Wait for a change (long-poll, up to 10 s)
nxt = api("GET", f"/orders/{order['id']}",
          params={"from_revision": order["revision"], "max_wait_ms": 10000})
print(nxt["status"])
<?php
// PHP 7.4+ with the curl extension
const BASE = '{{BASE}}';

function api(string $method, string $path, ?array $body = null, array $headers = []): array {
    $ch = curl_init(BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => array_merge([
            'Authorization: Bearer ' . getenv('MMS_API_KEY'),
            'Content-Type: application/json',
        ], $headers),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
    ]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $data = json_decode(curl_exec($ch), true);
    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($code >= 400) throw new RuntimeException("$code {$data['error']}: {$data['detail']}");
    return $data;
}

echo 'Balance: ', api('GET', '/balance')['balanceUsd'], PHP_EOL;

// Idempotency-Key = your own order id: safe to retry after a network error
$order = api('POST', '/orders', ['amount' => 1000, 'target' => 'player_42'], ['Idempotency-Key: shop-10482']);
echo $order['id'], ' ', $order['status'], ' ', $order['loginUrl'], PHP_EOL; // send loginUrl to the buyer

// Wait for a change (long-poll, up to 10 s)
$next = api('GET', "/orders/{$order['id']}?from_revision={$order['revision']}&max_wait_ms=10000");
echo $next['status'], PHP_EOL;
i

Ключ даёт доступ к балансу — храните его только на сервере. Не вставляйте ключ в код сайта, мобильного приложения или публичные репозитории: API не отдаёт CORS-заголовки и предназначен для вызовов с сервера.

The key gives access to your balance — keep it on your server only. Never put it into website or mobile app code or public repositories: the API sends no CORS headers and is meant for server-to-server calls.

АвторизацияAuthentication

Каждый запрос (кроме GET /health и GET /openapi.json) передаёт API-ключ в одном из заголовков. Ключ выглядит как mms_live_ + 43 символа и уже создан для вашего аккаунта — он в кабинете, в разделе «API».

  • У аккаунта один активный ключ. Кнопка «Перевыпустить» в кабинете создаёт новый ключ, а старый сразу перестаёт работать.
  • Ключ не принимается в строке запроса (?key=) и в cookies.
  • Неудачные попытки авторизации ограничены: 30 за 15 минут с одного IP, дальше — 429 rate_limit.
  • Каждый ответ с ключом содержит заголовки X-MMS-Terms-Version (действующая редакция Пользовательского соглашения) и X-MMS-Terms-Accepted (true, если вы её приняли). Новую редакцию принимают в кабинете. Если оператор включил такое требование, POST /orders без принятой редакции отвечает 403 terms_not_accepted (после смены редакции — через 14 дней).

Every request (except GET /health and GET /openapi.json) carries your API key in one of the headers. A key looks like mms_live_ + 43 characters and is already created for your account — find it in the dashboard, “API” section.

  • An account has one active key. “Regenerate” in the dashboard issues a new key and the old one stops working immediately.
  • The key is not accepted in the query string (?key=) or in cookies.
  • Failed authentication attempts are limited: 30 per 15 minutes per IP, then 429 rate_limit.
  • Every response to a keyed request carries X-MMS-Terms-Version (the current version of the Terms of Use) and X-MMS-Terms-Accepted (true if you have accepted it). New versions are accepted in the dashboard. If the operator has switched this requirement on, POST /orders without the accepted version returns 403 terms_not_accepted (14 days after a version change).
GET /api/v1/balance HTTP/1.1
Authorization: Bearer mms_live_AbCd…
GET /api/v1/balance HTTP/1.1
X-API-Key: mms_live_AbCd…
HTTPКодКогда
401unauthorizedКлюч не передан, неверный или перевыпущен.The key is missing, invalid or was regenerated.
403account_blockedАккаунт заблокирован.The account is blocked.
403terms_not_acceptedТолько POST /orders: не принята действующая редакция правил (примите её в кабинете). Поле termsVersion — действующая версия.POST /orders only: the current Terms of Use version is not accepted (accept it in the dashboard). termsVersion holds the current version.
403api_disabledДоступ к API для аккаунта отключён администратором.API access is disabled for the account by an administrator.

ЛимитыRate limits

Лимит запросов считается на ключ в фиксированном окне 60 секунд — по умолчанию 240 запросов в минуту (ваше значение — в rateLimit ответа GET /balance). Каждый ответ на запрос с ключом содержит заголовки:

Requests are limited per key in a fixed 60-second window — 240 requests per minute by default (your value is rateLimit in GET /balance). Every authenticated response carries the headers:

X-RateLimit-Limit: 240
X-RateLimit-Remaining: 0
Retry-After: 17
X-Request-Id: req_5c1e9a0b7d3f2e84

{ "error": "rate_limit", "detail": "Too many requests; retry later", "retryAfterSec": 17, "request_id": "req_5c1e9a0b7d3f2e84" }
ЛимитЗначениеПри превышении
Запросов на ключRequests per key240 / мин по умолчанию240 / min by default429 rate_limit + Retry-After
Активных заказовActive ordersЗадаётся для каждого клиента — ваше значение в maxActiveOrders ответа GET /balanceSet per client — your value is maxActiveOrders in GET /balance429 too_many_active (limit)
Действий с заказамиOrder actions60 / мин на аккаунт (cancel, answer, relink)60 / min per account (cancel, answer, relink)429 rate_limit
Цена суммы вне каталогаNon-catalog amount pricing10 подтверждений цены / мин на аккаунт10 price confirmations / min per account429 rate_limit
Создание endpoint’ов вебхуковWebhook endpoint creation30 / час30 / hour429 rate_limit
Тестовых вебхуковTest webhooks30 / 15 мин30 / 15 min429 rate_limit
Повторных доставок (redeliver)Redeliveries300 / 15 мин300 / 15 min429 rate_limit
Включений endpoint’а (enable)Endpoint re-enables10 / 15 мин10 / 15 min429 rate_limit
Неудачной авторизацииFailed auth30 / 15 мин на IP30 / 15 min per IP429 rate_limit
Одновременных long-pollConcurrent long-polls20 на ключ20 per keyответ сразу, без ожиданияanswered at once, no wait
Размер тела запросаRequest body size64 KB413 too_large
id в одном lookupids per lookup100400 too_many_ids
Endpoint’ов вебхуковWebhook endpoints5409 too_many_endpoints

Активные заказы — все незавершённые (need_login, queued, running, need_*); ваш лимит — maxActiveOrders в GET /balance. Окно лимита запросов на ключ — календарная минута, окна остальных лимитов отсчитываются от первого запроса; Retry-After и retryAfterSec — сколько секунд осталось до конца окна именно того лимита, который сработал (для вебхуков это может быть до 15 минут или часа). Ошибка 429 too_many_open_links (бывает при довыдаче) означает, что сеть доставки сейчас занята открытыми ссылками входа — повторите запрос позже. На 429, 502 и 503 повторяйте запрос с экспоненциальной паузой (1, 2, 4… до 60 с) и учитывайте Retry-After.

Active orders are all non-terminal ones (need_login, queued, running, need_*); your cap is maxActiveOrders in GET /balance. The per-key request window is the calendar minute; the windows of the other limits start at the first request; Retry-After and retryAfterSec tell how many seconds are left in the window of the limit that was hit (for webhook limits this can be up to 15 minutes or an hour). 429 too_many_open_links (seen on relink) means the delivery network is busy with open sign-in links right now — retry later. On 429, 502 and 503 retry with exponential backoff (1, 2, 4… up to 60 s) and honour Retry-After.

ОшибкиErrors

Успешный ответ — сам ресурс, без обёртки. Ошибка — HTTP-статус 4xx/5xx и объект с машинным кодом error, понятным текстом detail (на английском) и request_id; у некоторых ошибок есть дополнительные поля. Ветвитесь по error, а не по тексту.

A successful response is the resource itself, with no wrapper. An error is a 4xx/5xx HTTP status and an object with a machine code error, a human-readable detail and request_id; some errors add extra fields. Branch on error, never on the text.

{
  "error": "insufficient_balance",
  "detail": "Not enough balance for this order",
  "request_id": "req_8d0c2f6a1b9e4c37",
  "balanceUsd": 3.1,
  "requiredUsd": 8.25
}
HTTPКодКогдаДоп. поля
400bad_paramsПоле отсутствует или неверного типа/форматаA field is missing or has a wrong type/formatfield / missing
400bad_jsonТело — не валидный JSONThe body is not valid JSON
400bad_content_typePOST с телом не в application/jsonPOST with a body that is not application/json
400bad_amountСумма вне диапазона или не собирается точно — используйте robux из /quoteAmount out of range or not assembled exactly — use robux from /quotemin, max, suggestedAmount
400idempotency_key_requiredPOST /orders без Idempotency-KeyPOST /orders without Idempotency-Key
400bad_idempotency_keyКлюч не соответствует ^[A-Za-z0-9_.:-]{1,64}$The key does not match ^[A-Za-z0-9_.:-]{1,64}$
400invalid_cursorНеверный cursorBad cursor
400too_many_idsБольше 100 id/ключей в lookupMore than 100 ids/keys in a lookup
400bad_answerПустой ответ на шаг входаEmpty answer to a sign-in step
400bad_urlURL вебхука отклонён, причина — в detail (английский текст)Webhook URL rejected, the reason is in detail
401unauthorizedКлюч не передан, неверный или отозванMissing, invalid or revoked key
402insufficient_balanceНедостаточно средств на балансеNot enough balancebalanceUsd, requiredUsd
403account_blockedАккаунт заблокированThe account is blocked
403api_disabledAPI отключён для аккаунтаAPI is disabled for the account
403terms_not_acceptedНе принята действующая редакция правил (POST /orders, если оператор включил требование)The current Terms of Use version is not accepted (POST /orders, if the operator has switched this on)termsVersion
404no_orderЗаказ не найден или принадлежит другому аккаунтуOrder not found or belongs to another account
404not_foundEndpoint вебхука или доставка не найденыWebhook endpoint or delivery not found
404unknown_endpointТакого метода API нетNo such API route
409idempotency_conflictТот же Idempotency-Key с другими параметрамиSame Idempotency-Key with different parametersorderId
409price_changedПередан expectedTotalUsd, а цена другаяexpectedTotalUsd given and the price differstotalUsd
409not_cancellableЗаказ уже нельзя отменитьThe order can no longer be cancelled
409in_progressС заказом выполняется операция — повторите чуть позжеAn operation on this order is running — retry shortly
409not_waitingОтвет отправлен, но заказ его не ждётAnswer sent but the order does not wait for one
409stale_stepstepId не совпадает с текущим запросомstepId does not match the current prompt
409not_relinkableДовыдача невозможнаRelink is not possible
409order_refundedОстаток уже возвращён на баланс — довыдача невозможнаThe remainder was already refunded — relink is not possible
409link_expiredЗарезервирован: сейчас методы API его не возвращают (истёкшая ссылка — статус заказа expired)Reserved: no API method returns it today (an expired link shows as order status expired)
409too_many_endpointsБольше 5 endpoint’ов вебхуковMore than 5 webhook endpointslimit
409not_failedПовторить можно только неудавшуюся доставкуOnly a failed delivery can be redelivered
409endpoint_disabledEndpoint отключён — сначала включите егоThe endpoint is disabled — enable it first
413too_largeТело больше 64 KBBody larger than 64 KB
429rate_limitПревышен лимит запросовRate limit exceededretryAfterSec, заголовокheader Retry-After
429too_many_activeДостигнут лимит активных заказовActive-order cap reachedlimit
429too_many_open_linksСеть доставки занята открытыми ссылками (довыдача) — повторите позжеThe delivery network is busy with open sign-in links (relink) — retry later
502upstream_errorОшибка сети доставкиDelivery network error
503temporarily_unavailableСеть доставки временно недоступна или на обслуживании (в т.ч. цену суммы вне каталога сейчас нельзя подтвердить) — повторите с паузойThe delivery network is temporarily unavailable or under maintenance (incl. the price of a non-catalog amount cannot be confirmed right now) — retry with backoff
500internalВнутренняя ошибка — сообщите нам request_idInternal error — send us the request_id

ИдемпотентностьIdempotency

POST /orders требует заголовок Idempotency-Key (или поле key в теле) — 1–64 символа A–Z a–z 0–9 _ . : -. Используйте номер заказа в своей системе, например shop-10482. Благодаря ключу запрос можно смело повторять после таймаута или обрыва связи: второй раз деньги не спишутся.

  • Тот же ключ и те же параметры — вернётся уже созданный заказ с HTTP 200, полем "idempotent": true и заголовком Idempotent-Replayed: true, независимо от текущей цены и баланса.
  • Тот же ключ, другие параметры (amount, mode, target или наличие pass) — 409 idempotency_conflict с orderId существующего заказа.
  • Ключ уникален в пределах аккаунта и не истекает. Он сохраняется в поле key заказа — заказ можно найти через GET /orders/by-external-id/{key} или POST /orders/lookup.

POST /orders requires an Idempotency-Key header (or a key body field) — 1–64 characters of A–Z a–z 0–9 _ . : -. Use the order number from your own system, e.g. shop-10482. With the key you can safely retry the request after a timeout or a dropped connection: you will never be charged twice.

  • Same key, same parameters — the already created order is returned with HTTP 200, "idempotent": true and the Idempotent-Replayed: true header, regardless of the current price and balance.
  • Same key, different parameters (amount, mode, target or presence of pass) — 409 idempotency_conflict with the existing orderId.
  • A key is unique within your account and never expires. It is stored in the order’s key field — find the order with GET /orders/by-external-id/{key} or POST /orders/lookup.
Idempotent-Replayed: true
X-Request-Id: req_0a7e3c5d9b1f2468

{ "id": "M-7KQ2ZD", "key": "shop-10482", "status": "need_login", …, "idempotent": true }

Объект заказаThe order object

Все методы заказов возвращают один и тот же объект. Неизвестные поля, которые могут появиться позже, просто игнорируйте.

All order methods return the same object. Ignore unknown fields that may be added later.

idstring
Публичный id заказа, формат M-XXXXXX (6 символов A–Z, 0–9). На входе регистр не важен.Public order id, format M-XXXXXX (6 chars A–Z, 0–9). Case-insensitive on input.
keystring | null
Ваш Idempotency-Key; null у заказов из кабинета.Your Idempotency-Key; null for dashboard orders.
statusstring
Статус заказа — см. Статусы.Order status — see Statuses.
amountinteger
Заказано Robux.Robux ordered.
deliveredinteger
Сколько Robux уже зачислено.Robux credited so far.
mode"link" | "direct"
Режим заказа.Order mode.
robloxUsernamestring | null
Аккаунт, на который зачисляются Robux: переданный в target (в режиме link или direct) или тот, под которым вошёл покупатель; null, пока неизвестен.The credited account: the target you passed (link or direct mode) or the account the buyer signed in with; null until known.
loginUrlstring | null
Только для link: ссылка на нашу страницу входа — отправьте её покупателю. Для direct — null.Link mode only: our sign-in page — send it to the buyer. null for direct.
linkExpiresAtinteger | null
Когда истекает ссылка (мс). После этого незавершённый заказ переходит в expired с возвратом.When the link expires (ms). An unfinished order then becomes expired and is refunded.
opened, openedAtboolean, integer | null
Покупатель открыл ссылку, и когда.Whether and when the buyer opened the link.
priceUsd, feeUsd, totalUsdnumber
Цена Robux, комиссия и итог, списанный с баланса при создании.Robux price, fee and the total debited from the balance at creation.
refundedUsdnumber
Сколько уже возвращено на баланс.Amount returned to the balance so far.
cancellableboolean
Можно ли сейчас отменить заказ (cancel).Whether the order can be cancelled now (cancel).
relinkableboolean
Можно ли довыдать остаток частичного заказа (relink).Whether the remainder of a partial order can be delivered (relink).
promptobject | null
Что нужно от покупателя, пока статус need_* — см. ниже.What the buyer must do while the status is need_* — see below.
reasonCode, reasonstring | null
Код причины неудачи и её текст на английском — см. коды причин.Failure code and its English text — see reason codes.
failureobject | null
{ code, category, retriable } для error, partial, expired и отмены с причиной.{ code, category, retriable } for error, partial, expired and cancellations with a reason.
lastFailureobject | null
{ code, category } — предыдущая попытка входа по ещё живой ссылке не удалась (без списания); покупатель может попробовать снова.{ code, category } — the previous sign-in attempt on a still-live link failed (no charge); the buyer can try again.
source"api" | "cabinet"
Где создан заказ.Where the order was created.
revisionstring
16 hex-символов, меняется при любом видимом изменении заказа. Используйте для long-poll (from_revision) и чтобы не обрабатывать одно состояние дважды.16 hex chars, changes on any visible change. Use it for long-poll (from_revision) and to avoid processing the same state twice.
createdAt, updatedAt, completedAtinteger
Время в мс; completedAt — null, пока заказ не завершён.Times in ms; completedAt is null until the order is finished.
{
  "id": "M-7KQ2ZD",
  "key": "shop-10482",
  "status": "need_login",
  "amount": 1000,
  "delivered": 0,
  "mode": "link",
  "robloxUsername": null,
  "loginUrl": "{{ORIGIN}}/pay/Zx8kQ2rT5vLm9wPaJ3nB7cYd",
  "linkExpiresAt": 1791886440000,
  "opened": false,
  "openedAt": null,
  "priceUsd": 8.24,
  "feeUsd": 0.01,
  "totalUsd": 8.25,
  "refundedUsd": 0,
  "cancellable": true,
  "relinkable": false,
  "prompt": null,
  "reasonCode": null,
  "reason": null,
  "failure": null,
  "lastFailure": null,
  "source": "api",
  "revision": "9f2c4a1b7e30d5c2",
  "createdAt": 1791800040000,
  "updatedAt": 1791800040000,
  "completedAt": null
}

Запрос действия (prompt)Action prompt

Пока статус need_*, в prompt описано, что нужно от покупателя. В режиме link покупатель отвечает сам на нашей странице; ответить можно и через POST /orders/{id}/answer.

While the status is need_*, prompt describes what the buyer must do. In link mode the buyer answers on our page; you can also answer via POST /orders/{id}/answer.

{
  "kind": "2fa",
  "stepId": "st_4b8e1f",
  "mediaType": "Email",
  "maskedEmail": "p***@gmail.com",
  "games": []
}
kindЧто означаетОтвет (answer)
2faКод подтверждения: mediaType — Authenticator, SMS или Email (тогда есть maskedEmail)Verification code: mediaType is Authenticator, SMS or Email (then maskedEmail is set)Код, например 123456The code, e.g. 123456
email_codeКод из письма RobloxCode from the Roblox e-mailКод из письмаThe code
emailНужен e-mail, привязанный к аккаунтуThe e-mail linked to the account is requirede-mail
sqПроверка безопасности: выбрать 3 игры из games, в которые покупатель недавно игралSecurity check: pick 3 games from games the buyer played recentlyid игр через запятую: 111,222,333Game ids comma-separated: 111,222,333
cross_deviceПодтвердить вход в приложении Roblox на телефонеApprove the sign-in in the Roblox app on a phoneНе нужен — ждитеNone — wait
captchaЗарезервировано: сейчас не приходит — проверка безопасности проходит автоматически, заказ при этом в статусе runningReserved: not sent today — the security check is passed automatically while the order is runningНе нужен — ждитеNone — wait

Статусы заказаOrder statuses

Финальные статусы — delivered, partial, error, cancelled, expired. Список может расширяться: неизвестный статус считайте «в процессе».

Terminal statuses are delivered, partial, error, cancelled, expired. The list may grow: treat an unknown status as “in progress”.

СтатусЧто означаетФинальный
need_loginЖдём, пока покупатель войдёт по ссылке loginUrlWaiting for the buyer to sign in via loginUrl—
queuedПринят в работу, выдача сейчас начнётсяAccepted, delivery is about to start—
runningИдёт вход и зачисление RobuxSigning in and crediting Robux—
need_2faНужен код двухфакторной защитыA two-factor code is required—
need_email_codeНужен код из письмаA code from an e-mail is required—
need_emailНужен e-mail аккаунтаThe account e-mail is required—
need_sqНужно пройти проверку безопасности (выбрать игры)A security check is required (pick games)—
need_approveНужно подтвердить вход в приложении RobloxThe sign-in must be approved in the Roblox app—
deliveredRobux полностью зачисленыRobux fully credited✓
partialЗачислена часть (delivered < amount). Остаток можно довыдать без доплаты через relink, пока он не возвращён на балансPartly credited (delivered < amount). The remainder can be delivered at no extra cost via relink unless it was refunded✓
errorНе выполнен; причина в reasonCode, деньги возвращаются на баланс (если списание не подтверждено однозначно — после проверки)Failed; see reasonCode, the money is returned to the balance (after a check if the charge is not clearly confirmed)✓
cancelledОтменён, деньги возвращеныCancelled and refunded✓
expiredПокупатель не вошёл до linkExpiresAt, деньги возвращеныThe buyer did not sign in before linkExpiresAt; refunded✓
linkneed_login→queued→running→need_2fa→running→delivered
directqueued→running→delivered

Для фильтра status в GET /orders есть группы: active (need_login, queued, running, need_*), done (delivered), failed (error, partial, cancelled, expired).

The status filter of GET /orders accepts groups: active (need_login, queued, running, need_*), done (delivered), failed (error, partial, cancelled, expired).

Коды причинReason codes

reasonCode и failure.code — машинный код, reason — короткий текст на английском (в английской версии таблицы ниже — он дословно). failure.category подсказывает, чей ход: buyer_action_required — покупателю нужно что-то исправить, temporary_unavailable — временный сбой, permanent_failure — повтор не поможет. retriable = категория не permanent_failure. Неизвестный код считайте internal.

reasonCode and failure.code are machine codes, reason is a short English text (shown in the table below). failure.category tells whose move it is: buyer_action_required — the buyer must fix something, temporary_unavailable — a temporary glitch, permanent_failure — retrying will not help. retriable = category is not permanent_failure. Treat an unknown code as internal.

КодЧто означаетКатегория
bad_credentialsНеверный логин или пароль RobloxWrong Roblox username or passwordbuyer_action_required
account_lockedАккаунт Roblox заблокированThe Roblox account is lockedbuyer_action_required
too_many_attemptsСлишком много попыток входа, попробуйте позжеToo many sign-in attempts; try again laterbuyer_action_required
passkey_requiredАккаунт требует вход по passkey — не поддерживаетсяThe Roblox account requires a passkey, which is not supportedbuyer_action_required
twofa_failedДвухэтапная проверка не пройденаTwo-step verification was not completedbuyer_action_required
security_questionПроверка безопасности не пройденаThe security question was not answered correctlybuyer_action_required
captcha_failedНе удалось пройти captchaThe captcha could not be completedbuyer_action_required
login_cancelledВход отменёнThe sign-in was cancelledbuyer_action_required
login_failedНе удалось войтиThe sign-in failedbuyer_action_required
buyer_unverifiedПокупатель не подтвердил входThe buyer did not confirm the sign-inbuyer_action_required
link_expiredСсылка истекла до входа покупателяThe buyer link expired before sign-inbuyer_action_required
cancelled_by_clientЗаказ отменёнThe order was cancelledbuyer_action_required
unsupported_amountЭту сумму нельзя выдатьThis amount cannot be deliveredpermanent_failure
purchase_failedНе удалось провести покупкуThe purchase could not be completedtemporary_unavailable
grant_failedRobux не удалось зачислить полностьюRobux could not be credited in fulltemporary_unavailable
device_unavailableВыдача временно недоступнаDelivery is temporarily unavailabletemporary_unavailable
interruptedВыдача прерванаDelivery was interruptedtemporary_unavailable
internalВнутренняя ошибка — напишите в поддержкуInternal error; contact supporttemporary_unavailable

Эндпоинты

Баланс и ценыBalance & prices

БалансBalance

GET/balance

Баланс, комиссия и ваши лимиты. balanceUsd — с точностью до 3 знаков, tariff.feeUsd — комиссия за заказ, activeOrders / maxActiveOrders — активные заказы и их лимит, rateLimit — запросов в минуту.

Balance, fee and your limits. balanceUsd has 3 decimals, tariff.feeUsd is the per-order fee, activeOrders / maxActiveOrders — active orders and their cap, rateLimit — requests per minute.

curl {{BASE}}/balance -H "Authorization: Bearer $MMS_API_KEY"
{
  "balanceUsd": 125.4,
  "currency": "USD",
  "tariff": { "feeUsd": 0.01, "feePercent": 0 },
  "billing": true,
  "activeOrders": 3,
  "maxActiveOrders": 10,
  "rateLimit": 240
}

КаталогCatalog

GET/catalog

Массив доступных пакетов с вашими ценами. packs — из скольких пакетов собирается сумма, popular — популярный номинал. Цены в примере условные.

An array of available packages with your prices. packs — how many packs the amount is assembled from, popular — a popular amount. Prices below are illustrative.

[
  { "robux": 80, "priceUsd": 0.81, "feeUsd": 0.01, "totalUsd": 0.82, "packs": 1, "popular": false },
  { "robux": 400, "priceUsd": 3.3, "feeUsd": 0.01, "totalUsd": 3.31, "packs": 1, "popular": false },
  { "robux": 1000, "priceUsd": 8.24, "feeUsd": 0.01, "totalUsd": 8.25, "packs": 1, "popular": true }
]

Расчёт ценыQuote

GET/quote?amount={amount}

Цена любой суммы. Если сумму нельзя собрать точно, exact: false, а в robux — ближайшая доступная сумма: заказ создаётся именно на robux. Если сумма вне диапазона — ok: false с min и max.

Price for any amount. If the amount cannot be assembled exactly, exact: false and robux holds the nearest available amount: create the order for robux. Out of range — ok: false with min and max.

Параметры запроса

amountintegerобязательный
Желаемое количество Robux.Desired Robux amount.
{
  "ok": true,
  "requested": 100,
  "robux": 160,
  "exact": false,
  "priceUsd": 1.3,
  "feeUsd": 0.01,
  "totalUsd": 1.31,
  "packs": 2
}
{
  "ok": false,
  "requested": 30,
  "min": 80,
  "max": 22500
}

Ошибки

400 bad_params

Эндпоинты

ЗаказыOrders

Везде, где в ответе есть заказ, это полный объект заказа; в примерах ниже вложенные заказы для краткости сокращены.

Wherever a response contains an order, it is the full order object; nested orders in the examples below are shortened for brevity.

Создать заказCreate an order

POST/orders

Создаёт заказ и сразу списывает totalUsd с баланса. В режиме link возвращается статус need_login и loginUrl для покупателя. В режиме direct — статус queued: выдача продолжается в фоне, следите за ней через вебхуки, long-poll или lookup. Ответ — объект заказа плюс поле idempotent; новый заказ — 202, повтор с тем же ключом — 200.

Creates an order and debits totalUsd from the balance right away. In link mode you get status need_login and a loginUrl for the buyer. In direct mode the status is queued: delivery continues in the background — follow it via webhooks, long-poll or lookup. The response is the order object plus an idempotent field; a new order is 202, a replay with the same key is 200.

Заголовки

Idempotency-Keystringобязательный
1–64 символа [A-Za-z0-9_.:-], обычно номер заказа в вашей системе. Можно передать полем key в теле. См. Идемпотентность.1–64 chars [A-Za-z0-9_.:-], usually your order number. May be sent as a key body field instead. See Idempotency.
Content-Typestringобязательный
application/json

Тело запроса

amountintegerобязательный
Количество Robux. Должно точно совпадать с robux из каталога или /quote, иначе 400 bad_amount с подсказкой suggestedAmount.Robux amount. Must equal the robux from the catalog or /quote, otherwise 400 bad_amount with a suggestedAmount hint.
mode"link" | "direct"
По умолчанию direct, если передан pass, иначе link.Defaults to direct if pass is present, otherwise link.
targetstring
Ник Roblox покупателя, ^[A-Za-z0-9_]{3,20}$. Обязателен для direct; для link — необязателен, но если указан, на странице входа ник подставится и его нельзя будет изменить.Buyer’s Roblox username, ^[A-Za-z0-9_]{3,20}$. Required for direct; optional for link — if given, the sign-in page pre-fills and locks it.
passstring
Пароль покупателя (1–128 символов), только для direct. Не хранится, не логируется и не возвращается.Buyer’s password (1–128 chars), direct only. Never stored, logged or echoed.
expectedTotalUsdnumber
Защита от смены цены: если итог отличается — 409 price_changed с актуальным totalUsd, заказ не создаётся.Price guard: if the total differs — 409 price_changed with the current totalUsd, no order is created.
curl -X POST {{BASE}}/orders \
  -H "Authorization: Bearer $MMS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: shop-10482" \
  -d '{"amount": 1000, "target": "player_42", "expectedTotalUsd": 8.25}'
curl -X POST {{BASE}}/orders \
  -H "Authorization: Bearer $MMS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: shop-10483" \
  -d '{"amount": 1000, "mode": "direct", "target": "player_42", "pass": "buyer-password"}'
{
  "id": "M-7KQ2ZD",
  "key": "shop-10482",
  "status": "need_login",
  "amount": 1000,
  "delivered": 0,
  "mode": "link",
  "robloxUsername": "player_42",
  "loginUrl": "{{ORIGIN}}/pay/Zx8kQ2rT5vLm9wPaJ3nB7cYd",
  "linkExpiresAt": 1791886440000,
  "opened": false,
  "openedAt": null,
  "priceUsd": 8.24,
  "feeUsd": 0.01,
  "totalUsd": 8.25,
  "refundedUsd": 0,
  "cancellable": true,
  "relinkable": false,
  "prompt": null,
  "reasonCode": null,
  "reason": null,
  "failure": null,
  "lastFailure": null,
  "source": "api",
  "revision": "9f2c4a1b7e30d5c2",
  "createdAt": 1791800040000,
  "updatedAt": 1791800040000,
  "completedAt": null,
  "idempotent": false
}

Ошибки

400 bad_params400 bad_json400 bad_content_type400 bad_amount400 idempotency_key_required400 bad_idempotency_key402 insufficient_balance403 account_blocked403 api_disabled403 terms_not_accepted409 idempotency_conflict409 price_changed413 too_large429 too_many_active429 rate_limit503 temporarily_unavailable

Сеть доставки при создании не вызывается — исключение: сумма вне каталога, её цену мы подтверждаем у сети доставки (503 temporarily_unavailable, если сейчас это невозможно; не больше 10 подтверждений в минуту — иначе 429 rate_limit). Отказ сети доставки в режиме direct приходит уже после 202: заказ переходит в error с reasonCode: "device_unavailable" (категория temporary_unavailable) и деньги возвращаются автоматически (order.failed + order.refunded) — создайте заказ заново с новым ключом.

Creation does not call the delivery network — except for non-catalog amounts, whose price we confirm with it (503 temporarily_unavailable if that is not possible right now; at most 10 confirmations per minute, otherwise 429 rate_limit). A delivery-network refusal in direct mode arrives after the 202: the order becomes error with reasonCode: "device_unavailable" (category temporary_unavailable) and is refunded automatically (order.failed + order.refunded) — create it again with a new key.

!

Если POST /orders завершился таймаутом или ошибкой 5xx, не создавайте заказ с новым ключом — повторите запрос с тем же Idempotency-Key или проверьте GET /orders/by-external-id/{key}. Так вы не оплатите заказ дважды.

If POST /orders timed out or returned 5xx, do not create the order with a new key — retry with the same Idempotency-Key or check GET /orders/by-external-id/{key}. That way you never pay twice.

Список заказовList orders

GET/orders

Заказы аккаунта (из API и из кабинета), новые сверху, включая неоткрытые ссылки. Постраничный вывод по курсору: передайте nextCursor в следующий запрос; null — страниц больше нет.

Account orders (API and dashboard), newest first, including unopened links. Cursor pagination: pass nextCursor to the next request; null means no more pages.

Параметры запроса

limitinteger
1–100, по умолчанию 50.1–100, default 50.
cursorstring
Непрозрачная строка из nextCursor.Opaque string from nextCursor.
statusstring
Статусы и/или группы через запятую: active, done, failed, например status=active,partial.Comma-separated statuses and/or groups active, done, failed, e.g. status=active,partial.
source"api" | "cabinet"
Только заказы из API или из кабинета.Only API or only dashboard orders.
curl "{{BASE}}/orders?status=active&limit=20" -H "Authorization: Bearer $MMS_API_KEY"
{
  "orders": [
    { "id": "M-7KQ2ZD", "key": "shop-10482", "status": "need_login", "amount": 1000, "delivered": 0, "mode": "link", "source": "api", "revision": "9f2c4a1b7e30d5c2", "createdAt": 1791800040000 }
  ],
  "nextCursor": "MTc5MTc5OTk4MDAwMDo0MjA"
}

Ошибки

400 bad_params400 invalid_cursor

Получить заказRetrieve an order

GET/orders/{id}

Текущее состояние заказа. Поддерживает long-poll: передайте from_revision (последний известный вам revision) и max_wait_ms — если заказ уже изменился, ответ придёт сразу, иначе сервер подождёт изменения до max_wait_ms и вернёт текущее состояние. Завершённые заказы не удерживаются. Метод читает только нашу базу — его можно вызывать часто.

Current order state. Supports long-poll: pass from_revision (the last revision you know) and max_wait_ms — if the order has already changed, the answer is immediate, otherwise the server waits for a change up to max_wait_ms and returns the current state. Terminal orders are never held. The method reads only our database — it is cheap to call.

Параметры пути

idstringобязательный
Id заказа M-XXXXXX, регистр не важен.Order id M-XXXXXX, case-insensitive.

Параметры запроса

from_revisionstring
Последний известный revision.The last known revision.
max_wait_msinteger
0–10000, сколько ждать изменения. Одновременно — до 20 ожидающих запросов на ключ.0–10000, how long to wait for a change. Up to 20 concurrent waiting requests per key.
curl "{{BASE}}/orders/M-7KQ2ZD?from_revision=9f2c4a1b7e30d5c2&max_wait_ms=10000" \
  -H "Authorization: Bearer $MMS_API_KEY"
{
  "id": "M-7KQ2ZD",
  "key": "shop-10482",
  "status": "need_2fa",
  "amount": 1000,
  "delivered": 0,
  "mode": "link",
  "robloxUsername": "player_42",
  "loginUrl": "{{ORIGIN}}/pay/Zx8kQ2rT5vLm9wPaJ3nB7cYd",
  "linkExpiresAt": 1791886440000,
  "opened": true,
  "openedAt": 1791800071000,
  "priceUsd": 8.24,
  "feeUsd": 0.01,
  "totalUsd": 8.25,
  "refundedUsd": 0,
  "cancellable": false,
  "relinkable": false,
  "prompt": { "kind": "2fa", "stepId": "st_4b8e1f", "mediaType": "Authenticator", "maskedEmail": null, "games": [] },
  "reasonCode": null,
  "reason": null,
  "failure": null,
  "lastFailure": null,
  "source": "api",
  "revision": "c03e7d91a4b25f68",
  "createdAt": 1791800040000,
  "updatedAt": 1791800082000,
  "completedAt": null
}

Ошибки

400 bad_params404 no_order

Заказ по вашему ключуOrder by external id

GET/orders/by-external-id/{key}

Находит заказ по Idempotency-Key, с которым он создавался. Удобно после таймаута: «создался ли мой заказ?». Ответ — объект заказа.

Finds an order by the Idempotency-Key it was created with. Handy after a timeout: “was my order created?”. Returns the order object.

curl {{BASE}}/orders/by-external-id/shop-10482 -H "Authorization: Bearer $MMS_API_KEY"

Ошибки

404 no_order

Пакетный поискBatch lookup

POST/orders/lookup

До 100 заказов за один запрос по id и/или вашим ключам — удобно для периодической сверки. Дубликаты схлопываются, ненайденные и чужие id попадают в notFound.

Up to 100 orders per request by ids and/or your keys — handy for periodic reconciliation. Duplicates are collapsed, unknown and foreign ids go to notFound.

Тело запроса

idsstring[]
Id заказов M-XXXXXX.Order ids M-XXXXXX.
keysstring[]
Ваши Idempotency-Key. Всего ids + keys — не больше 100.Your Idempotency-Keys. ids + keys ≤ 100 in total.
curl -X POST {{BASE}}/orders/lookup \
  -H "Authorization: Bearer $MMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["M-7KQ2ZD", "M-ZZZZZZ"], "keys": ["shop-10483"]}'
{
  "data": [
    { "id": "M-7KQ2ZD", "key": "shop-10482", "status": "delivered", "amount": 1000, "delivered": 1000, "revision": "e81b4c02d9f3a576" },
    { "id": "M-P4WN8T", "key": "shop-10483", "status": "running", "amount": 1000, "delivered": 0, "revision": "1a9d3f7c5e2b8046" }
  ],
  "notFound": ["M-ZZZZZZ"]
}

Ошибки

400 bad_params400 too_many_ids

Отменить заказCancel an order

POST/orders/{id}/cancel

Отмена возможна, пока ничего не зачислено и нет активной попытки выдачи (cancellable: true). status: "cancelled" — заказ отменён и деньги возвращены; "cancelling" — запрос на остановку принят, итоговый статус придёт вебхуком или в GET /orders/{id}. Тело запроса не нужно.

Possible while nothing was credited and no delivery attempt is in flight (cancellable: true). status: "cancelled" — cancelled and refunded; "cancelling" — the stop request was accepted, the final status arrives via webhook or GET /orders/{id}. No request body.

curl -X POST {{BASE}}/orders/M-7KQ2ZD/cancel -H "Authorization: Bearer $MMS_API_KEY"
{
  "ok": true,
  "status": "cancelled",
  "order": { "id": "M-7KQ2ZD", "status": "cancelled", "refundedUsd": 8.25, "reasonCode": "cancelled_by_client", "cancellable": false }
}

Ошибки

404 no_order409 not_cancellable409 in_progress502 upstream_error503 temporarily_unavailable

Ответ на шаг входаAnswer a sign-in step

POST/orders/{id}/answer

Передаёт ответ покупателя на текущий prompt (код 2FA, код из письма, e-mail, игры). Нужен в основном в режиме direct — в режиме link покупатель отвечает на нашей странице сам.

Sends the buyer’s answer to the current prompt (2FA code, e-mail code, e-mail, games). Mostly for direct mode — in link mode the buyer answers on our page.

Тело запроса

answerstringобязательный
До 200 символов. Для sq — id игр через запятую.Up to 200 chars. For sq — game ids comma-separated.
stepIdstring
Из prompt.stepId. Если шаг уже сменился — 409 stale_step, ответ не отправляется.From prompt.stepId. If the step has already changed — 409 stale_step, nothing is sent.
curl -X POST {{BASE}}/orders/M-7KQ2ZD/answer \
  -H "Authorization: Bearer $MMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"answer": "123456", "stepId": "st_4b8e1f"}'
{
  "ok": true,
  "order": { "id": "M-7KQ2ZD", "status": "running", "prompt": null, "revision": "5b0f2e8a7c1d3946" }
}

Ошибки

400 bad_answer404 no_order409 stale_step409 not_waiting409 in_progress502 upstream_error503 temporarily_unavailable
POST/orders/{id}/relink

Довыдаёт остаток заказа в статусе partial без повторной оплаты (relinkable: true). В режиме link ссылка продлевается — отправьте покупателю loginUrl ещё раз; заказ остаётся partial, пока покупатель снова не войдёт. В режиме direct пароль покупателя обязателен — передайте его заново в pass (он не хранится); без него — 400 bad_params, довыдача не запускается.

Delivers the remainder of a partial order with no second charge (relinkable: true). In link mode the link is extended — send the loginUrl to the buyer again; the order stays partial until the buyer signs in again. In direct mode the buyer’s password is required — pass it again in pass (it is not stored); without it you get 400 bad_params and the relink does not start.

Тело запроса

passstring
Обязателен для заказов direct (1–128 символов); для link не передавайте — будет 400 bad_params.Required for direct orders (1–128 chars); do not send it for link orders — 400 bad_params.
curl -X POST {{BASE}}/orders/M-7KQ2ZD/relink -H "Authorization: Bearer $MMS_API_KEY"
{
  "ok": true,
  "order": { "id": "M-7KQ2ZD", "status": "partial", "amount": 1000, "delivered": 400, "loginUrl": "{{ORIGIN}}/pay/Zx8kQ2rT5vLm9wPaJ3nB7cYd", "linkExpiresAt": 1791786400000, "relinkable": true }
}

Ошибки

400 bad_params404 no_order409 not_relinkable409 order_refunded409 in_progress429 too_many_open_links502 upstream_error503 temporarily_unavailable

409 in_progress — остаток уже выдаётся (предыдущая довыдача ещё идёт): это не отказ, повторите позже и следите за заказом. 409 not_relinkable — довыдача невозможна.

409 in_progress means the remainder is already being delivered (a previous relink is still running): not a refusal — retry later and watch the order. 409 not_relinkable means relink is not possible.

История заказаOrder events

GET/orders/{id}/events

Хронология заказа теми же типами событий, что и в вебхуках, плюс order.answer_sent и order.relink_requested. Поля status, delivered, reasonCode, amountUsd есть только там, где они уместны.

The order timeline with the same event types as webhooks, plus order.answer_sent and order.relink_requested. status, delivered, reasonCode, amountUsd appear only where relevant.

{
  "events": [
    { "type": "order.created", "at": 1791800040000, "status": "need_login" },
    { "type": "order.link_opened", "at": 1791800071000 },
    { "type": "order.processing", "at": 1791800074000, "status": "queued" },
    { "type": "order.processing", "at": 1791800079000, "status": "running" },
    { "type": "order.completed", "at": 1791800103000, "status": "delivered", "delivered": 1000 }
  ]
}

order.created несёт начальный статус (need_login для link, queued для direct); order.processing повторяется, только когда меняется status (queued → running). Каждый вызов relink добавляет своё order.relink_requested.

order.created carries the initial status (need_login for link, queued for direct); order.processing repeats only when status changes (queued → running). Every relink call adds its own order.relink_requested.

Ошибки

404 no_order

Уведомления

ВебхукиWebhooks

Вебхуки — самый быстрый способ узнавать о заказах: мы отправляем POST с JSON на ваш URL при каждом изменении. События приходят по всем заказам аккаунта — и из API, и из кабинета. Добавить endpoint можно в кабинете (раздел «API») или методом POST /webhooks; до 5 endpoint’ов, у каждого свой секрет whsec_… для проверки подписи.

  • Отвечайте любым 2xx в течение 10 секунд, а обработку делайте асинхронно. Редиректы не выполняются, тело ответа игнорируется.
  • Одно событие может прийти повторно (ретраи, ручная повторная доставка) — дедуплицируйте по eventId.
  • Порядок доставки не гарантирован. Вложенный order — снимок на момент события; сравнивайте order.updatedAt или запрашивайте свежее состояние через GET /orders/{id}.
  • Изменения заказа проверяются примерно раз в 2 секунды: очень короткое промежуточное состояние (например, шаг 2FA, на который ответили за секунду) может не прийти отдельным событием, а order.processing может прийти дважды (queued, затем running). Ориентируйтесь на поле status; источник истины — GET /orders/{id}.

Webhooks are the fastest way to learn about orders: we send a JSON POST to your URL on every change. Events cover all account orders — both API and dashboard ones. Add an endpoint in the dashboard (“API” section) or with POST /webhooks; up to 5 endpoints, each with its own whsec_… secret for signature verification.

  • Reply with any 2xx within 10 seconds and process asynchronously. Redirects are not followed, the response body is ignored.
  • An event may arrive more than once (retries, manual redelivery) — deduplicate by eventId.
  • Delivery order is not guaranteed. The nested order is a snapshot at event time; compare order.updatedAt or fetch the fresh state with GET /orders/{id}.
  • Order changes are checked about every 2 seconds: a very short intermediate state (e.g. a 2FA step answered within a second) may not get its own event, and order.processing may arrive twice (queued, then running). Branch on status; GET /orders/{id} is the source of truth.

СобытияEvents

При создании endpoint’а можно выбрать события; пустой список — все события заказов. webhook.test и webhook.endpoint_disabled приходят всегда.

You can pick events when creating an endpoint; an empty list means all order events. webhook.test and webhook.endpoint_disabled are always delivered.

СобытиеКогдаДоп. поля
order.createdЗаказ создан (в API или кабинете)Order created (API or dashboard)
order.link_openedПокупатель открыл ссылкуThe buyer opened the link
order.processingСтатус стал queued или runningStatus became queued or running
order.action_requiredНужно действие покупателя (need_*); приходит снова при новом шагеBuyer action required (need_*); sent again on a new stepaction, stepId
order.needs_retryПопытка входа не удалась, ссылка ещё действует — покупатель может попробовать сноваA sign-in attempt failed, the link is still valid — the buyer can try againreasonCode
order.completedRobux зачислены полностьюRobux fully credited
order.partialЗачислена часть суммыPartly creditedreasonCode, reason
order.failedЗаказ не выполненOrder failedreasonCode, reason
order.cancelledЗаказ отменёнOrder cancelledreasonCode, reason
order.expiredСсылка истеклаThe link expiredreasonCode, reason
order.refundedДеньги возвращены на балансMoney returned to the balancerefundUsd, refundedUsd
webhook.testТестовое событие (test)Test event (test)id — id endpoint’аid — endpoint id
webhook.endpoint_disabledДругой ваш endpoint отключён из-за ошибокAnother endpoint of yours was disabled after failuresid, url

Тело событияPayload

Тело одинаково на всех попытках доставки. order — полный объект заказа на момент события; основные поля продублированы на верхнем уровне.

The body is identical on every delivery attempt. order is the full order object at event time; the key fields are duplicated at the top level.

{
  "event": "order.completed",
  "eventId": "6b1f0c8e-3d2a-4f7b-9c51-2e8a7d4b90f3",
  "createdAt": "2026-10-12T10:15:03.120Z",
  "at": 1791800103120,
  "id": "M-7KQ2ZD",
  "key": "shop-10482",
  "status": "delivered",
  "amount": 1000,
  "delivered": 1000,
  "robloxUsername": "player_42",
  "order": {
    "id": "M-7KQ2ZD",
    "key": "shop-10482",
    "status": "delivered",
    "amount": 1000,
    "delivered": 1000,
    "mode": "link",
    "robloxUsername": "player_42",
    "loginUrl": "{{ORIGIN}}/pay/Zx8kQ2rT5vLm9wPaJ3nB7cYd",
    "linkExpiresAt": 1791886440000,
    "opened": true,
    "openedAt": 1791800071000,
    "priceUsd": 8.24,
    "feeUsd": 0.01,
    "totalUsd": 8.25,
    "refundedUsd": 0,
    "cancellable": false,
    "relinkable": false,
    "prompt": null,
    "reasonCode": null,
    "reason": null,
    "failure": null,
    "lastFailure": null,
    "source": "api",
    "revision": "e81b4c02d9f3a576",
    "createdAt": 1791800040000,
    "updatedAt": 1791800103000,
    "completedAt": 1791800103000
  }
}
{
  "event": "order.failed",
  "eventId": "0c5e2a71-94d8-4b3f-a6e2-7f1d3b9c8a40",
  "createdAt": "2026-10-12T10:21:40.511Z",
  "at": 1791800500511,
  "id": "M-P4WN8T",
  "key": "shop-10483",
  "status": "error",
  "amount": 1000,
  "delivered": 0,
  "robloxUsername": "player_42",
  "reasonCode": "bad_credentials",
  "reason": "Wrong Roblox username or password",
  "order": { "id": "M-P4WN8T", "status": "error" }
}
{
  "event": "order.action_required",
  "eventId": "9a7d4e13-2b6c-4f80-8e15-c3a0f6b2d759",
  "createdAt": "2026-10-12T10:14:42.008Z",
  "at": 1791800082008,
  "id": "M-7KQ2ZD",
  "key": "shop-10482",
  "status": "need_2fa",
  "amount": 1000,
  "delivered": 0,
  "robloxUsername": "player_42",
  "action": "2fa",
  "stepId": "st_4b8e1f",
  "order": { "id": "M-7KQ2ZD", "status": "need_2fa" }
}

Заголовки и подписьHeaders & signature

ЗаголовокЗначение
Content-Typeapplication/json
User-AgentMMS-Webhooks/1.0
X-MMS-EventТип события, напримерEvent type, e.g. order.completed
X-MMS-Event-Id= eventId, одинаковый для всех endpoint’ов и попыток — ключ дедупликации= eventId, same for all endpoints and attempts — the dedup key
X-MMS-Delivery-IdId доставки на этот endpoint (как в журнале доставок)Delivery id for this endpoint (as in the delivery log)
X-MMS-Delivery-AttemptНомер попытки: 1–5 для автоматических; ручная повторная доставка (redeliver) продолжает счёт — 6, 7, …Attempt number: 1–5 for automatic attempts; a manual redeliver continues the count — 6, 7, …
X-MMS-TimestampUnix-время отправки в секундахSend time, Unix seconds
X-MMS-Signaturet=<unix>,v1=<hex>

v1 = HMAC-SHA256 в hex от строки "<t>." + сырое тело запроса, ключ — секрет endpoint’а целиком, вместе с префиксом whsec_. Проверяйте подпись по сырым байтам тела до разбора JSON, сравнивайте за постоянное время и отклоняйте запросы, у которых t отличается от текущего времени больше чем на 5 минут.

v1 = hex HMAC-SHA256 of the string "<t>." + raw request body, keyed with the whole endpoint secret including the whsec_ prefix. Verify against the raw body bytes before parsing JSON, compare in constant time and reject requests whose t is more than 5 minutes away from the current time.

// Node.js + Express
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.MMS_WEBHOOK_SECRET; // whsec_…

// verify the RAW body, before any JSON parsing
app.post('/mms/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = Object.fromEntries((req.get('X-MMS-Signature') || '').split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', SECRET).update(`${sig.t}.`).update(req.body).digest('hex');
  const valid = typeof sig.v1 === 'string' && sig.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig.v1), Buffer.from(expected));
  if (!valid || Math.abs(Date.now() / 1000 - Number(sig.t)) > 300) return res.status(400).end();

  const event = JSON.parse(req.body);
  res.status(200).end(); // answer fast, then process
  // deduplicate by event.eventId, then handle event.event / event.order
});

app.listen(3000);
# Python + Flask
import hashlib, hmac, json, os, time
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["MMS_WEBHOOK_SECRET"].encode()  # whsec_…

@app.post("/mms/webhook")
def mms_webhook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    sig = dict(p.split("=", 1) for p in request.headers.get("X-MMS-Signature", "").split(",") if "=" in p)
    t = sig.get("t", "")
    expected = hmac.new(SECRET, t.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not t.isdigit() or abs(time.time() - int(t)) > 300 or not hmac.compare_digest(expected, sig.get("v1", "")):
        abort(400)
    event = json.loads(raw)
    # deduplicate by event["eventId"], then handle event["event"] / event["order"]
    return "", 200
<?php
// Plain PHP
$secret = getenv('MMS_WEBHOOK_SECRET'); // whsec_…
$raw = file_get_contents('php://input'); // raw body, before json_decode

$sig = [];
foreach (explode(',', $_SERVER['HTTP_X_MMS_SIGNATURE'] ?? '') as $part) {
    [$k, $v] = array_pad(explode('=', $part, 2), 2, '');
    $sig[$k] = $v;
}
$t = $sig['t'] ?? '';
$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);

if (!ctype_digit($t) || abs(time() - (int)$t) > 300 || !hash_equals($expected, $sig['v1'] ?? '')) {
    http_response_code(400);
    exit;
}

$event = json_decode($raw, true);
http_response_code(200);
// deduplicate by $event['eventId'], then handle $event['event'] / $event['order']

Повторы и отключениеRetries & auto-disable

  • Повторы. Если endpoint не ответил 2xx за 10 секунд, доставка повторяется: сразу, затем через 1 мин, 5 мин, 15 мин и 60 мин — всего 5 попыток. После этого доставка помечается failed; её можно повторить вручную (redeliver) или в кабинете.
  • Автоотключение. После 20 неудачных попыток подряд endpoint отключается, а на остальные ваши endpoint’ы приходит webhook.endpoint_disabled. Новые события для отключённого endpoint’а копятся (до 1000 ожидающих доставок на endpoint) и доставляются после включения (enable или кнопка в кабинете). Любая успешная доставка обнуляет счётчик ошибок.
  • Очередь. На один endpoint ждут отправки не больше 1000 доставок; новые события сверх этого на него не ставятся, а у endpoint’а появляется lastError: "queue_full" — актуальное состояние заказов берите из GET /orders. Отправка распределяется между аккаунтами поровну: медленный endpoint задерживает только свои доставки.
  • Безопасность адресов. Принимаются только https:// URL до 500 символов, на порт 443 или 8443, без логина и пароля, ведущие на публичные адреса: частные, локальные и зарезервированные IP, а также имена, которые не резолвятся, отклоняются (400 bad_url). Адрес проверяется и при каждой попытке (ответ DNS кэшируется на минуту) — соединение идёт на проверенный IP.
  • Ошибки доставки. В lastError — класс ошибки: HTTP <код>, timeout, connection_failed, tls_error, url_rejected (адрес перестал проходить проверку), queue_full, endpoint_deleted.
  • Retries. If the endpoint does not answer 2xx within 10 seconds, delivery is retried: immediately, then after 1 min, 5 min, 15 min and 60 min — 5 attempts in total. Then the delivery is marked failed; you can retry it manually (redeliver) or in the dashboard.
  • Auto-disable. After 20 failed attempts in a row the endpoint is disabled and webhook.endpoint_disabled is sent to your other endpoints. New events for a disabled endpoint are kept (up to 1000 pending deliveries per endpoint) and delivered once it is enabled again (enable or the dashboard button). Any successful delivery resets the failure counter.
  • Queue. At most 1000 deliveries wait per endpoint; further events are not queued for it and the endpoint shows lastError: "queue_full" — take the current order state from GET /orders. Sending is shared fairly between accounts: a slow endpoint delays only its own deliveries.
  • Address safety. Only https:// URLs up to 500 chars, on port 443 or 8443, without username/password, pointing to public addresses are accepted: private, loopback and reserved IPs, and names that do not resolve, are rejected (400 bad_url). The address is checked again on every attempt (the DNS answer is cached for a minute) — the connection goes to the vetted IP.
  • Delivery errors. lastError holds an error class: HTTP <code>, timeout, connection_failed, tls_error, url_rejected (the address no longer passes the check), queue_full, endpoint_deleted.

Вебхуки через API

Управление вебхукамиManaging webhooks

То же самое можно делать в кабинете, в разделе «API».

You can do the same in the dashboard, “API” section.

Список endpoint’овList endpoints

GET/webhooks
[
  {
    "id": "3f6c2b1e-8a4d-4e7f-9b0c-5d2e1a7f8c63",
    "url": "https://shop.example.com/mms/webhook",
    "events": ["order.completed", "order.failed"],
    "disabled": false,
    "disabledAt": null,
    "consecutiveFailures": 0,
    "createdAt": 1791700000000
  }
]

Добавить endpointCreate an endpoint

POST/webhooks

Возвращает secret для проверки подписи — сохраните его. Секрет также можно посмотреть в кабинете.

Returns the secret for signature verification — store it. You can also view it in the dashboard.

Тело запроса

urlstringобязательный
https:// URL на публичном адресе, до 500 символов.An https:// URL on a public address, up to 500 chars.
eventsstring[]
События order.*; пустой или не передан — все.order.* events; empty or missing — all.
curl -X POST {{BASE}}/webhooks \
  -H "Authorization: Bearer $MMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://shop.example.com/mms/webhook", "events": ["order.completed", "order.failed"]}'
{
  "id": "3f6c2b1e-8a4d-4e7f-9b0c-5d2e1a7f8c63",
  "url": "https://shop.example.com/mms/webhook",
  "events": ["order.completed", "order.failed"],
  "secret": "whsec_q8V2nL5xT0bR7mK3pW9sD1fG4hJ6cZ8aE2yU5iO7tN0"
}

Ошибки

400 bad_params400 bad_url409 too_many_endpoints429 rate_limit

Удалить endpointDelete an endpoint

DELETE/webhooks/{id}

Неотправленные доставки этого endpoint’а отменяются.

Pending deliveries of this endpoint are dropped.

{ "ok": true }

Ошибки

404 not_found

Журнал доставокDelivery log

GET/webhooks/{id}/deliveries?limit=50

Последние доставки endpoint’а, новые сверху. limit — 1–200, по умолчанию 50. Журнал хранится 14 дней.

Latest deliveries of the endpoint, newest first. limit is 1–200, default 50. The log is kept for 14 days.

{
  "deliveries": [
    {
      "id": "d2a9c4e7-1f3b-4a6d-8e0c-9b5f7a2d1c48",
      "eventId": "6b1f0c8e-3d2a-4f7b-9c51-2e8a7d4b90f3",
      "type": "order.completed",
      "status": "delivered",
      "attempts": 1,
      "lastStatus": 200,
      "lastError": null,
      "createdAt": 1791800103120,
      "deliveredAt": 1791800103388,
      "nextAttemptAt": null
    }
  ]
}

Ошибки

400 bad_params404 not_found

Повторить доставкуRedeliver

POST/webhooks/{id}/deliveries/{deliveryId}/redeliver

Ещё одна попытка для доставки в статусе failed — с тем же телом и eventId.

One more attempt for a failed delivery — same body and eventId.

{ "ok": true }

Ошибки

404 not_found409 not_failed409 endpoint_disabled429 rate_limit

Тестовое событиеSend a test event

POST/webhooks/{id}/test

Ставит в очередь событие webhook.test — удобно проверить приём и подпись.

Queues a webhook.test event — handy to check reception and signature.

{ "ok": true }
{
  "event": "webhook.test",
  "eventId": "71e0b5c3-6d2f-4a98-b4c1-0e8f3d7a2b56",
  "createdAt": "2026-10-12T10:30:00.000Z",
  "at": 1791801000000,
  "id": "3f6c2b1e-8a4d-4e7f-9b0c-5d2e1a7f8c63"
}

Ошибки

404 not_found409 endpoint_disabled429 rate_limit

Для отключённого endpoint’а — 409 endpoint_disabled: сначала включите его (enable).

For a disabled endpoint — 409 endpoint_disabled: enable it first (enable).

Включить endpointEnable an endpoint

POST/webhooks/{id}/enable

Включает автоматически отключённый endpoint и обнуляет счётчик ошибок; накопленные доставки отправятся.

Re-enables an auto-disabled endpoint and resets the failure counter; queued deliveries resume.

{ "ok": true }

Ошибки

404 not_found429 rate_limit

Служебное

Health

GET/healthбез ключа

Проверка доступности API для мониторинга. Ключ не нужен, лимитов нет.

API availability check for monitoring. No key, no rate limit.

{ "ok": true }

OpenAPI

GET/openapi.jsonбез ключа

Машиночитаемое описание API в формате OpenAPI 3.1 — импортируйте его в Postman, Insomnia или генератор SDK: /api/v1/openapi.json.

Machine-readable OpenAPI 3.1 description — import it into Postman, Insomnia or an SDK generator: /api/v1/openapi.json.