EmiPay API for platforms
Partner API · v1

API for executing platforms

The platform takes pay-outs from the EmiPay order book, transfers the money to the recipient and sends the receipt. For each completed pay-out the platform gets USDT on its EmiPay balance.

The platform’s cabinet stays: operators see orders, statistics, balance and disputes, and can take a pay-out by hand.

1Order book

You get the available pay-outs: a list, plus webhooks when they appear and disappear.

2Take

The first to take a pay-out gets it, together with the recipient’s card.

3Pay

You transfer the UAH to the recipient’s card.

4Receipt

You send the receipt — the pay-out completes, USDT is credited.

Access and key

Base URLhttps://partner.emipay.net/api/partner/v1All methods
API keyCabinet → “API integration” → a key with the read and execute scopesHeader Authorization: Bearer emi_…. Shown once.
Webhook secretCabinet → “API integration” → a webhook with your URLEvent signature check (whsec_…). Shown once.

Keys and webhooks are created by the platform’s supervisor. A key can be restricted to your servers’ IP addresses. Up to 10 active keys per account.

The API is for server-to-server requests only. Never put the key into a web page or a mobile app.

Request format

  • Request and response bodies are JSON (Content-Type: application/json).
  • Every POST carries an Idempotency-Key header (a unique string): a retry with the same key never repeats the action, so a request can be safely repeated after a network error.
  • Amounts are strings: UAH with 2 decimals ("1800.00"), USDT with 6 ("38.730671"). Times are Unix milliseconds.
  • Rate limit — 600 requests per minute per account. Remaining — in X-RateLimit-Remaining; over the limit — 429.
  • An error is HTTP 4xx with {"error":"…","request_id":"…"}. Quote the request_id when contacting support.
Working with pay-outs

Turn intake on

POST/api/partner/v1/intake

While intake is off the book is empty and nothing can be taken. GET /profile shows the admission (directions and amount limits), the intake state and the balance.

Request
curl -X POST https://partner.emipay.net/api/partner/v1/intake \
  -H "Authorization: Bearer $EMIPAY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"enabled": true}'

Order book

GET/api/partner/v1/orders/available?page=1

The initial state of the book is the list of available pay-outs, 50 per page. The book has no recipient data: it appears once you take the order.

Request
curl https://partner.emipay.net/api/partner/v1/orders/available?page=1 \
  -H "Authorization: Bearer $EMIPAY_KEY"
Response 200
{
  "page": 1,
  "total": 1,
  "intake": true,
  "rows": [
    {
      "id": "100edd64-1f20-42db-85a8-e52fc4896646",
      "num": 102121,
      "direction": "payout",
      "status": "pending",
      "currency": "UAH",
      "settlement_currency": "USDT",
      "amount": "1800.00",
      "settlement_amount": "38.730671",
      "reward": "0.000000",
      "rate": "46.48",
      "expires": 1793228865353,
      "created": 1793217888298,
      "updated": 1793217888298
    }
  ]
}
amountUAH to pay to the recipient
settlement_amountUSDT the platform receives for this pay-out
rateYour rate: amount ÷ settlement_amount
expiresUntil when the pay-out is available / must be done

Then keep the book current with the trade.available and trade.unavailable webhooks (see “Webhooks”). Every 1–2 minutes reconcile it with this list in case a delivery was missed.

Take a pay-out

POST/api/partner/v1/orders/{id}/accept

The first to take a pay-out (a trader or a platform) gets it. The response is the order with status active and the recipient’s card in recipient.

Request
curl -X POST https://partner.emipay.net/api/partner/v1/orders/100edd64-1f20-42db-85a8-e52fc4896646/accept \
  -H "Authorization: Bearer $EMIPAY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'
Response 200
{
  "id": "100edd64-1f20-42db-85a8-e52fc4896646",
  "num": 102121,
  "direction": "payout",
  "status": "active",
  "amount": "1800.00",
  "settlement_amount": "38.730671",
  "rate": "46.48",
  "recipient": "5375414112345672",
  "expires": 1793228865353,
  …
}
  • 200 — it is yours: pay to recipient before expires.
  • 409 — already taken — remove it from the book.
  • {"ok":true,"result":"auction","auction_until":…} — several takers at the same moment; by auction_until the executor is decided — you get trade.accepted (yours) or trade.unavailable (not yours).

Receipt

POST/api/partner/v1/orders/{id}/receipt

After the transfer send the receipt — a PDF or an image, up to 10 MB, base64. The order goes to paid, we confirm the pay-out at the source, and it becomes completed — the USDT is credited to the platform’s balance. Every receipt is checked by AI: make sure the recipient’s card / IBAN and the amount match the order and are clearly visible.

Request
curl -X POST https://partner.emipay.net/api/partner/v1/orders/100edd64-1f20-42db-85a8-e52fc4896646/receipt \
  -H "Authorization: Bearer $EMIPAY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"filename": "receipt.pdf", "base64": "JVBERi0xLjQK…"}'

Several files for one pay-out: send all but the last with "more": true — the pay-out closes after the last file.

Request
POST /orders/{id}/receipt   {"filename": "1.jpg", "base64": "…", "more": true}
POST /orders/{id}/receipt   {"filename": "2.jpg", "base64": "…"}

Decline

POST/api/partner/v1/orders/{id}/decline

Cannot do a pay-out you took — decline it with a reason:

Request
curl -X POST https://partner.emipay.net/api/partner/v1/orders/100edd64-1f20-42db-85a8-e52fc4896646/decline \
  -H "Authorization: Bearer $EMIPAY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"reason": "error", "comment": "…"}'
  • error — taken by mistake: the pay-out goes back to the book for other executors.
  • wrong_requisite — the recipient’s details are wrong (card blocked, does not exist): the pay-out is cancelled.

Your orders and balance

GET/api/partner/v1/orders?status=active&page=1
GET/api/partner/v1/orders/{id}
GET/api/partner/v1/balance
GET/api/partner/v1/profile
GET/api/partner/v1/events?after=0
GET/api/partner/v1/ping

Your orders by status (active, paid, review, disputed, completed, cancelled, expired), one order, the balance and the event log — a fallback to webhooks.

Webhooks

Events

EmiPay sends events to your URL. Answer 2xx within 10 s, otherwise the delivery is retried (10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h). At-least-once delivery: skip repeats by EmiPay-Event-Id.

HTTP
POST https://your-server/emipay
Content-Type: application/json
EmiPay-Event: trade.available
EmiPay-Event-Id: evt_3f2a…
EmiPay-Delivery: 7c1e…
EmiPay-Signature: t=1793218065,v1=<hex HMAC-SHA256(secret, t + "." + body)>

{"id":"evt_3f2a…","type":"trade.available","created":1793218065475,"data":{ …order… }}
EventWhenWhat to do
trade.availableA pay-out entered the book (new, or returned to the pool) within your admissionAdd it to the book; data is the order as in the list
trade.unavailableA pay-out left the book: reason = taken (another executor took it), cancelled, expiredRemove it from the book
trade.acceptedThe pay-out is assigned to you (also after an auction)Pay before expires
trade.deadlineThe time to pay is running outSend the receipt or decline
trade.paidReceipt accepted, confirming—
trade.completedPay-out completed, USDT credited—
trade.review, trade.disputedThe pay-out went to review / a dispute was openedContact support, add evidence in the cabinet
trade.cancelled, trade.expired, trade.recalculatedThe order was cancelled / expired / recalculatedUpdate on your side
trade.unavailable
{"id":"evt_…","type":"trade.unavailable","created":1793218065475,"data":{"id":"100edd64-…","num":102121,"reason":"taken"}}

Signature check

The signature is HMAC-SHA256 of t + "." + body with the webhook secret. Compute it over the raw request body and reject t older than 5 minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody — the request body exactly as received (string), header — EmiPay-Signature
function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
  return v1?.length === expected.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Reference

Order statuses

StatusMeaning
pendingIn the book, nobody has taken it
activeTaken by you — pay and send the receipt
paidReceipt received, confirming at the source
reviewUnder review by the administration
disputedA dispute is open
completedPay-out completed, USDT credited
cancelledCancelled
expiredTime is up

Error codes

CodeWhen
401No key, or the key is wrong, expired or revoked
403The key lacks the scope, the request comes from a disallowed IP, or intake is disabled by the administration
404Order not found (or not yours)
409The order’s state does not allow the action — e.g. already taken
413The receipt file is too large
422Invalid request data
429Rate limit exceeded

Launch checklist

  • The key with the read and execute scopes is stored in server secrets; your servers’ IPs are set.
  • The webhook is reachable over HTTPS and answers 2xx within 10 seconds.
  • The handler checks EmiPay-Signature over the raw body and skips repeats by EmiPay-Event-Id.
  • The book is built from the list and updated by trade.available / trade.unavailable; reconciled with the list every 1–2 minutes.
  • Every POST has a unique Idempotency-Key.
  • 409 on taking (someone else took it) and the auction answer are handled.
  • A receipt is sent only after the actual transfer; several files — with "more": true.
  • The first pay-out is a small amount.
EmiPay Partner API · v1Back to top ↑