YuppiPayAPI v1
UK RU EN
Merchant Integration Reference

Accept and pay out through one consistent contract

A P2P payment system: create payin and payout orders, reconcile statuses, receive webhooks in a single canonical format. All amounts are in major units, 2 decimal places.

BASE URL https://yuppipay.net/api/v1

Overview

Every request is signed and goes to a versioned path /api/v1/…. The version lives in the path: when v2 arrives, your v1 integration keeps working unchanged. Within a version we only add fields — we never remove or rename them.

MethodPathPurpose
POST/ordersCreate a payin order, get the requisite for the payer
GET/orders/{uuid}Current state of the order
POST/payoutsCreate a payout to the recipient's card
GET/payouts/{uuid}Current state of the payout
POST/payouts/{uuid}/cancelCancel a payout no trader has taken yet
GET/balanceBalance and turnover broken down by currency

Authentication & signature

Add two headers to every request. The server's verification order is: rate limit → body integrity → key → signature, so the error names its exact cause.

HeaderValue
X-API-KeyYour api_key (the merchant's public identifier)
X-SignatureHMAC-SHA256(<raw request body>, api_secret) in hex
Content-Typeapplication/json

The exact bytes of the body you send are signed — serialize the JSON once and sign that very string. For GET requests the body is empty, so X-Signature is the HMAC of an empty string with api_secret.

Two different secrets — don't mix them up

api_secret signs your requests to us. webhook_secret is what you use to verify our callbacks. Verifying a callback with the wrong secret is the most expensive integration mistake.

Signature example (bash)
# the body we send
BODY='{"order_id":"A-1001","amount":1500.00,"currency":"UAH"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | sed 's/^.*= //')

curl -X POST https://yuppipay.net/api/v1/orders \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Signature: $SIG" \
  -d "$BODY"

Additionally: if an IP allowlist is set in the profile, requests are accepted only from it (REMOTE_ADDR is checked, X-Forwarded-For is ignored).

Idempotency

Orders and payouts are idempotent by the pair (merchant, order_id). An exact repeat of the same request returns the original object (no duplicate is created). The same order_id with a changed body (amount, currency, payer / recipient card) is a 409 idempotency_conflict conflict, and the response contains the existing object.

In practice

Set your own order_id and safely retry network failures with the same request — there will be no duplicate. callback_url is not part of the fingerprint: changing the delivery address and retrying is the same order.

Payin orders

POST/api/v1/orders

Creates a payin order and returns the requisite (card) you show to the payer. The commission is withheld from the amount (inclusive): the merchant is credited amount − commission.

Request parameters
FieldTypeDescription
order_idstringrequiredYour external number. Idempotency key. Up to 255 characters.
amountnumberrequiredAmount in major units, ≥ 0.01, 2 decimals.
currencystring(3)requiredISO currency code. Must be active and enabled for payins.
payer_idstringoptionalPayer identifier. Participates in routing and idempotency.
payer_bankstringoptionalPayer's bank. Used only when the bank-match rule is enabled.
callback_urlstring(url)optionalCallback address for this order. Empty → the address from the dashboard.
Request
{
  "order_id": "A-1001",
  "amount": 1500.00,
  "currency": "UAH",
  "payer_id": "user-42"
}
Response · 201
{
  "id": "7af7c1df-0629-42d4-9068-df6500b3acd6",
  "order_id": "A-1001",
  "status": "awaiting_payment",
  "amount": 1500.00,
  "amount_settled": null,
  "commission": 30.00,
  "currency": "UAH",
  "exchange_rate": 45.58,
  "settlement_currency": "USDT",
  "payer_id": "user-42",
  "requisite": {
    "card_number": "5375 4141 0000 1234",
    "card_holder": "IVAN PETRENKO",
    "bank": "PrivatBank",
    "type": "card"
  },
  "created_at": "2026-08-09T10:00:00+00:00",
  "expires_at": "2026-08-09T10:15:00+00:00",
  "completed_at": null
}

Show requisite.card_number to the payer; wait for the order.completed webhook (not polling). The id field is the {uuid} for the status request.

GET/api/v1/orders/{uuid}

Returns the current state of the order (the same object as POST /orders). Another merchant's order is invisible even with a known UUID.

Order statuses
awaiting_paymentRequisite issued, waiting for the payer's payment
processingPayment is seen, crediting reconciliation is in progress
completedCredited successfully — order.completed webhook
expiredNot paid in time — order.expired webhook
cancelledCancelled by an operator — order.cancelled webhook
disputedA dispute is open; the resolution will close the order — order.disputed webhook
POST/api/v1/orders/{uuid}/cancel

Cancel an order that has not been paid yet: the payer changed their mind, or an operator closes it on your side. The trader card is freed for the next order immediately. The response carries the same order object with status cancelled, and an order.cancelled event is sent to your callback_url.

The body is optional. You may pass reason — it goes into the webhook, the audit log and the order card, where a payment-system operator reads it, so write a human-readable text rather than an internal code; an empty body {} is accepted too, and «Cancelled by merchant» is recorded. Sign exactly the string you send.

Only unpaid orders can be cancelled — statuses pending and awaiting_payment. Once the payer has sent the money (processing) or a dispute is open, API cancellation no longer works: funds may already have reached the trader card, and support handles such cases. The response is 409 order_not_cancellable with the current order state in the order field.

Payouts

POST/api/v1/payouts

Creates a payout to the recipient's card. The commission is added on top (on-top): the merchant is charged amount + commission = amount_charged.

Request parameters
FieldTypeDescription
order_idstringrequiredYour external payout number. Idempotency key.
amountnumberrequiredTransfer amount, ≥ 0.01, 2 decimals.
currencystring(3)requiredISO code. Active and enabled for payouts.
recipient_cardstringrequiredRecipient's card number (up to 32).
recipient_namestringoptionalRecipient's name. Participates in idempotency.
recipient_bankstringoptionalRecipient's bank.
callback_urlstring(url)optionalCallback address for this payout.
Response · 201
{
  "id": "b1e2c3d4-5678-90ab-cdef-1234567890ab",
  "order_id": "P-2001",
  "status": "pending",
  "amount": 1000.00,
  "commission": 15.00,
  "amount_charged": 1015.00,
  "currency": "UAH",
  "exchange_rate": 45.58,
  "settlement_currency": "USDT",
  "recipient": { "card_number": "4149 •••• •••• 3179", "name": "IVAN P.", "bank": "A-Bank" },
  "receipt_attached": false,
  "created_at": "2026-08-09T10:00:00+00:00",
  "expires_at": "2026-08-09T14:00:00+00:00",
  "completed_at": null
}
GET/api/v1/payouts/{uuid}

Payout state (the same object as at creation), scoped to your merchant.

Payout statuses
pendingIn the shared pool, waiting for a trader
assigned / processingTaken into work — payout.processing webhook
completedDone — payout.completed webhook, has tx_receipt
expiredTime window exhausted
cancelledCancelled
disputedA dispute is open
POST/api/v1/payouts/{uuid}/cancel

Withdraw a payout while no trader has taken it. The body is empty — send {} and sign exactly those two bytes. The response is the same payout object with status cancelled, and a payout.cancelled event goes to your callback_url.

Once a trader has taken the payout, the API will not cancel it: the transfer may already be under way. Support closes such cases — the answer is 409 payout_not_cancellable with the current payout state in the payout field.

Balance

GET/api/v1/balance

Balance and turnover broken down by currency, computed the same way as in the dashboard: settled − paid out − hold on open payouts − withdrawal requests + adjustments (requests in reserving statuses; adjustments are manual corrections made by an administrator, the adjustments field).

Response · 200
{
  "merchant_id": 1,
  "balances": [
    {
      "currency": "UAH",
      "balance": 598229.84,
      "settled": 6449109.84,
      "paid_out": 5849865.00,
      "held": 1015.00,
      "withdrawn": 0.00,
      "adjustments": 0.00,
      "turnover": 7010100.00,
      "commission": 560792.16,
      "orders": 438,
      "payouts": 56,
      "withdrawals": 0
    }
  ],
  "generated_at": "2026-08-09T10:15:00+00:00"
}
held - funds locked by open payouts

A created payout locks the amount with commission at once: it moves to held and lowers balance. Completion moves it to paid_out, cancellation or expiry returns it to the available funds.

Webhooks

We send a POST to your delivery address on key transitions — order completion and expiry, amount correction, and payout changes — rather than literally on every status change. Respond with 2xx quickly; process the body idempotently.

Delivery headers
Content-Typeapplication/json
X-EventEvent type, e.g. order.completed — you can route handlers without parsing the body
X-SignatureHMAC-SHA256(<raw body>, webhook_secret) — verify before processing
Signature verification

Compute the HMAC over the raw bytes of the body (before JSON parsing) and the webhook_secret — not api_secret. Compare in constant time.

Order events

The canonical payload is the same for all order.* events: fixed field order and names.

EventWhen it is sent
order.completedThe order was closed successfully — credit it as a deposit
order.amount_correctedThe amount of an already completed order was corrected. Arrives instead of a repeated order.completed
order.expiredThe order was not paid in time
order.cancelledOrder was cancelled by an operator, the hold was released. Extra field: reason
order.disputedA dispute was opened for the order, the final status is not yet decided. Extra field: dispute_id

A manual resend from the admin panel sends the same payload with an extra flag "resent": true.

Canonical body · order.completed
{
  "event": "order.completed",
  "order_id": "7af7c1df-0629-42d4-9068-df6500b3acd6", // our uuid
  "merchant_order_id": "A-1001",                      // your number
  "status": "completed",
  "currency": "UAH",
  "amount": 1500.00,                                  // declared (does not change)
  "amount_received": 1500.00,                         // actually received
  "amount_settled": 1470.00,                          // to be credited (minus commission)
  "exchange_rate": 45.58,
  "settlement_currency": "USDT"
}
Key amounts

Credit the player/customer by amount_received (actually received), and amount is the initial order, it never changes. amount_settled is what will land on your balance after commission.

Extended body · order.amount_corrected
{
  "event": "order.amount_corrected",
  "order_id": "7af7c1df-0629-42d4-9068-df6500b3acd6",
  "merchant_order_id": "A-1001",
  "status": "completed",
  "currency": "UAH",
  "amount": 1500.00,
  "amount_received": 1400.00,
  "amount_settled": 1372.00,
  "exchange_rate": 45.58,
  "settlement_currency": "USDT",
  "correction_id": 42,
  "reason": "partial_payment",
  "amount_original": 1500.00, // amount before correction
  "amount_corrected": 1400.00 // new amount
}
Why a separate event

If the amount is corrected after the order was already completed (and you already received order.completed), a repeated order.completed would cause a double credit. That's why order.amount_corrected arrives — update the amount, don't credit it a second time.

Payout events

EventWhen
payout.processingThe payout was taken into work
payout.completedThe payout was executed (has tx_receipt)
payout.expiredThe payout's time window is exhausted
payout.cancelledThe payout was cancelled
payout.disputedA dispute was opened on the payout
Payout webhook body
{
  "event": "payout.completed",
  "payout_id": "b1e2c3d4-5678-90ab-cdef-1234567890ab", // our uuid
  "order_id": "P-2001",                                // YOUR external number
  "amount": 1000.00,
  "amount_charged": 1015.00,                           // charged to the merchant
  "currency": "UAH",
  "exchange_rate": 45.58,
  "settlement_currency": "USDT",
  "status": "completed",
  "tx_receipt": "https://.../receipt.jpg",
  "completed_at": "2026-08-09T10:15:00+00:00"
}
Order exchange rate

exchange_rate is the settlement_currency rate against the document currency, fixed when the order or payout is created and never recalculated. Use it for your own average rate. Older documents without a rate return null.

order_id semantics differ

In payout events order_id is your external number, and the payout's uuid is in payout_id. In order events it's the opposite: order_id is our uuid, and your number is merchant_order_id. Map carefully.

Delivery & retries

Delivery is asynchronous via a queue. Success is any 2xx. A non-2xx or timeout (connect 5s / read 15s) schedules a retry with increasing delay.

Attempt12345+
Delay30s60s5 min15 min1 h

Up to 5 attempts; after they are exhausted the callback is marked failed. Process idempotently by order_id/payout_id — the same callback may arrive again.

Error codes

All errors are JSON with an error field (machine code) and message. Switch on error, not on the text.

HTTPerrorWhen
400empty_bodyEmpty body in a POST
400malformed_jsonThe body is not valid JSON
400content_type_missingContent-Type: application/json was not sent
401api_key_missingNo X-API-Key header
401api_key_invalidThe key does not match any merchant
401signature_missingNo X-Signature header
401signature_invalidThe signature did not match the body's HMAC
403merchant_suspendedThe merchant is suspended/blocked
403ip_not_allowedThe IP is not on the allowlist
404order_not_foundOrder not found (or belongs to someone else)
404payout_not_foundPayout not found (or belongs to someone else)
404unsupported_api_versionPath without a version or an unknown version
404endpoint_not_foundThe version exists, the path does not (the response lists the available ones)
405method_not_allowedThe path exists, but not for this method
409idempotency_conflictThe same order_id with a changed body
422validation_failedFields failed validation (details are in details)
422amount_out_of_rangeAmount outside min/max (present in the body)
422currency_not_supportedThe currency is not active on the platform
422currency_not_enabledPayins in this currency are not enabled for the merchant
422payout_not_enabledPayouts in this currency are not enabled
422insufficient_balanceMerchant balance plus the overdraft limit does not cover the payout with commission. Body carries required, balance, overdraft_limit
429merchant_limitDaily amount or count limit
429Rate limit: 600 requests per minute per key. The response is the server default and carries no error field — check the 429 status before parsing the body. The Retry-After header tells you how long to wait
503no_requisite_availableNo free requisite available (has Retry-After: 5)
503rate_unavailableNo exchange rate configured for the currency - order or payout not created
503accepting_suspendedKill-switch blocks payins
503payouts_suspendedKill-switch blocks payouts

YuppiPay · Merchant API v1 · all amounts in major units (2 decimals), times in ISO 8601 (UTC). Secrets are managed in the merchant dashboard → "Integration".