API для мерчантов
Всё, что нужно для интеграции: как подписывать запросы, какие ручки есть и что означают ответы. Приём платежей по СБП и банковским картам.
| Параметр | Значение |
|---|---|
| Базовый адрес | https://api.e00q.micropenis.online |
| Префикс путей | /api/v1/ |
| Формат | JSON, Content-Type: application/json; charset=utf-8 |
Быстрый старт
1. Получите API-ключ
Ключ выдаётся при подключении и выглядит как ak_<идентификатор>_<секрет>. Секретную часть не публикуйте: подпись подделает любой, у кого есть ключ целиком. Ключ заводится в личном кабинете.
2. Проверьте связь
Единственная ручка без подписи:
curl -s https://api.e00q.micropenis.online/api/v1/ping
{"message":"pong"}3. Отправьте первый подписанный запрос
API_KEY='ak_демо_секрет'
BODY='{"merchant_order_id":"order-1","subject":"Заказ 1","amount":{"amount_minor":"10000","currency":"RUB"}}'
SIG=$(printf '%s' "$BODY" | base64 | tr -d '\n' \
| openssl dgst -sha256 -hmac "$API_KEY" -hex | awk '{print $2}')
TS=$(python3 -c 'import time; print(int(time.time()*1000))')
NONCE=$(python3 -c 'import uuid; print(uuid.uuid4())')
curl -s -X POST https://api.e00q.micropenis.online/api/v1/payments \
-H 'Content-Type: application/json' \
-H "X-Api-Key: $API_KEY" -H "X-Signature: $SIG" \
-H "X-Timestamp: $TS" -H "X-Nonce: $NONCE" \
-d "$BODY"Аутентификация
Токенов нет: каждый запрос несёт ключ и подпись тела. Четыре заголовка, все обязательные.
| Заголовок | Содержимое |
|---|---|
X-Api-Key | Ключ целиком, включая секретную часть |
X-Signature | Подпись тела, нижний регистр hex |
X-Timestamp | Время отправки, Unix-миллисекунды |
X-Nonce | Уникальное значение на каждый запрос (подойдёт UUID) |
Как считается подпись
- Тело сериализуется в компактный JSON — без отступов и переводов строк.
- Компактный JSON кодируется в base64. Шаг обязателен: он выравнивает поведение разных языков на не-ASCII символах.
- Считается HMAC-SHA256 от base64-строки на ключе; результат — hex в нижнем регистре.
signature = lower(hex(HMAC_SHA256(base64(compact_json(body)), api_key)))Запросы без тела (GET) подписываются по пустой строке.
Примеры
payload := base64.StdEncoding.EncodeToString(compactJSON)
mac := hmac.New(sha256.New, []byte(apiKey))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))payload = base64.b64encode(compact_json.encode()).decode()
signature = hmac.new(api_key.encode(), payload.encode(), hashlib.sha256).hexdigest()const payload = Buffer.from(compactJson).toString("base64");
const signature = crypto.createHmac("sha256", apiKey).update(payload).digest("hex");$payload = base64_encode($compactJson);
$signature = hash_hmac('sha256', $payload, $apiKey);Окно времени и повторы
X-Timestamp должен попадать в окно в несколько минут вокруг текущего времени — синхронизируйте часы по NTP. X-Nonce должен быть уникален: повтор ранее использованного значения отклоняется, даже если подпись верна. Так перехваченный запрос нельзя отправить второй раз.
Права ключа
Подпись отвечает на вопрос «кто пришёл», права — «что этому ключу можно». У каждой ручки своё право; они выдаются при подключении.
| Право | Что открывает |
|---|---|
payments:write | Создание платежей |
payments:read | Чтение платежей |
balances:read | Остаток счёта |
operations:read | Лента операций |
Ручка ответила 403 — запросите недостающее право у менеджера. Какого именно права не хватает, ответ не сообщает.
Формат ответа
Успех — данные всегда в поле data:
{"data": {"payment_uuid": "0192f7a1-8c3d-7c2b-9f4e-1a2b3c4d5e6f"}}Списки дополняются meta:
{"data": [], "meta": {"next_page_token": ""}}Пустой список приходит как [], а не null — можно обходить без проверки.
Ошибка — всегда одно поле error с текстом, иногда с разбором по полям:
{"error": "Unauthorized"}
{"error": "Validation failed", "errors": {"amount": ["required"]}}Корреляция
В каждом ответе есть X-Request-ID. Свой идентификатор можно прислать в одноимённом заголовке запроса — принимается значение в формате UUID, иначе выдаётся своё. Логируйте его: это единственное, что нужно поддержке для поиска конкретного запроса.
Коды состояния
| Код | Что означает | Что делать |
|---|---|---|
| 200 | Успех | — |
| 401 | Не аутентифицирован | Проверьте ключ, подпись, время и nonce |
| 403 | Ключу не хватает права либо мерчант не активирован | Обратитесь к менеджеру |
| 404 | Ручки или объекта не существует | Проверьте путь и идентификатор |
| 405 | Метод не поддержан | Смотрите заголовок Allow в ответе |
| 409 | Заказ с таким идентификатором уже есть с другими параметрами | Используйте новый merchant_order_id |
| 413 | Тело запроса слишком большое | Уменьшите объём данных |
| 422 | Запрос не прошёл проверку | Исправьте поля, указанные в errors |
| 429 | Слишком часто | Дождитесь Retry-After |
| 503, 504 | Временная недоступность | Повторите позже, запрос не выполнен |
401 одинаков для всех причин — неверный ключ, неверная подпись, просроченное время и повтор nonce дают один и тот же ответ. Иначе по разнице ответов подбирались бы значения.
503 и 504 не означают отказ в доступе: ключ в порядке, запрос просто не выполнен — повторите его позже.
Ограничение частоты
Частота ограничена по адресу и по ключу. Превышение даёт 429:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{"error":"Too many requests"}Retry-After — точное число секунд до момента, когда освободится место, а не фиксированная пауза. Не повторяйте запрос раньше: повторы внутри этого времени только продлевают ограничение. Остаток лимита в ответе не сообщается.
Платежи
POST /api/v1/payments — создать платёж
Требует права payments:write. Повтор запроса с тем же merchant_order_id возвращает существующий платёж, а не создаёт второй. Отдельного заголовка идемпотентности нет — ключом служит сам идентификатор заказа.
{
"merchant_order_id": "order-42",
"subject": "Подписка на месяц",
"amount": {"amount_minor": "10000", "currency": "RUB"},
"payment_method": "PAYMENT_METHOD_SBP",
"payer": {"id": "payer-7", "name": "Иван"},
"payload": "любая ваша строка"
}| Поле | Обязательно | Описание |
|---|---|---|
merchant_order_id | да | Ваш идентификатор заказа. Уникален в пределах мерчанта, он же ключ идемпотентности |
subject | нет | Назначение платежа, видит плательщик |
amount | да | Сумма в минимальных единицах валюты |
payment_method | нет | Способ оплаты; не указан — берётся ваш способ по умолчанию |
payer | нет | Плательщик: id и name. Нужен антифроду; стабильный id между платежами одного человека улучшает проходимость |
payload | нет | Сквозная строка, возвращается как есть |
{
"data": {
"payment_uuid": "0192f7a1-8c3d-7c2b-9f4e-1a2b3c4d5e6f",
"merchant_order_id": "order-42",
"subject": "Подписка на месяц",
"amounts": {
"amount": {"amount_minor": "10000", "currency": "RUB"},
"charged": {"amount_minor": "10500", "currency": "RUB"},
"commission": {"amount_minor": "500", "currency": "RUB"},
"amount_rub": {"amount_minor": "10500", "currency": "RUB"},
"quote_id": "",
"rate": "1.0"
},
"commission_payer": "COMMISSION_PAYER_CLIENT",
"payment_method": "PAYMENT_METHOD_SBP",
"status": "PAYMENT_STATUS_PENDING",
"external_id": "",
"payment_url": "https://pay.e00q.example/0192f7a1",
"created_at": "2026-08-25T09:00:00Z",
"expires_at": "2026-08-25T10:00:00Z",
"paid_at": "",
"payload": "любая ваша строка"
}
}payment_url — ссылка, которую вы показываете плательщику. По ней открывается страница оплаты: QR СБП или форма карты.
GET /api/v1/payments/{payment_uuid} — платёж по идентификатору
Требует права payments:read. Чужой платёж отвечает 404: другой код подтвердил бы, что такой платёж существует.
GET /api/v1/payments — список платежей
| Параметр | Описание |
|---|---|
page_size | Размер страницы. Не указан — размер по умолчанию |
page_token | Курсор следующей страницы из meta.next_page_token |
status | Фильтр по исходу платежа |
period_start, period_end | Период создания, полуинтервал [начало, конец), RFC 3339 |
Листание идёт курсором, а не номером страницы: список пополняется во время обхода, и нумерация показала бы одну запись дважды, а соседнюю пропустила. Пустой next_page_token — страница последняя. Содержимое курсора непрозрачно, не разбирайте его.
Счёт и операции
GET /api/v1/balances — остаток
Требует права balances:read. Счёт определяется вашим ключом: назвать чужой счёт в параметрах нельзя и не нужно.
{
"data": {
"amount": {"amount_minor": "125000", "currency": "USD"},
"projection_lag_seconds": "0",
"synced_at": ""
}
}Остаток может быть отрицательным — это задолженность, а не ошибка. Не приводите знак к нулю на своей стороне.
GET /api/v1/operations — лента операций
Требует права operations:read. Параметры те же, что у списка платежей.
{
"data": [
{
"operation_uuid": "0192f7a1-8c3d-7c2b-9f4e-1a2b3c4d5e6f",
"type": "payment",
"amount": {"amount_minor": "25000", "currency": "RUB"},
"payment_uuid": "0192f7a1-8c3d-7c2b-9f4e-1a2b3c4d5e6f",
"created_at": "2026-08-25T07:36:12Z"
}
],
"meta": {"next_page_token": ""}
}Важно, чтобы не потерять время:
- лента показывает те же платежи, что и
GET /api/v1/payments, только в форме операций; - свести по ней остаток нельзя — удержаний комиссии и выплат в ней нет; остаток берите из
/api/v1/balances; operation_uuidравенpayment_uuid— отдельной сущности нет, второй идентификатор хранить не нужно;amount— сумма платежа, а не сумма зачисления: они расходятся на комиссию, когда её платите вы.
Курсы валют
GET /api/v1/rates — витрина спот-курсов. Требует подписи, отдельного права не требует.
{
"data": [
{"from_currency": "USDT", "to_currency": "RUB", "rate": "78.50",
"fetched_at": "2026-08-25T09:00:00Z"}
]
}Курс — десятичная строка, а не число: округление курса это потерянные деньги. Считайте на своей стороне в decimal-типе.
Словари
Деньги
{"amount_minor": "10000", "currency": "RUB"}amount_minor — сумма в минимальных единицах валюты (копейки, центы), строкой: JSON-число потеряло бы точность на больших значениях. 10000 при RUB — это 100 рублей 00 копеек.
Суммы платежа
| Поле | Что это |
|---|---|
amount | Сумма, которую запросили вы |
charged | Сумма, предъявленная плательщику: amount плюс комиссия, если её платит плательщик |
commission | Комиссия платформы по этому платежу |
amount_rub | Сумма в рублях. Пока quote_id пуст — справочная величина по текущему курсу |
rate | Применённый курс, десятичная строка |
Статусы платежа
| Значение | Что означает |
|---|---|
PAYMENT_STATUS_PENDING | Создан, ждёт действия плательщика |
PAYMENT_STATUS_CONFIRMED | Деньги получены |
PAYMENT_STATUS_CANCELED | Не состоялся: не оплачен, истёк срок или отклонён |
PAYMENT_STATUS_CHARGEBACKED | Ранее подтверждённые деньги списаны обратно |
Повторная оплата — это новый платёж с новым merchant_order_id, а не возврат прежнего в PENDING.
Способы оплаты
| Значение | Что означает |
|---|---|
PAYMENT_METHOD_UNSPECIFIED | Взять ваш способ по умолчанию |
PAYMENT_METHOD_SBP | Система быстрых платежей |
PAYMENT_METHOD_CARD | Банковская карта |
Плательщик комиссии
| Значение | Что означает |
|---|---|
COMMISSION_PAYER_MERCHANT | Комиссию платите вы: плательщик платит сумму заказа, вам зачисляется сумма минус комиссия |
COMMISSION_PAYER_CLIENT | Комиссию платит плательщик: он платит сумму плюс комиссию, вам зачисляется полная сумма заказа |
Значение фиксируется в платеже на момент создания: смена ваших настроек не переписывает уже созданные платежи.
Частые ошибки интеграции
| Симптом | Причина |
|---|---|
| 401 при верном ключе | Подписали не то, что отправили: подпись считается по компактному JSON, а библиотека сериализует с отступами |
| 401 | Пропустили base64 — шаг обязателен |
| 401 | Hex в верхнем регистре; ожидается нижний |
| 401 | Разъехались часы: расхождение больше окна. Синхронизируйте по NTP |
| 401 | Переиспользовали nonce. UUID подходит; счётчик в памяти процесса — нет, после перезапуска он начнётся заново |
| 401 | Прислали ключ не целиком: X-Api-Key — полное значение с секретной частью |
| 405 на POST | Подмена метода не поддерживается: X-HTTP-Method-Override игнорируется |
| Плательщик видит не ту сумму | Читаете amount вместо charged — они различаются, когда комиссию платит плательщик |
Практики интеграции
- Повторяйте по идемпотентному ключу. Если ответ на создание платежа не дошёл, повторите запрос с тем же
merchant_order_id— придёт тот же платёж, второго списания не будет. - Уважайте Retry-After. На
429подождите указанное число секунд. 503и504повторяемые. Повторяйте с нарастающей паузой.- Логируйте X-Request-ID. Это единственное, что нужно поддержке для поиска запроса.
- Не выводите секретную часть ключа в логи и не кладите её в клиентский код: подпись считается на вашем сервере.
- Сверяйте состояние по ручке платежа, а не по собственным ожиданиям: статус меняет платформа.
Поддержка
При обращении приложите:
X-Request-IDиз ответа;- время запроса и открытый префикс ключа (
ak_<идентификатор>, без секретной части); - путь и метод.
Ни ключ целиком, ни подпись, ни тело запроса присылать не нужно.