API приймання та виплат
Приймайте оплати переказом на картку (Pay-in) і робіть виплати на картки клієнтів (Pay-out). Розрахунки з мерчантом — у USDT за курсом, зафіксованим під час створення заявки.
Ви створюєте заявку на суму — у відповіді одразу картка для оплати.
Клієнт переказує рівно amount на картку або сплачує на нашій сторінці оплати.
Виконавець бачить надходження і підтверджує заявку.
Ви отримуєте підписаний колбек зі status_id: 5 і зарахуванням на баланс.
Швидкий старт
- Увійдіть у кабінет мерчанта на merchant.emipay.net → «Інтеграція API» → створіть ключ із правами
readіcreate. Для перших запитів виберіть позначку «Тестовий». - У тому ж розділі вкажіть
callback_urlі збережітьwebhook_secret— він показується один раз. - Перевірте зв’язок:
POST /api/v1/health_check. - Створіть заявку
POST /api/v4/order/create, покажіть клієнту картку або перенаправте його наredirect_url. - Обробляйте колбеки: перевіряйте підпис
X-Signatureі зараховуйте оплату лише наstatus_id: 5.
Доступи й базова адреса
| Що | Де взяти | Для чого |
|---|---|---|
| Базова адреса | https://api.emipay.net | Усі методи API |
| API-токен | Кабінет → «Інтеграція API» → «Ключі» | Заголовок x-api-token. Формат emi_ + 64 hex-символи. Показується один раз. |
webhook_secret | Кабінет → «Інтеграція API» → «Колбеки» | Перевірка підпису колбеків. Можна перевипустити. |
callback_url | Ви задаєте в кабінеті (або окремо для кожного магазину) | Адреса HTTPS, куди надходять колбеки |
Ключ має строк дії (1–365 днів) і необов’язковий перелік дозволених IP. До 10 активних ключів на акаунт. Ключ можна прив’язати до магазину — тоді заявки отримують цей магазин, а колбеки йдуть на адресу магазину.
API призначене лише для запитів сервер-сервер. Не вставляйте токен у код сторінки чи мобільного застосунку.
Авторизація
Передавайте токен у заголовку x-api-token. Підтримується також Authorization: Bearer emi_….
Невірний, прострочений чи відкликаний токен, токен без потрібного права, запит з недозволеного IP або вимкнений мерчант — відповідь 401 з error_code: 101.
/api/v1/health_checkПеревірка доступності, токен не потрібен.
curl -X POST https://api.emipay.net/api/v1/health_check
const r = await fetch('https://api.emipay.net/api/v1/health_check', { method: 'POST' });
console.log(await r.json()); // { status: 'ok' }
import requests
print(requests.post("https://api.emipay.net/api/v1/health_check").json()) # {'status': 'ok'}{
"status": "error",
"error_code": 101,
"error": "Merchant not found or inactive"
}Формат запитів і відповідей
- Усі методи —
POSTз тілом JSON (Content-Type: application/json), крім завантаження квитанції виплати (GET). - Успіх: HTTP 200 і
{"status":"ok", …}. Помилка: HTTP 4xx і{"status":"error","error_code":N,"error":"…"}. - Суми в запитах — числа в одиницях валюти, до 2 знаків після коми:
1500,1500.5. - У відповіді на створення Pay-in суми — числа; у статусах, списках і колбеках — рядки з 2 знаками (
"1500.00"). - Час — ISO 8601 в UTC (
2026-10-21T10:15:00.000Z). - Захист від дублів — унікальний
order_id: повтор повертає104, друга заявка не створюється. Тому запит створення можна безпечно повторювати після мережевої помилки. - Ліміт — 600 запитів на хвилину на мерчанта (за потреби більше — напишіть нам). Залишок — у заголовку
X-RateLimit-Remaining, перевищення —429. - Кожна відповідь має заголовок
X-Request-Id— вкажіть його, звертаючись у підтримку.
Тип реквізиту cred_type
| Значення | Реквізит |
|---|---|
1 | Номер картки (16 цифр) — UAH, KZT |
3 | IBAN (TR + 24 цифри) — TRY |
Тарифи й розрахунок сум
Тариф задає адміністрація, мерчант бачить його в кабінеті: «Інтеграція API → Тариф».
- Pay-in — фіксована ставка або ступінчаста сітка за сумою заявки (межі включно; сума поза сіткою — за базовою ставкою).
- Pay-out — окрема фіксована ставка.
- Ставка й курс фіксуються в момент створення заявки й далі не змінюються.
| Поле | Як рахується | Приклад (1500 ₴, 1%, курс 45) |
|---|---|---|
commission | amount × ставка | 15.00 |
merch_amount | amount − commission | 1485.00 |
usdt_amount | amount ÷ курс | 33.33 |
merch_usdt_amount | Зарахування на баланс в USDT: merch_amount ÷ merch_usdt_rate | 33.00 |
final_amount (Pay-out) | amount + commission | — |
final_usdt_amount (Pay-out) | Списання з балансу в USDT | — |
Суми в USDT округлюються до центів (0,005 — угору) окремо для кожної заявки.
Створити заявку (v4)
/api/v4/order/createСтворює заявку і одразу призначає реквізит виконавця. У відповіді — картка, на яку клієнт має переказати рівно amount, і redirect_url на сторінку оплати.
Реквізит підбирається з урахуванням лімітів, банку клієнта (client_bank) і рейтингу виконавців. На одній картці ніколи не буває двох відкритих заявок з однаковою сумою, тому за сумою надходження однозначно визначає заявку. Якщо вільної картки немає — помилка 106, заявка не створюється.
Параметри
| Поле | Тип | Опис | |
|---|---|---|---|
order_id | string | обов’язково | Ваш унікальний ID заявки, до 100 символів. Унікальний для всіх ваших Pay-in і Pay-out. |
amount | number | обов’язково | Сума в валюті заявки, до 2 знаків. Має бути в межах вашого мінімуму й максимуму. |
rate | number | обов’язково | Ваш курс для цієї заявки — скільки UAH за 1 USDT, до 2 знаків (наприклад 41.85). Курс задаєте ви в кожній заявці, своїх курсів платформа не має. Замість rate можна передати amount_usdt. |
amount_usdt | number | замість rate | Скільки USDT ви отримаєте за заявку; курс рахується як amount / amount_usdt. |
currency | string | необов’язково | UAH, TRY або KZT — з дозволених для акаунта. Типово — валюта акаунта. |
customer_id | string | необов’язково | ID клієнта у вашій системі. Використовується антифродом і лімітами на клієнта — радимо передавати завжди. |
client_bank | string | необов’язково | Банк клієнта (наприклад mono, privat). Якщо є картка того самого банку, перевага їй — переказ доходить швидше. |
return_url | string | необов’язково | Куди повернути клієнта зі сторінки оплати (http/https). |
cred_type | number | необов’язково | Лише 1. Тип реквізиту у відповіді визначається валютою. |
method | string | необов’язково | transgran — для транскордонного акаунта, див. Валюти. |
randomize_enable | number | необов’язково | 1 — дозволити зсув суми, якщо на всіх картках уже є відкрита заявка з такою сумою. |
randomize_stepping | number | необов’язково | Крок зсуву. Типово 5. |
randomize_max_steps | number | необов’язково | Скільки кроків пробувати, 1–50. Типово 10. |
randomize_direction | string | необов’язково | increase (типово) або decrease. |
randomize_max_final_amount | number | необов’язково | Межа, вище якої сума не зсувається. |
Якщо сума зсунулася, списати з клієнта треба нове значення amount. Запитана сума повертається в amount_initial.
curl -X POST https://api.emipay.net/api/v4/order/create \
-H "Content-Type: application/json" \
-H "x-api-token: $EMIPAY_TOKEN" \
-d '{
"order_id": "ORDER-10002",
"amount": 1500,
"rate": 41.85,
"customer_id": "cust-77",
"client_bank": "privat",
"return_url": "https://shop.example/thanks",
"randomize_enable": 1,
"randomize_stepping": 5,
"randomize_max_steps": 10
}'
const r = await fetch('https://api.emipay.net/api/v4/order/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-token': process.env.EMIPAY_TOKEN },
body: JSON.stringify({
order_id: 'ORDER-10002',
amount: 1500,
rate: 41.85,
customer_id: 'cust-77',
client_bank: 'privat',
return_url: 'https://shop.example/thanks',
randomize_enable: 1,
}),
});
const order = await r.json();
if (order.status !== 'ok') throw new Error(`${order.error_code}: ${order.error}`);
// показати order.cred і order.amount або перенаправити на order.redirect_url
import os, requests
r = requests.post(
"https://api.emipay.net/api/v4/order/create",
headers={"x-api-token": os.environ["EMIPAY_TOKEN"]},
json={
"order_id": "ORDER-10002",
"amount": 1500,
"rate": 41.85,
"rate": 41.85,
"customer_id": "cust-77",
"client_bank": "privat",
"return_url": "https://shop.example/thanks",
"randomize_enable": 1,
},
timeout=30,
)
order = r.json()
if order["status"] != "ok":
raise RuntimeError(f'{order["error_code"]}: {order["error"]}'){
"status": "ok",
"order_id": "ORDER-10002",
"transaction_id": 100002,
"amount": 1505,
"amount_initial": 1500,
"usdt_amount": 33.44,
"usdt_rate": 45,
"status_id": 2,
"bank_name": "ПриватБанк",
"receiver_name": "IVAN PETRENKO",
"cred": "4149499912345673",
"cred_formatted": "4149 49** **** 5673",
"cred_type": 1,
"commission": "15.05",
"merch_amount": "1489.95",
"merch_usdt_amount": "33.11",
"merch_usdt_rate": "45.00",
"redirect_url": "https://pay.emipay.net/checkout/pk_4db1853a36d652dbc53765c294f4ef78669e17f9",
"deadline": "2026-10-21T10:45:00.000Z",
"version": 2
}Поля відповіді
| Поле | Опис |
|---|---|
transaction_id | Номер заявки в EmiPay (число) |
amount / amount_initial | Сума до оплати / запитана сума |
cred, cred_formatted, cred_type | Реквізит для оплати: повний, маскований і тип |
bank_name, receiver_name | Банк і отримувач, яких варто показати клієнту |
status_id | 2 — реквізит видано, див. коди статусів |
deadline | До коли клієнт має оплатити (типово 30 хв) |
redirect_url | Готова сторінка оплати |
version | Версія стану; зростає на кожній зміні |
Створити заявку (v1)
/api/v1/order/createТе саме, що v4, без автоматичного зсуву суми: параметри randomize_* ігноруються. Якщо на всіх картках уже є відкрита заявка з такою сумою — 106.
Статус заявки
/api/v1/order/statusПередайте order_id або transaction_id. Усі значення в order_data — рядки.
curl -X POST https://api.emipay.net/api/v1/order/status \
-H "Content-Type: application/json" \
-H "x-api-token: $EMIPAY_TOKEN" \
-d '{"order_id": "ORDER-10001"}'
const r = await fetch('https://api.emipay.net/api/v1/order/status', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-token': process.env.EMIPAY_TOKEN },
body: JSON.stringify({ order_id: 'ORDER-10001' }),
});
const { order_data } = await r.json();
r = requests.post("https://api.emipay.net/api/v1/order/status",
headers={"x-api-token": TOKEN}, json={"order_id": "ORDER-10001"}, timeout=30)
order = r.json()["order_data"]{
"status": "ok",
"order_data": {
"order_id": "ORDER-10001",
"transaction_id": "100001",
"amount": "1500.00",
"amount_initial": "1500.00",
"commission": "15.00",
"merch_amount": "1485.00",
"usdt_amount": "33.33",
"usdt_rate": "45",
"merch_usdt_amount": "33.00",
"merch_usdt_rate": "45.00",
"currency": "UAH",
"cred": "4149499912345673",
"cred_formatted": "4149 49** **** 5673",
"cred_type": "1",
"receiver_name": "IVAN PETRENKO",
"receiver_bank": "ПриватБанк",
"status_id": "2",
"deadline": "2026-10-21T10:45:00.000Z",
"customer_id": "cust-77",
"version": 2,
"created": "2026-10-21T10:15:00.000Z",
"updated": "2026-10-21T10:15:00.000Z"
}
}Для відхиленої апеляції додаються appeal_reject_reason і appeal_reject_reason_text. Заявка не знайдена — 404.
Список заявок
/api/v1/order/list| Поле | Тип | Опис | |
|---|---|---|---|
status_id | number | необов’язково | Фільтр за статусом |
limit | number | необов’язково | 1–1000, типово 500 |
offset | number | необов’язково | Зсув для сторінок |
Відповідь: {"status":"ok","orders":[…],"total":N}. Кожен елемент orders має той самий формат, що order_data. Нові заявки — першими.
Сторінка оплати
redirect_url веде на готову сторінку оплати з брендом вашого магазину. На ній: сума, картка з кнопкою копіювання, банк і отримувач, таймер до deadline, кнопка «Я оплатив» і повернення на ваш return_url.
- Кнопка «Я оплатив» переводить заявку в
status_id: 3. Це лише повідомлення клієнта, а не підтвердження оплати. - Для магазину можна задати назву, логотип і колір сторінки (кабінет → «Магазини»).
- Сторінка адаптована під телефон і світлу/темну тему.
- Мова: українська, російська або англійська. Сторінка бере мову браузера клієнта, а клієнт може перемкнути її сам. Щоб задати мову, додайте до посилання
?lang=uk,?lang=ruабо?lang=en.
Ви можете й не використовувати сторінку — достатньо показати клієнту amount, cred, bank_name і receiver_name у своєму інтерфейсі.
Валюти й трансгран
Валюту акаунта й додаткові валюти вмикає адміністрація. Передайте currency з дозволених, інакше — 111.
| Валюта | Реквізит у відповіді | Виплати |
|---|---|---|
UAH | Картка, cred_type: 1 | Картка 16 цифр |
KZT | Картка, cred_type: 1 | Картка 16 цифр |
TRY | IBAN, cred_type: 3 | IBAN TR…, cred_type: 3, payout_type: "IBAN" |
Транскордонне приймання
Для транскордонного акаунта передайте "method": "transgran". Відповіді й колбеки отримують додаткові поля:
| Поле | Опис |
|---|---|
method | transgran |
usdt_amount | Повна сума заявки в USDT |
merch_usdt_amount | Зарахування мерчанту в USDT після комісії |
usdt_rate | Курс валюти до USDT |
Якщо трансгран для акаунта не ввімкнено — 108.
Створити виплату
/api/v2/payout/createРезервує final_usdt_amount на вашому балансі й ставить виплату в чергу виконавців. Виконавець бере виплату, переказує кошти й завантажує квитанцію — це одразу завершує виплату (COMPLETED).
| Поле | Тип | Опис | |
|---|---|---|---|
order_id | string | обов’язково | Ваш унікальний ID, до 100 символів |
amount | number | обов’язково | Сума, яку отримає одержувач |
cred | string | обов’язково | Картка (16 цифр, перевірка Луна) або IBAN TR + 24 цифри. Пробіли й дефіси допускаються. |
cred_type | number | необов’язково | 1 — картка, 3 — IBAN (для TRY) |
currency | string | необов’язково | Типово — валюта акаунта |
recipient_name | string | необов’язково | ПІБ одержувача, до 120 символів |
receiver_bank | string | необов’язково | Банк одержувача, до 80 символів |
pay_before | string | необов’язково | Ваш SLA в ISO 8601 — такі виплати виконавці бачать як термінові. Якщо до цього часу виплату ніхто не взяв, вона автоматично скасовується (CANCELLED), резерв повертається на баланс |
curl -X POST https://api.emipay.net/api/v2/payout/create \
-H "Content-Type: application/json" \
-H "x-api-token: $EMIPAY_TOKEN" \
-d '{
"order_id": "PAYOUT-501",
"amount": 2000,
"cred": "5375414112345672",
"cred_type": 1,
"recipient_name": "OLENA KOVAL",
"receiver_bank": "monobank",
"pay_before": "2026-10-21T11:15:00Z"
}'
const r = await fetch('https://api.emipay.net/api/v2/payout/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-token': process.env.EMIPAY_TOKEN },
body: JSON.stringify({
order_id: 'PAYOUT-501', amount: 2000, cred: '5375414112345672', cred_type: 1,
recipient_name: 'OLENA KOVAL', receiver_bank: 'monobank',
}),
});
const payout = await r.json();
r = requests.post("https://api.emipay.net/api/v2/payout/create",
headers={"x-api-token": TOKEN},
json={"order_id": "PAYOUT-501", "amount": 2000, "cred": "5375414112345672", "cred_type": 1,
"recipient_name": "OLENA KOVAL", "receiver_bank": "monobank"},
timeout=30)
payout = r.json(){
"status": "ok",
"payout_id": "100003",
"payout_type": "CARD",
"amount": "2000.00",
"commission": "20.00",
"final_amount": "2020.00",
"final_usdt_amount": "44.89",
"merch_usdt_rate": "45.00",
"cred_formatted": "5375 41** **** 5672",
"currency": { "id": "1", "code": "UAH", "title": "UAH" },
"receiver_country": "UA",
"receiver_bank": "monobank",
"payout_status": "NEW",
"version": 1,
"pay_before": "2026-10-21T11:15:00.000Z"
}Не вистачає балансу або немає виконавця для такої суми — 109, нічого не списується. Той самий order_id можна надіслати повторно.
Правила завершення
- Квитанція виконавця одразу завершує виплату. Після цього скасувати її неможливо.
- Якщо одержувач не отримав кошти — відкрийте спір по виплаті в кабінеті. Кошти виконавця блокуються до рішення адміністрації.
- Банківське повернення виконаної виплати оформлює адміністрація: статус стає
CANCELLEDз новоюversion, сума повертається на баланс. - Виплата, яку ніхто не виконав за 24 години, —
FAILED, резерв повертається.
Статус і список виплат
/api/v2/payout/statusПередайте order_id або payout_id. Не знайдено — 110.
{
"status": "ok",
"payout_id": "100003",
"order_id": "PAYOUT-501",
"payout_type": "CARD",
"type": "CARD",
"amount": "2000.00",
"commission": "20.00",
"final_amount": "2020.00",
"final_usdt_amount": "44.89",
"merch_usdt_rate": "45.00",
"version": 2,
"payout_status": "COMPLETED",
"cred_formatted": "5375 41** **** 5672",
"currency": { "id": "1", "code": "UAH", "title": "UAH" },
"receiver_country": "UA",
"country_iso": "UA",
"recipient_name": "OLENA KOVAL",
"receiver_bank": "monobank",
"pay_before": "2026-10-21T11:15:00.000Z",
"receipts": [
{
"id": "3f2b8c1d-6a0e-4f57-9b2a-1c7d5e9a4b10",
"filename": "receipt.pdf",
"content_type": "application/pdf",
"byte_size": 48213,
"download_url": "https://api.emipay.net/api/v1/payout/receipt?file_id=3f2b8c1d-6a0e-4f57-9b2a-1c7d5e9a4b10"
}
],
"created": "2026-10-21T10:15:00.000Z",
"updated": "2026-10-21T10:22:41.000Z"
}/api/v1/payouts/listПараметри: status (NEW, PROCESSING, COMPLETED, CANCELLED, FAILED), limit (1–1000), offset. Відповідь: {"status":"ok","payouts":[…],"total":N} з квитанціями в кожному елементі.
Квитанція виплати
/api/v1/payout/receipt?file_id=…Повертає файл квитанції, завантажений виконавцем (з тим самим заголовком x-api-token). Адреса — у receipts[].download_url. Квитанція чужої виплати — 404.
curl -H "x-api-token: $EMIPAY_TOKEN" -o receipt.pdf \ "https://api.emipay.net/api/v1/payout/receipt?file_id=FILE_ID"
Відкрити апеляцію
/api/v1/appeal/createЯкщо клієнт оплатив, а заявка не підтверджена, скасована або прострочена — відкрийте апеляцію з чеком. Заявка переходить у status_id: 7, кошти виконавця резервуються під апеляцію. Підтримка перевіряє чек, рішення ухвалює адміністрація.
| Поле | Тип | Опис | |
|---|---|---|---|
order_id | string | обов’язково | Ваш ID заявки Pay-in |
comments | string | обов’язково | Опис ситуації, до 2000 символів |
receipts | array | необов’язково | До 10 файлів: [{"filename":"check.jpg","base64":"…"}] |
file_base64, filename | string | необов’язково | Один файл — альтернатива receipts |
Файли: JPEG, PNG, WebP, PDF, HEIC до 10 МБ, формат перевіряється за вмістом. Невідповідний файл не зупиняє апеляцію — причина повертається в receipts_skipped_reason.
curl -X POST https://api.emipay.net/api/v1/appeal/create \
-H "Content-Type: application/json" \
-H "x-api-token: $EMIPAY_TOKEN" \
-d "{\"order_id\":\"ORDER-10002\",\"comments\":\"Клієнт оплатив, чек додано\",
\"receipts\":[{\"filename\":\"check.png\",\"base64\":\"$(base64 -w0 check.png)\"}]}"
import { readFileSync } from 'node:fs';
const r = await fetch('https://api.emipay.net/api/v1/appeal/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-token': process.env.EMIPAY_TOKEN },
body: JSON.stringify({
order_id: 'ORDER-10002',
comments: 'Клієнт оплатив, чек додано',
receipts: [{ filename: 'check.png', base64: readFileSync('check.png').toString('base64') }],
}),
});
import base64
with open("check.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
r = requests.post("https://api.emipay.net/api/v1/appeal/create",
headers={"x-api-token": TOKEN},
json={"order_id": "ORDER-10002", "comments": "Клієнт оплатив, чек додано",
"receipts": [{"filename": "check.png", "base64": b64}]},
timeout=60){
"status": "ok",
"appeal_id": 100301,
"upload_url": "https://api.emipay.net/appeal/upload?hash=addf3caeb7f1090d81465eb591146c2203c09a0b601bf119",
"receipts_attached": 1,
"receipts_skipped_reason": null
}Помилки: 102 немає order_id, 103 немає comments, 104 заявку не знайдено, 105 апеляція вже відкрита, 106 заявка вже успішна.
Рішення
- Схвалено (
5) — заявка завершується, ви отримуєте звичайний колбекstatus_id: 5. Якщо клієнт сплатив іншу суму, адміністрація вказує фактичну, комісія — за тією самою ставкою. - Відхилено (
3) — заявка скасовується (status_id: 4) з причиноюappeal_reject_reason:no_payment,fake_receipt,wrong_requisiteабоamount_mismatch.
Статус і список апеляцій
/api/v1/appeal/statusПередайте appeal_id або order_id (повертається остання апеляція по заявці).
{
"status": "ok",
"appeal_data": {
"appeal_id": "100301",
"order_id": "100002",
"status": "1",
"comments": "Клієнт оплатив, чек додано",
"screenshot": "attached",
"reject_reason": null,
"reject_reason_text": null,
"order_data": {
"id": "100002",
"external_id": "ORDER-10002",
"amount": "1505.00",
"amount_initial": 1500,
"datetime": "2026-10-21 10:15:00",
"executed_datetime": null,
"cred": "4149499912345673"
}
}
}/api/v1/appeal/listНеобов’язковий фільтр status. Відповідь: {"status":"ok","appeals":[…]}, до 1000 останніх.
| Статус апеляції | Значення |
|---|---|
1 | Нова |
7 | На модерації |
5 | Схвалена |
3 | Відхилена |
Дослати чек
/appeal/upload?hash=…Адресу повертає upload_url. Токен не потрібен — доступ дає hash, тому посилання можна передати клієнту. Приймається multipart/form-data з полем file або JSON {"file_base64":"…","filename":"…"}. До 10 файлів на апеляцію, лише поки вона відкрита.
curl -X POST "https://api.emipay.net/appeal/upload?hash=HASH" -F "file=@check.jpg"
Відповідь: {"status":"ok","file_url":"attached","file_id":"…"}. Помилки: 107 формат файлу, 413 файл завеликий.
Життєвий цикл заявки
1 NEW ──▶ 2 CRED_ISSUED ──▶ 3 CUSTOMER_PAID ──▶ 5 COMPLETED
│ │
├──▶ 9 EXPIRED └──▶ 4 CANCELLED
└──▶ 7 APPEAL ──▶ 5 COMPLETED | 4 CANCELLEDNEW ──▶ PROCESSING ──▶ COMPLETED ──(банківське повернення)──▶ CANCELLED │ ├──▶ CANCELLED └──▶ FAILED (не виконано за 24 год)
Остаточні стани Pay-in — 5, 4, 9, 8; але апеляція може перевести прострочену чи скасовану заявку в 7 і далі в 5. Тому орієнтуйтеся на version, а не на припущення, що стан більше не зміниться.
Формат колбека
На кожну зміну стану EmiPay надсилає POST на ваш callback_url:
- Тіло — компактний JSON, ключі відсортовані за алфавітом на кожному рівні.
- Заголовок
X-Signature— hex(HMAC-SHA256(webhook_secret, сире тіло)). - Успіх — будь-яка відповідь 2xx протягом 10 секунд. Інакше — повтори через 1, 5, 15 і 60 хвилин (усього 5 спроб). Кожну доставку видно й можна повторити в кабінеті.
- Один колбек на кожний окремий стан. Колбек з
version≤ уже обробленої ігноруйте — так ви не «відкотите» стан через запізнілий повтор. - Зараховуйте оплату лише за
status_id: 5, і рахуйте заmerch_usdt_amountз колбека: адміністрація може виправити суму.
Pay-in
{"amount":"1500.00","amount_initial":"1500.00","commission":"15.00","cred":"4149499912345673","cred_type":"1","currency":"UAH","merch_amount":"1485.00","merch_usdt_amount":"33.00","merch_usdt_rate":"45.00","order_id":"ORDER-10001","receiver_bank":"ПриватБанк","receiver_name":"IVAN PETRENKO","status_id":5,"transaction_id":100001,"version":4}Поля: order_id, transaction_id (число), amount, amount_initial, commission, merch_amount, currency, status_id (число), cred, cred_type, receiver_name, receiver_bank, merch_usdt_amount, merch_usdt_rate, version. Для відхиленої апеляції — ще appeal_reject_reason і appeal_reject_reason_text; для трансграну — method, usdt_amount, usdt_rate.
Pay-out
{"amount":"2000.00","commission":"20.00","country_iso":"UA","cred_formatted":"5375 41** **** 5672","currency":{"code":"UAH","id":"1","title":"UAH"},"final_amount":"2020.00","final_usdt_amount":"44.89","merch_usdt_rate":"45.00","order_id":"PAYOUT-501","payout_id":"100003","payout_status":"COMPLETED","status":"ok","type":"CARD","version":2}Перевірка підпису
Рахуйте HMAC від сирих байтів тіла, як вони прийшли, — не від розпарсеного й заново серіалізованого JSON. Порівнюйте підписи функцією з постійним часом.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
app.post('/emipay/callback', express.raw({ type: '*/*' }), (req, res) => {
const expected = createHmac('sha256', process.env.EMIPAY_WEBHOOK_SECRET).update(req.body).digest();
const got = Buffer.from(String(req.get('X-Signature') || ''), 'hex');
if (got.length !== expected.length || !timingSafeEqual(got, expected)) return res.sendStatus(401);
const cb = JSON.parse(req.body.toString('utf8'));
// ігнорувати, якщо cb.version не більша за вже збережену для cb.order_id
// зарахувати оплату, якщо cb.status_id === 5
res.sendStatus(200);
});
import hmac, hashlib, json, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["EMIPAY_WEBHOOK_SECRET"].encode()
@app.post("/emipay/callback")
def callback():
raw = request.get_data()
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Signature", "")):
abort(401)
cb = json.loads(raw)
# ігнорувати, якщо cb["version"] не більша за збережену
# зарахувати, якщо cb.get("status_id") == 5
return "ok"
<?php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('EMIPAY_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$cb = json_decode($raw, true);
// ігнорувати, якщо $cb['version'] не більша за збережену
// зарахувати, якщо ($cb['status_id'] ?? null) === 5
http_response_code(200);
echo 'ok';Тестовий вектор
Секрет test_secret і тіло нижче мають дати підпис 819116bbc2fc62631b6d1e3af24ee77a0ce51897d1a7a0f96512c6a107495300.
{"amount":"50000.00","amount_initial":"50000.00","commission":"3000.00","cred":"4111111111111111","cred_type":"1","currency":"KZT","merch_amount":"47000.00","merch_usdt_amount":"93.11","method":"transgran","order_id":"KZ-20931","receiver_bank":"Demo Bank UA","receiver_name":"Ivan P.","status_id":5,"transaction_id":100001,"usdt_amount":"99.05","usdt_rate":"504.79"}Тестові ключі
Ключ із позначкою «Тестовий» працює з тими самими методами, але заявки створюються без виконавців і руху коштів: Pay-in одразу отримує тестову картку 4242…. Тестові й бойові заявки не перетинаються — тестовий ключ не бачить бойових і навпаки.
Статусами тестової заявки керуйте методом:
/api/v1/test/{paid|complete|cancel|expire}curl -X POST https://api.emipay.net/api/v1/test/complete \
-H "Content-Type: application/json" \
-H "x-api-token: $EMIPAY_TEST_TOKEN" \
-d '{"order_id": "TEST-1"}'Колбеки по тестових заявках надходять так само, як по бойових, — зручно перевірити обробник і підпис до запуску. Бойовим ключем цей метод повертає 403.
Коди статусів
Pay-in — status_id
| Код | Назва | Що означає |
|---|---|---|
1 | NEW | Заявку створено, реквізит ще не видано |
2 | CRED_ISSUED | Реквізит видано, чекаємо оплату |
3 | CUSTOMER_PAID | Клієнт повідомив про оплату, перевіряємо надходження |
4 | CANCELLED | Скасовано |
5 | COMPLETED | Оплату підтверджено, кошти зараховано на баланс |
6 | REJECTED | Зарезервовано |
7 | APPEAL | Відкрито апеляцію або спір |
8 | NOT_TAKEN | Час вийшов, реквізит так і не видано |
9 | EXPIRED | Час на оплату вийшов |
Pay-out — payout_status
| Статус | Що означає |
|---|---|
NEW | У черзі, чекає виконавця |
PROCESSING | Виконавець узяв і виконує, або відкрито спір |
COMPLETED | Виконано, квитанція доступна |
CANCELLED | Скасовано, ніхто не взяв до pay_before, або банківське повернення — кошти повернуто на баланс |
FAILED | Не виконано за 24 години, резерв повернуто |
Коди помилок
error_code | HTTP | Причина |
|---|---|---|
101 | 401 | Токен невірний, прострочений, відкликаний або без права; недозволений IP; мерчант вимкнений |
102 | 400 | Сума менша за мінімум · в апеляції — немає order_id |
103 | 422 | Сума більша за максимум · в апеляції — немає comments |
104 | 409 | Заявка з таким order_id уже існує · в апеляції — заявку не знайдено (404) |
105 | 400 | Невірний cred_type · в апеляції — апеляцію вже відкрито (409) |
106 | 400 | Немає вільного реквізиту під заявку · у виплаті — невірна картка чи IBAN · в апеляції — заявка вже успішна (409) |
107 | 400 | Непідтримуваний формат файлу |
108 | 400 | Невірний формат картки чи IBAN, або невідомий method |
109 | 400 | Виплату не створено: недостатньо балансу або немає виконавця для суми |
110 | 404 | Виплату не знайдено |
111 | 400 | Валюта не дозволена для акаунта |
400 | 400 | Некоректне поле (amount, order_id, return_url, pay_before…) |
403 | 403 | Клієнт чи реквізит заблоковано антифродом · тестовий метод з бойовим ключем |
404 | 404 | Заявку, апеляцію чи файл не знайдено |
409 | 409 | Дія недоступна для поточного стану |
413 | 413 | Файл завеликий (понад 10 МБ) |
429 | 429 | Перевищено ліміт запитів, ліміт на клієнта або частоту заявок |
503 | 503 | Приймання тимчасово зупинено платформою |
Чекліст запуску
- Бойовий і тестовий ключі створено, токени збережено в секретах сервера, не в коді.
- Для бойового ключа задано перелік IP ваших серверів.
callback_urlвказано, доступний з інтернету по HTTPS і відповідає 2xx за 10 секунд.- Обробник перевіряє
X-Signatureна сирому тілі; тестовий вектор дає правильний підпис. - Колбеки обробляються ідемпотентно: зберігається
version, застарілі ігноруються. - Оплата зараховується лише за
status_id: 5і за сумою з колбека. - Клієнту показується саме
amountз відповіді (може відрізнятися від запитаної). - Передається
customer_id; за можливості —client_bank. - Обробляються помилки
106(немає реквізиту) і109(виплату не створено) — показати клієнту зрозуміле повідомлення або повторити пізніше. - Повний цикл пройдено тестовим ключем:
paid→complete,cancel,expire, апеляція, виплата. - Перша бойова заявка — на невелику суму.
EmiPay