Введение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 aloginUrland 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, eventorder.refunded) — after a check if the charge status is unclear. - Tracking. Webhooks (recommended), long-poll
GET /orders/{id}or batchPOST /orders/lookup. - Support. Every response carries an
X-Request-Idheader — 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 inGET /balance(tariff.feeUsd,maxActiveOrders,rateLimit).
Быстрый стартQuick start
- Откройте кабинет → раздел API и скопируйте ключ
mms_live_…. Сохраните его в переменной окруженияMMS_API_KEYна сервере. - Проверьте баланс:
GET /balance. - Создайте заказ:
POST /ordersс заголовкомIdempotency-Key— номером заказа в вашем магазине. - Отправьте покупателю
loginUrlи дождитесь статусаdelivered— через вебхукorder.completedили long-poll.
- Open the dashboard → API section and copy your
mms_live_…key. Keep it in theMMS_API_KEYenvironment variable on your server. - Check your balance:
GET /balance. - Create an order:
POST /orderswith anIdempotency-Keyheader — your store’s order number. - Send the
loginUrlto the buyer and wait fordelivered— via theorder.completedwebhook 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;
Ключ даёт доступ к балансу — храните его только на сервере. Не вставляйте ключ в код сайта, мобильного приложения или публичные репозитории: 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) andX-MMS-Terms-Accepted(trueif you have accepted it). New versions are accepted in the dashboard. If the operator has switched this requirement on,POST /orderswithout the accepted version returns403 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 | Код | Когда |
|---|---|---|
| 401 | unauthorized | Ключ не передан, неверный или перевыпущен.The key is missing, invalid or was regenerated. |
| 403 | account_blocked | Аккаунт заблокирован.The account is blocked. |
| 403 | terms_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. |
| 403 | api_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 key | 240 / мин по умолчанию240 / min by default | 429 rate_limit + Retry-After |
| Активных заказовActive orders | Задаётся для каждого клиента — ваше значение в maxActiveOrders ответа GET /balanceSet per client — your value is maxActiveOrders in GET /balance | 429 too_many_active (limit) |
| Действий с заказамиOrder actions | 60 / мин на аккаунт (cancel, answer, relink)60 / min per account (cancel, answer, relink) | 429 rate_limit |
| Цена суммы вне каталогаNon-catalog amount pricing | 10 подтверждений цены / мин на аккаунт10 price confirmations / min per account | 429 rate_limit |
| Создание endpoint’ов вебхуковWebhook endpoint creation | 30 / час30 / hour | 429 rate_limit |
| Тестовых вебхуковTest webhooks | 30 / 15 мин30 / 15 min | 429 rate_limit |
| Повторных доставок (redeliver)Redeliveries | 300 / 15 мин300 / 15 min | 429 rate_limit |
| Включений endpoint’а (enable)Endpoint re-enables | 10 / 15 мин10 / 15 min | 429 rate_limit |
| Неудачной авторизацииFailed auth | 30 / 15 мин на IP30 / 15 min per IP | 429 rate_limit |
| Одновременных long-pollConcurrent long-polls | 20 на ключ20 per key | ответ сразу, без ожиданияanswered at once, no wait |
| Размер тела запросаRequest body size | 64 KB | 413 too_large |
| id в одном lookupids per lookup | 100 | 400 too_many_ids |
| Endpoint’ов вебхуковWebhook endpoints | 5 | 409 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.
Нужны лимиты выше или индивидуальные цены (опт)? Пишите на e-mail — индивидуальные условия согласуем письменно по e-mail. Действующие лимиты вашего ключа всегда видны в GET /balance (rateLimit, maxActiveOrders).
Need higher limits or individual (wholesale) pricing? E-mail us at — individual terms are agreed in writing by e-mail. The current limits of your key are always in GET /balance (rateLimit, maxActiveOrders).
Ошибки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 | Код | Когда | Доп. поля |
|---|---|---|---|
| 400 | bad_params | Поле отсутствует или неверного типа/форматаA field is missing or has a wrong type/format | field / missing |
| 400 | bad_json | Тело — не валидный JSONThe body is not valid JSON | |
| 400 | bad_content_type | POST с телом не в application/jsonPOST with a body that is not application/json | |
| 400 | bad_amount | Сумма вне диапазона или не собирается точно — используйте robux из /quoteAmount out of range or not assembled exactly — use robux from /quote | min, max, suggestedAmount |
| 400 | idempotency_key_required | POST /orders без Idempotency-KeyPOST /orders without Idempotency-Key | |
| 400 | bad_idempotency_key | Ключ не соответствует ^[A-Za-z0-9_.:-]{1,64}$The key does not match ^[A-Za-z0-9_.:-]{1,64}$ | |
| 400 | invalid_cursor | Неверный cursorBad cursor | |
| 400 | too_many_ids | Больше 100 id/ключей в lookupMore than 100 ids/keys in a lookup | |
| 400 | bad_answer | Пустой ответ на шаг входаEmpty answer to a sign-in step | |
| 400 | bad_url | URL вебхука отклонён, причина — в detail (английский текст)Webhook URL rejected, the reason is in detail | |
| 401 | unauthorized | Ключ не передан, неверный или отозванMissing, invalid or revoked key | |
| 402 | insufficient_balance | Недостаточно средств на балансеNot enough balance | balanceUsd, requiredUsd |
| 403 | account_blocked | Аккаунт заблокированThe account is blocked | |
| 403 | api_disabled | API отключён для аккаунтаAPI is disabled for the account | |
| 403 | terms_not_accepted | Не принята действующая редакция правил (POST /orders, если оператор включил требование)The current Terms of Use version is not accepted (POST /orders, if the operator has switched this on) | termsVersion |
| 404 | no_order | Заказ не найден или принадлежит другому аккаунтуOrder not found or belongs to another account | |
| 404 | not_found | Endpoint вебхука или доставка не найденыWebhook endpoint or delivery not found | |
| 404 | unknown_endpoint | Такого метода API нетNo such API route | |
| 409 | idempotency_conflict | Тот же Idempotency-Key с другими параметрамиSame Idempotency-Key with different parameters | orderId |
| 409 | price_changed | Передан expectedTotalUsd, а цена другаяexpectedTotalUsd given and the price differs | totalUsd |
| 409 | not_cancellable | Заказ уже нельзя отменитьThe order can no longer be cancelled | |
| 409 | in_progress | С заказом выполняется операция — повторите чуть позжеAn operation on this order is running — retry shortly | |
| 409 | not_waiting | Ответ отправлен, но заказ его не ждётAnswer sent but the order does not wait for one | |
| 409 | stale_step | stepId не совпадает с текущим запросомstepId does not match the current prompt | |
| 409 | not_relinkable | Довыдача невозможнаRelink is not possible | |
| 409 | order_refunded | Остаток уже возвращён на баланс — довыдача невозможнаThe remainder was already refunded — relink is not possible | |
| 409 | link_expired | Зарезервирован: сейчас методы API его не возвращают (истёкшая ссылка — статус заказа expired)Reserved: no API method returns it today (an expired link shows as order status expired) | |
| 409 | too_many_endpoints | Больше 5 endpoint’ов вебхуковMore than 5 webhook endpoints | limit |
| 409 | not_failed | Повторить можно только неудавшуюся доставкуOnly a failed delivery can be redelivered | |
| 409 | endpoint_disabled | Endpoint отключён — сначала включите егоThe endpoint is disabled — enable it first | |
| 413 | too_large | Тело больше 64 KBBody larger than 64 KB | |
| 429 | rate_limit | Превышен лимит запросовRate limit exceeded | retryAfterSec, заголовокheader Retry-After |
| 429 | too_many_active | Достигнут лимит активных заказовActive-order cap reached | limit |
| 429 | too_many_open_links | Сеть доставки занята открытыми ссылками (довыдача) — повторите позжеThe delivery network is busy with open sign-in links (relink) — retry later | |
| 502 | upstream_error | Ошибка сети доставкиDelivery network error | |
| 503 | temporarily_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 | |
| 500 | internal | Внутренняя ошибка — сообщите нам 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": trueand theIdempotent-Replayed: trueheader, regardless of the current price and balance. - Same key, different parameters (
amount,mode,targetor presence ofpass) —409 idempotency_conflictwith the existingorderId. - A key is unique within your account and never expires. It is stored in the order’s
keyfield — find the order withGET /orders/by-external-id/{key}orPOST /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.
idstringM-XXXXXX (6 символов A–Z, 0–9). На входе регистр не важен.Public order id, format M-XXXXXX (6 chars A–Z, 0–9). Case-insensitive on input.keystring | nullIdempotency-Key; null у заказов из кабинета.Your Idempotency-Key; null for dashboard orders.amountintegerdeliveredintegermode"link" | "direct"robloxUsernamestring | nulltarget (в режиме 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 | nulllink: ссылка на нашу страницу входа — отправьте её покупателю. Для direct — null.Link mode only: our sign-in page — send it to the buyer. null for direct.linkExpiresAtinteger | nullexpired с возвратом.When the link expires (ms). An unfinished order then becomes expired and is refunded.opened, openedAtboolean, integer | nullpriceUsd, feeUsd, totalUsdnumberrefundedUsdnumbercancellablebooleanrelinkablebooleanpromptobject | nullneed_* — см. ниже.What the buyer must do while the status is need_* — see below.reasonCode, reasonstring | nullfailureobject | 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"revisionstringfrom_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, completedAtintegercompletedAt — 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 required | |
sq | Проверка безопасности: выбрать 3 игры из games, в которые покупатель недавно игралSecurity check: pick 3 games from games the buyer played recently | id игр через запятую: 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 | — |
| delivered | Robux полностью зачислены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 | ✓ |
Для фильтра 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 password | buyer_action_required |
account_locked | Аккаунт Roblox заблокированThe Roblox account is locked | buyer_action_required |
too_many_attempts | Слишком много попыток входа, попробуйте позжеToo many sign-in attempts; try again later | buyer_action_required |
passkey_required | Аккаунт требует вход по passkey — не поддерживаетсяThe Roblox account requires a passkey, which is not supported | buyer_action_required |
twofa_failed | Двухэтапная проверка не пройденаTwo-step verification was not completed | buyer_action_required |
security_question | Проверка безопасности не пройденаThe security question was not answered correctly | buyer_action_required |
captcha_failed | Не удалось пройти captchaThe captcha could not be completed | buyer_action_required |
login_cancelled | Вход отменёнThe sign-in was cancelled | buyer_action_required |
login_failed | Не удалось войтиThe sign-in failed | buyer_action_required |
buyer_unverified | Покупатель не подтвердил входThe buyer did not confirm the sign-in | buyer_action_required |
link_expired | Ссылка истекла до входа покупателяThe buyer link expired before sign-in | buyer_action_required |
cancelled_by_client | Заказ отменёнThe order was cancelled | buyer_action_required |
unsupported_amount | Эту сумму нельзя выдатьThis amount cannot be delivered | permanent_failure |
purchase_failed | Не удалось провести покупкуThe purchase could not be completed | temporary_unavailable |
grant_failed | Robux не удалось зачислить полностьюRobux could not be credited in full | temporary_unavailable |
device_unavailable | Выдача временно недоступнаDelivery is temporarily unavailable | temporary_unavailable |
interrupted | Выдача прерванаDelivery was interrupted | temporary_unavailable |
internal | Внутренняя ошибка — напишите в поддержкуInternal error; contact support | temporary_unavailable |
Эндпоинты
Баланс и ценыBalance & prices
Баланс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
Массив доступных пакетов с вашими ценами. 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
Цена любой суммы. Если сумму нельзя собрать точно, 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обязательный{
"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
}
Ошибки
Эндпоинты
Заказы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
Создаёт заказ и сразу списывает 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обязательный[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 из каталога или /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^[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.passstringdirect. Не хранится, не логируется и не возвращается.Buyer’s password (1–128 chars), direct only. Never stored, logged or echoed.expectedTotalUsdnumber409 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
}
Ошибки
Сеть доставки при создании не вызывается — исключение: сумма вне каталога, её цену мы подтверждаем у сети доставки (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
Заказы аккаунта (из 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.
Параметры запроса
limitintegercursorstringnextCursor.Opaque string from nextCursor.statusstringactive, done, failed, например status=active,partial.Comma-separated statuses and/or groups active, done, failed, e.g. status=active,partial.source"api" | "cabinet"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"
}
Ошибки
Получить заказRetrieve an order
Текущее состояние заказа. Поддерживает 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обязательныйM-XXXXXX, регистр не важен.Order id M-XXXXXX, case-insensitive.Параметры запроса
from_revisionstringrevision.The last known revision.max_wait_msintegercurl "{{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
}
Ошибки
Заказ по вашему ключуOrder by external id
Находит заказ по 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"
Ошибки
Пакетный поискBatch 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[]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"]
}
Ошибки
Отменить заказCancel an order
Отмена возможна, пока ничего не зачислено и нет активной попытки выдачи (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 }
}
Ошибки
Ответ на шаг входаAnswer a sign-in step
Передаёт ответ покупателя на текущий 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обязательныйsq — id игр через запятую.Up to 200 chars. For sq — game ids comma-separated.stepIdstringprompt.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" }
}
Ошибки
Довыдача остаткаRelink (deliver the remainder)
Довыдаёт остаток заказа в статусе 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.
Тело запроса
passstringdirect (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 }
}
Ошибки
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
Хронология заказа теми же типами событий, что и в вебхуках, плюс 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.
Ошибки
Уведомления
Вебхуки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
2xxwithin 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
orderis a snapshot at event time; compareorder.updatedAtor fetch the fresh state withGET /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.processingmay arrive twice (queued, thenrunning). Branch onstatus;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 step | action, stepId |
order.needs_retry | Попытка входа не удалась, ссылка ещё действует — покупатель может попробовать сноваA sign-in attempt failed, the link is still valid — the buyer can try again | reasonCode |
order.completed | Robux зачислены полностьюRobux fully credited | |
order.partial | Зачислена часть суммыPartly credited | reasonCode, reason |
order.failed | Заказ не выполненOrder failed | reasonCode, reason |
order.cancelled | Заказ отменёнOrder cancelled | reasonCode, reason |
order.expired | Ссылка истеклаThe link expired | reasonCode, reason |
order.refunded | Деньги возвращены на балансMoney returned to the balance | refundUsd, refundedUsd |
webhook.test | Тестовое событие (test)Test event (test) | id — id endpoint’аid — endpoint id |
webhook.endpoint_disabled | Другой ваш endpoint отключён из-за ошибокAnother endpoint of yours was disabled after failures | id, 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-Type | application/json |
User-Agent | MMS-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-Id | Id доставки на этот 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-Timestamp | Unix-время отправки в секундахSend time, Unix seconds |
X-MMS-Signature | t=<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
2xxwithin 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 markedfailed; 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_disabledis 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 fromGET /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.
lastErrorholds 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
[
{
"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
Возвращает 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"
}
Ошибки
Удалить endpointDelete an endpoint
Неотправленные доставки этого endpoint’а отменяются.
Pending deliveries of this endpoint are dropped.
{ "ok": true }
Ошибки
Журнал доставокDelivery log
Последние доставки 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
}
]
}
Ошибки
Повторить доставкуRedeliver
Ещё одна попытка для доставки в статусе failed — с тем же телом и eventId.
One more attempt for a failed delivery — same body and eventId.
{ "ok": true }
Ошибки
Тестовое событиеSend a test event
Ставит в очередь событие 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"
}
Ошибки
Для отключённого endpoint’а — 409 endpoint_disabled: сначала включите его (enable).
For a disabled endpoint — 409 endpoint_disabled: enable it first (enable).
Включить endpointEnable an endpoint
Включает автоматически отключённый endpoint и обнуляет счётчик ошибок; накопленные доставки отправятся.
Re-enables an auto-disabled endpoint and resets the failure counter; queued deliveries resume.
{ "ok": true }
Ошибки
Служебное
Health
Проверка доступности API для мониторинга. Ключ не нужен, лимитов нет.
API availability check for monitoring. No key, no rate limit.
{ "ok": true }
OpenAPI
Машиночитаемое описание 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.