Документація

API MEVPAY

MEVPAY — некастодіальний шлюз криптоплатежів. Ви створюєте платіж через REST API, клієнт оплачує на нашій сторінці, а кошти надходять одразу на ваш гаманець (USDT TRC20, USDT TON або TON). Про результат ми повідомляємо ваш сервер підписаним вебхуком.

Базова URL-адреса: https://pay.mevcore.com/v1
  • Формат: JSON у запитах і відповідях, кодування UTF-8.
  • Автентифікація: секретний ключ у заголовку Authorization: Bearer.
  • Суми завжди передаються десятковими рядками ("25.00"), ніколи числами з плаваючою комою. Значення amount_atomic — ціле число в найменших одиницях активу.
  • Повна специфікація: OpenAPI 3.1. Її можна відкрити в Swagger UI, Redoc або Postman.

Швидкий старт

  1. Зареєструйтеся, створіть організацію та магазин.
  2. Додайте гаманець лише для перегляду, тобто публічні адреси TRON або TON. Сід-фрази й приватні ключі не потрібні та не приймаються.
  3. Увімкніть хоча б один спосіб оплати: USDT TRC20, USDT TON або TON.
  4. У розділі Розробникам → Ключі API створіть тестовий ключ sk_test_… з правами payments:write і payments:read. Секрет показується лише один раз.
  5. У розділі Розробникам → Вебхуки додайте 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:readGET /payments, GET /payments/{id}, GET /payment-methods
payments:writePOST /payments, POST /payments/{id}/expire
transactions:readGET /transactions
wallets:readGET /wallets
wallets:writePOST /wallets
webhooks:readGET /webhook-endpoints
webhooks:writePOST /webhook-endpoints, POST /webhook-endpoints/{id}/test
reports:readGET /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 *stringUSD, EUR, UAH, USDT або TON. Фіатні суми конвертуються за курсом на момент виставлення рахунку.
store_idstringID магазину. Необов’язковий, якщо ключ прив’язаний до магазину.
external_order_idstringВаш номер замовлення, повертається у вебхуках.
descriptionstringОпис для клієнта, до 500 символів.
success_url, cancel_urluriКуди повернути клієнта після оплати або скасування.
customer_emailemailEmail клієнта.
assetstringUSDT_TRC20, USDT_TON або TON. Якщо не вказано, клієнт обирає актив на сторінці оплати.
localestringМова сторінки оплати: uk, en, ru або de.
ttl_minutesintegerВікно оплати від 5 до 1440 хвилин, за замовчуванням 30.
metadataobjectДо 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/paymentspayments:writeСтворити платіж
GET/v1/paymentspayments:readСписок платежів
GET/v1/payments/{id}payments:readПлатіж із транзакціями та подіями
POST/v1/payments/{id}/expirepayments:writeЗакрити вікно оплати
GET/v1/payment-methodspayments:readУвімкнені активи та мережі магазину
GET/v1/transactionstransactions:readВхідні перекази на ваші адреси
GET/v1/walletswallets:readГаманці лише для перегляду
POST/v1/walletswallets:writeДодати гаманець (пул адрес)
GET/v1/webhook-endpointswebhooks:readСписок адрес вебхуків
POST/v1/webhook-endpointswebhooks:writeДодати адресу вебхука (секрет повертається один раз)
POST/v1/webhook-endpoints/{id}/testwebhooks:writeНадіслати тестову подію webhook.test
GET/v1/reports/transactionsreports: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>.<сире тіло>").

Що має робити ваш обробник:

  1. Перевіряти підпис над сирим тілом (до розбору JSON), порівнюючи значення за сталий час.
  2. Відхиляти мітки часу, старші або новіші за 300 секунд.
  3. Пропускати дублікати за id події, бо та сама подія може прийти кілька разів. Видача замовлення має бути ідемпотентною.
  4. Звіряти livemode, external_order_id, amount і currency з вашим замовленням.
  5. Відповідати 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КодПричина
400idempotency_key_required, invalid_idempotency_keyНемає ключа ідемпотентності або він має неприпустимий формат
401missing_or_malformed_api_key, invalid_api_keyКлюч не передано, або він недійсний чи відкликаний
403insufficient_scopeКлючу бракує потрібного права
403organization_suspendedОрганізацію призупинено
404not_foundОб’єкт не знайдено або він належить іншій організації
409request_in_progress, idempotency_conflictТой самий запит ще обробляється
409invalid_transitionДія неможлива в поточному статусі платежу
422validation_errorНеправильні поля, подробиці в fields
422idempotency_key_reusedКлюч уже використано з іншим тілом
429rate_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

  1. Додайте свої справжні гаманці лише для перегляду та перевірте адреси в кабінеті.
  2. Створіть ключ sk_live_ з мінімально потрібними правами та окремий бойовий вебхук із новим секретом.
  3. Переконайтеся, що обробник перевіряє підпис і livemode: true і що він ідемпотентний.
  4. Проведіть один невеликий реальний платіж від початку до кінця, перш ніж показувати спосіб оплати клієнтам.
  5. Увімкніть двофакторну автентифікацію для всіх учасників команди.

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

  1. Скопіюйте папку modules/ модуля в корінь WHMCS, об’єднавши її з наявною.
  2. У System Settings → Payment Gateways активуйте Crypto Payment Gateway (USDT / TON).
  3. Укажіть адресу API https://pay.mevcore.com (без /v1), ключ із правами payments:read і payments:write, ID магазину (необов’язковий для ключа, прив’язаного до магазину) та секрет вебхука. Галочка «Test mode» має відповідати типу ключа.
  4. У кабінеті MEVPAY додайте вебхук на https://<ваш-whmcs>/modules/gateways/callback/cryptogateway.php.

Paymenter

  1. Скопіюйте extensions/Gateways/CryptoGateway/ у Paymenter. Composer не потрібен, бо SDK уже вкладено.
  2. Виконайте php artisan optimize:clear, якщо у вас кешовано маршрути або конфігурацію.
  3. У Admin → Gateways додайте шлюз CryptoGateway і заповніть ті самі параметри, що й для WHMCS.
  4. Додайте вебхук на https://<ваш-paymenter>/extensions/gateways/cryptogateway/webhook і надішліть тестову подію.

Тарифи й комісії

ТарифАбонплатаКомісія з оплаченого обороту
Free $0 1%
Pro $15 0.5%
Business $49 0.2%
Enterprise Індивідуально

Комісія не утримується з переказів: клієнт платить вам повну суму. Нарахування накопичуються в кабінеті (Білінг), і раз на місяць ми виставляємо рахунок разом з абонплатою. Порівняти тарифи й порахувати вартість →