API MEVPAY
MEVPAY — некастодіальний шлюз криптоплатежів. Ви створюєте платіж через REST API, клієнт оплачує на нашій сторінці, а кошти надходять одразу на ваш гаманець (USDT TRC20, USDT TON або TON). Про результат ми повідомляємо ваш сервер підписаним вебхуком.
https://pay.mevcore.com/v1- Формат: JSON у запитах і відповідях, кодування UTF-8.
- Автентифікація: секретний ключ у заголовку
Authorization: Bearer. - Суми завжди передаються десятковими рядками (
"25.00"), ніколи числами з плаваючою комою. Значенняamount_atomic— ціле число в найменших одиницях активу. - Повна специфікація: OpenAPI 3.1. Її можна відкрити в Swagger UI, Redoc або Postman.
Швидкий старт
- Зареєструйтеся, створіть організацію та магазин.
- Додайте гаманець лише для перегляду, тобто публічні адреси TRON або TON. Сід-фрази й приватні ключі не потрібні та не приймаються.
- Увімкніть хоча б один спосіб оплати: USDT TRC20, USDT TON або TON.
- У розділі Розробникам → Ключі API створіть тестовий ключ
sk_test_…з правамиpayments:writeіpayments:read. Секрет показується лише один раз. - У розділі Розробникам → Вебхуки додайте HTTPS-адресу вашого сервера та збережіть секрет підпису
whsec_….
Створіть платіж. Завжди передавайте Idempotency-Key: тоді повторний запит не створить другий платіж.
curl -sS https://pay.mevcore.com/v1/payments \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: order-10482-1" \ -H "Content-Type: application/json" \ -d '{ "store_id": "store_123", "external_order_id": "10482", "amount": "25.00", "currency": "USD", "description": "VPS Hosting", "success_url": "https://shop.example/orders/10482", "cancel_url": "https://shop.example/cart" }'
Відповідь (скорочено):
{
"id": "pay_abc123",
"object": "payment",
"livemode": false,
"status": "CREATED",
"amount": "25",
"currency": "USD",
"checkout_url": "http://localhost:5174/p/pay_abc123",
"expires_at": "2026-10-19T12:30:00+00:00"
}
Перенаправте клієнта на checkout_url і дочекайтеся вебхука payment.paid. Статус платежу також можна перевірити запитом:
curl -sS https://pay.mevcore.com/v1/payments/pay_abc123 \ -H "Authorization: Bearer sk_test_..."
success_url не є доказом оплати, бо цю адресу може відкрити будь-хто. Видавайте товар лише після вебхука зі статусом PAID або OVERPAID, перевіривши підпис.Ключі API та права
Кожен запит має містити секретний ключ:
Authorization: Bearer sk_live_... # бойовий режим Authorization: Bearer sk_test_... # тестовий режим
Тестові ключі бачать лише тестові об’єкти, а бойові лише бойові: кожен об’єкт має поле livemode. Ключ можна прив’язати до одного магазину, тоді store_id можна не передавати, а інші магазини будуть недоступні.
| Право (scope) | Дає доступ |
|---|---|
payments:read | GET /payments, GET /payments/{id}, GET /payment-methods |
payments:write | POST /payments, POST /payments/{id}/expire |
transactions:read | GET /transactions |
wallets:read | GET /wallets |
wallets:write | POST /wallets |
webhooks:read | GET /webhook-endpoints |
webhooks:write | POST /webhook-endpoints, POST /webhook-endpoints/{id}/test |
reports:read | GET /reports/transactions |
Запит без потрібного права отримує відповідь 403 insufficient_scope.
Ідемпотентність
Запити POST, що створюють об’єкти, приймають заголовок Idempotency-Key (друковані ASCII-символи, до 191 символу). Ключ діє в межах організації та режиму (тестового чи бойового):
- той самий ключ і те саме тіло повертають початкову відповідь із заголовком
Idempotent-Replayed: true, і другий платіж не створюється; - той самий ключ з іншим тілом повертає
422 idempotency_key_reused; - дублікат, надісланий, поки перший запит ще обробляється, повертає
409 request_in_progress, тож повторіть його за мить; - відповіді зі статусом 5xx не зберігаються, тому запит можна повторити з тим самим ключем.
Рекомендований формат ключа: номер вашого замовлення плюс номер спроби, наприклад order-10482-1.
Платежі
POST/v1/payments — створити платіж
| Поле | Тип | Опис |
|---|---|---|
amount * | string | Ціна у валюті currency, десятковий рядок, до 8 знаків після коми. |
currency * | string | USD, EUR, UAH, USDT або TON. Фіатні суми конвертуються за курсом на момент виставлення рахунку. |
store_id | string | ID магазину. Необов’язковий, якщо ключ прив’язаний до магазину. |
external_order_id | string | Ваш номер замовлення, повертається у вебхуках. |
description | string | Опис для клієнта, до 500 символів. |
success_url, cancel_url | uri | Куди повернути клієнта після оплати або скасування. |
customer_email | Email клієнта. | |
asset | string | USDT_TRC20, USDT_TON або TON. Якщо не вказано, клієнт обирає актив на сторінці оплати. |
locale | string | Мова сторінки оплати: uk, en, ru або de. |
ttl_minutes | integer | Вікно оплати від 5 до 1440 хвилин, за замовчуванням 30. |
metadata | object | До 20 пар «ключ — рядок», повертаються без змін. |
GET/v1/payments/{id} — отримати платіж
Повертає платіж разом з інструкцією оплати (адреса, сума, memo), транзакціями та історією подій.
GET/v1/payments — список платежів
Найновіші першими. Фільтри: store_id, status, external_order_id, created_from, created_to.
POST/v1/payments/{id}/expire — скасувати
Достроково закриває вікно оплати, наприклад коли клієнт скасував замовлення.
Як зіставляється оплата
Для кожного рахунку гаманець видає унікальну комбінацію: окрему адресу з пулу, унікальну суму (наприклад, 25.000417 USDT) або memo для TON. Тому один переказ ніколи не зарахується двом замовленням. Якщо переказ підходить кільком платежам, платіж отримує статус AMBIGUOUS і потрапляє на ручну перевірку.
Статуси платежу
| Статус | Значення | Що робити |
|---|---|---|
CREATED | Платіж створено, клієнт ще не обрав актив | чекати |
AWAITING_PAYMENT | Видано адресу, суму та memo (якщо потрібен) | чекати |
DETECTED | Переказ знайдено в мережі, але він ще не фінальний | показати «оплату отримано, підтверджуємо» |
CONFIRMING | Набираються підтвердження | чекати |
PAID | Отримано повну суму, переказ фінальний | видати замовлення |
OVERPAID | Отримано більше, ніж очікувалося, переказ фінальний | видати замовлення й за потреби повернути різницю |
PARTIALLY_PAID | Отримано частину суми, вікно ще відкрите | чекати або попросити клієнта доплатити |
UNDERPAID | Вікно закрилося, отримано менше, ніж потрібно | зв’язатися з клієнтом |
EXPIRED | Вікно закрилося, коштів не надійшло | скасувати або створити новий платіж |
AMBIGUOUS | Переказ може належати кільком платежам | вирішити вручну в кабінеті |
REVIEW_REQUIRED | Потрібна ручна перевірка: запізнілий переказ, інший актив або спрацювало правило ризику | вирішити в кабінеті |
FAILED | Остаточна помилка, наприклад скасований переказ | не видавати |
REFUNDED | Зафіксовано повернення | — |
Остаточною оплатою є лише PAID і OVERPAID. Їх виставляємо тільки після перевірки мережі, контракту токена, адреси, суми та кількості підтверджень: для USDT TRC20 це 20 блоків (приблизно хвилина), а транзакції TON фінальні майже одразу.
Довідник ендпоінтів
| Метод і шлях | Право | Опис |
|---|---|---|
POST/v1/payments | payments:write | Створити платіж |
GET/v1/payments | payments:read | Список платежів |
GET/v1/payments/{id} | payments:read | Платіж із транзакціями та подіями |
POST/v1/payments/{id}/expire | payments:write | Закрити вікно оплати |
GET/v1/payment-methods | payments:read | Увімкнені активи та мережі магазину |
GET/v1/transactions | transactions:read | Вхідні перекази на ваші адреси |
GET/v1/wallets | wallets:read | Гаманці лише для перегляду |
POST/v1/wallets | wallets:write | Додати гаманець (пул адрес) |
GET/v1/webhook-endpoints | webhooks:read | Список адрес вебхуків |
POST/v1/webhook-endpoints | webhooks:write | Додати адресу вебхука (секрет повертається один раз) |
POST/v1/webhook-endpoints/{id}/test | webhooks:write | Надіслати тестову подію webhook.test |
GET/v1/reports/transactions | reports:read | Звіт за період from–to: JSON до 5000 рядків або повний CSV (format=csv) |
Схеми запитів і відповідей описано в openapi.yaml.
Вебхуки
Кожна подія надходить на вашу адресу як POST із JSON-тілом:
{
"id": "evt_123",
"type": "payment.paid",
"created_at": "2026-10-19T12:00:00+00:00",
"livemode": false,
"data": {
"payment_id": "pay_abc123", "store_id": "store_123", "external_order_id": "10482",
"status": "PAID", "amount": "25", "currency": "USD", "expected_amount": "25",
"paid_amount": "25", "paid_asset": "USDT", "network": "TRON",
"txid": "0f3c...", "txids": ["0f3c..."], "paid_at": "2026-10-19T12:00:00+00:00"
}
}
Типи подій
| Подія | Коли надсилається |
|---|---|
payment.detected | переказ знайдено в мережі |
payment.confirming | набираються підтвердження |
payment.paid | оплата повна й фінальна |
payment.overpaid | переплата, переказ фінальний |
payment.partially_paid | отримано частину суми |
payment.underpaid | вікно закрилося з недоплатою |
payment.expired | вікно закрилося без оплати |
payment.review_required | потрібна ручна перевірка |
payment.ambiguous | переказ підходить кільком платежам |
payment.failed | остаточна помилка |
payment.refunded | зафіксовано повернення |
webhook.test | тестова подія з кабінету або API |
Заголовки та підпис
Заголовки: X-Gateway-Event-Id, X-Gateway-Event-Type, X-Gateway-Delivery-Attempt і X-Gateway-Signature: t=<unix>,v1=<hex>, де v1 = HMAC_SHA256(secret, "<t>.<сире тіло>").
Що має робити ваш обробник:
- Перевіряти підпис над сирим тілом (до розбору JSON), порівнюючи значення за сталий час.
- Відхиляти мітки часу, старші або новіші за 300 секунд.
- Пропускати дублікати за
idподії, бо та сама подія може прийти кілька разів. Видача замовлення має бути ідемпотентною. - Звіряти
livemode,external_order_id,amountіcurrencyз вашим замовленням. - Відповідати
2xxпротягом 10 секунд, а повільну роботу виконувати у фоні.
PHP
<?php function verifyGatewaySignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool { $parts = []; foreach (explode(',', $header) as $kv) { [$k, $v] = array_pad(explode('=', trim($kv), 2), 2, ''); $parts[$k] = $v; } if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) { return false; } if (abs(time() - (int) $parts['t']) > $tolerance) { return false; // replayed or clock skew } $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret); return hash_equals($expected, $parts['v1']); } $raw = file_get_contents('php://input'); if (!verifyGatewaySignature($raw, $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '', getenv('GATEWAY_WEBHOOK_SECRET'))) { http_response_code(400); exit; } $event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); // 1) skip if $event['id'] was already processed 2) handle $event['type'] 3) respond 200 http_response_code(200);
Node.js (Express)
import crypto from 'node:crypto'; import express from 'express'; function verifyGatewaySignature(rawBody, header, secret, tolerance = 300) { const parts = Object.fromEntries( String(header || '').split(',').map((kv) => kv.trim().split('=', 2)), ); if (!/^\d+$/.test(parts.t || '') || !/^[0-9a-f]{64}$/.test(parts.v1 || '')) return false; if (Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) > tolerance) return false; const expected = crypto .createHmac('sha256', secret) .update(`${parts.t}.`) .update(rawBody) // Buffer with the exact bytes received .digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex')); } const app = express(); // Raw body for this route: re-serialized JSON would not match the signature. app.post('/webhooks/gateway', express.raw({ type: 'application/json' }), (req, res) => { if (!verifyGatewaySignature(req.body, req.get('X-Gateway-Signature'), process.env.GATEWAY_WEBHOOK_SECRET)) { return res.sendStatus(400); } const event = JSON.parse(req.body.toString('utf8')); // 1) skip if event.id was already processed 2) enqueue handling of event.type 3) acknowledge res.sendStatus(200); });
Повтори
Якщо доставка не вдалася, ми повторюємо її через 1 хв, 5 хв, 15 хв, 1 год, 3 год, 6 год, 12 год і 24 год, після чого подія потрапляє в чергу недоставлених. Звідти її можна повторити з кабінету. Події можуть надходити не по порядку, тому орієнтуйтеся на status або, якщо сумніваєтеся, запитуйте GET /v1/payments/{id}.
Адреси вебхуків мають бути публічними HTTPS-адресами: внутрішні та приватні IP блокуються.
Помилки та ліміти
Усі помилки мають однаковий формат. Поле fields присутнє лише для помилок валідації.
{
"error": {
"code": "validation_error",
"message": "The amount field is required.",
"fields": { "amount": ["The amount field is required."] }
}
}
| HTTP | Код | Причина |
|---|---|---|
| 400 | idempotency_key_required, invalid_idempotency_key | Немає ключа ідемпотентності або він має неприпустимий формат |
| 401 | missing_or_malformed_api_key, invalid_api_key | Ключ не передано, або він недійсний чи відкликаний |
| 403 | insufficient_scope | Ключу бракує потрібного права |
| 403 | organization_suspended | Організацію призупинено |
| 404 | not_found | Об’єкт не знайдено або він належить іншій організації |
| 409 | request_in_progress, idempotency_conflict | Той самий запит ще обробляється |
| 409 | invalid_transition | Дія неможлива в поточному статусі платежу |
| 422 | validation_error | Неправильні поля, подробиці в fields |
| 422 | idempotency_key_reused | Ключ уже використано з іншим тілом |
| 429 | rate_limited | Перевищено ліміт запитів |
Ліміти рахуються окремо для кожного ключа та IP-адреси. Відповідь 429 містить заголовок Retry-After. Помилки 5xx і мережеві збої можна безпечно повторювати з тим самим Idempotency-Key.
Тестування
- Тестовий режим. Ключі
sk_test_створюють платежі зlivemode: false, повністю відокремлені від бойових даних. - Симуляція переказу. Відкрийте тестовий платіж у кабінеті (Платежі → деталі) і натисніть «Симулювати переказ». Платіж пройде шлях
DETECTED→PAID, а ваш сервер отримає справжні підписані вебхуки без жодного блокчейна. Надішліть меншу чи більшу суму, щоб перевіритиPARTIALLY_PAIDіOVERPAID. - Тестова подія.
POST /v1/webhook-endpoints/{id}/testабо кнопка в кабінеті надсилаєwebhook.test, щоб перевірити доступність адреси та перевірку підпису. - Локальна розробка. Щоб отримувати вебхуки на своєму комп’ютері, скористайтеся тунелем (ngrok, cloudflared).
Запуск у production
- Додайте свої справжні гаманці лише для перегляду та перевірте адреси в кабінеті.
- Створіть ключ
sk_live_з мінімально потрібними правами та окремий бойовий вебхук із новим секретом. - Переконайтеся, що обробник перевіряє підпис і
livemode: trueі що він ідемпотентний. - Проведіть один невеликий реальний платіж від початку до кінця, перш ніж показувати спосіб оплати клієнтам.
- Увімкніть двофакторну автентифікацію для всіх учасників команди.
SDK та плагіни
PHP SDK
Пакет cryptogateway/sdk-php для PHP 8.1+ потребує лише ext-curl і ext-json, тож його можна вбудувати в будь-який проєкт.
use CryptoGateway\Client; $client = new Client('sk_test_...', 'https://pay.mevcore.com'); $payment = $client->payments()->create([ 'amount' => '25.00', 'currency' => 'USD', 'external_order_id' => '10482', ], idempotencyKey: 'order-10482-1'); header('Location: ' . $payment['checkout_url']);
TypeScript / Node.js SDK
Пакет @cryptogateway/sdk для Node 18+ працює без залежностей і має типи для всіх об’єктів.
import { CryptoGatewayClient } from '@cryptogateway/sdk'; const client = new CryptoGatewayClient(process.env.GATEWAY_KEY!, { baseUrl: 'https://pay.mevcore.com' }); const payment = await client.payments.create( { external_order_id: '10482', amount: '25.00', currency: 'USD' }, 'order-10482-1', // idempotency key ); console.log(payment.checkout_url);
WHMCS
- Скопіюйте папку
modules/модуля в корінь WHMCS, об’єднавши її з наявною. - У System Settings → Payment Gateways активуйте Crypto Payment Gateway (USDT / TON).
- Укажіть адресу API
https://pay.mevcore.com(без/v1), ключ із правамиpayments:readіpayments:write, ID магазину (необов’язковий для ключа, прив’язаного до магазину) та секрет вебхука. Галочка «Test mode» має відповідати типу ключа. - У кабінеті MEVPAY додайте вебхук на
https://<ваш-whmcs>/modules/gateways/callback/cryptogateway.php.
Paymenter
- Скопіюйте
extensions/Gateways/CryptoGateway/у Paymenter. Composer не потрібен, бо SDK уже вкладено. - Виконайте
php artisan optimize:clear, якщо у вас кешовано маршрути або конфігурацію. - У Admin → Gateways додайте шлюз CryptoGateway і заповніть ті самі параметри, що й для WHMCS.
- Додайте вебхук на
https://<ваш-paymenter>/extensions/gateways/cryptogateway/webhookі надішліть тестову подію.
Тарифи й комісії
| Тариф | Абонплата | Комісія з оплаченого обороту |
|---|---|---|
| Free | $0 | 1% |
| Pro | $15 | 0.5% |
| Business | $49 | 0.2% |
| Enterprise | Індивідуально | |
Комісія не утримується з переказів: клієнт платить вам повну суму. Нарахування накопичуються в кабінеті (Білінг), і раз на місяць ми виставляємо рахунок разом з абонплатою. Порівняти тарифи й порахувати вартість →