Skip to content
Beta

2Pay API v1

Reference for the 2Pay partner API: buy and sell USDT for AZN from your own systems. For banks, fintechs and other companies onboarded by 2Pay.

Contents

Overview

The 2Pay API lets banks, fintechs and other companies buy and sell USDT for Azerbaijani manat (AZN) from their own systems. It is a REST API over HTTPS: JSON requests and replies, every call signed with your API key.

Buy: you pay AZN by bank transfer and 2Pay sends USDT to your wallet. Sell: you send USDT to 2Pay's deposit address and 2Pay pays AZN to your company's bank account. A 2Pay operator checks every payment before anything is sent; nothing is sent automatically.

Access is by agreement: the 2Pay team onboards each partner after due diligence (KYB). There is no self-service sign-up. Request access.

Base URL
https://api.2pay.az/v1
Signing
HMAC-SHA256
Quotes
5 min, single use
Payment window
2 hours
Webhooks
10 retries, about 28 h
Rate limit
120 a minute per key
Minimum order
10 AZN
Networks
8, USDT only

Quickstart

  1. Get your API key (tp_live_…) and secret (tps_…) from the 2Pay team. The secret is shown once: keep it in a secrets manager on your server.
  2. Send 2Pay the public IP addresses of your servers (for the key's allowlist) and your webhook URL (https).
  3. Sign a call to GET /v1/account to check your setup: it returns your company, limits and keys.
  4. Ask for a quote with POST /v1/quotes, then place the order with POST /v1/orders within 5 minutes.
  5. Pay as paymentInstructions says, report it with POST /v1/orders/{id}/payment, and follow the status by webhook or with GET /v1/orders/{id}.

Shell, with the twopay function from Authentication & signing

# Check the setup.
twopay GET /v1/account

# Lock a price for 5 minutes: buy USDT for 1,700.00 AZN on TRON.
twopay POST /v1/quotes '{"side":"buy","amount":"1700.00","network":"TRC20"}'

# Place the order with the quote id and your own unique id.
twopay POST /v1/orders '{"quoteId":"3c8f5e2a-91d4-4b6e-a7f0-5d2e8c1b9a46","clientOrderId":"inv-20261015-0042","walletAddress":"<your TRC20 address>"}'

# After the bank transfer, report your bank's reference for it.
twopay POST /v1/orders/inv-20261015-0042/payment '{"reference":"FT26288QX7Z1"}'

# Follow the status (webhooks tell you too).
twopay GET /v1/orders/inv-20261015-0042

Base URL & environments

Every endpoint is under https://api.2pay.az/v1. There is one environment, production: no sandbox and no test mode. Once your account is set up, test with small real orders; the minimum order value is 10 AZN.

Use HTTPS only. Every reply carries an X-Request-Id header (req_…); quote it when you write to 2Pay about a call. Replies can gain new fields over time, so ignore fields you do not know.

Authentication & signing

Every call except GET /v1/openapi.yaml is signed with HMAC-SHA256 and carries three headers:

Headers

X-Api-KeystringRequired
Your key id, tp_live_….
X-TimestampstringRequired
Current Unix time in milliseconds, e.g. 1792056665120. It must be within 5 minutes of 2Pay's clock, either way.
X-SignaturestringRequired
Hex HMAC-SHA256 of the canonical string, keyed with your secret: 64 characters, any case.

Canonical string: four lines joined with \n, no newline at the end

<X-Timestamp>
<HTTP method in upper case>
<path and query string exactly as sent, e.g. /v1/orders?status=completed&limit=20>
<hex SHA-256 of the raw request body>
  • Sign the exact bytes you send: serialize the JSON once and send that string. Build the query string yourself and send it unchanged.
  • A call without a body (every GET, POST …/cancel, POST /v1/webhooks/test) hashes the empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
  • Each signature works once. Sign every retry again with a new timestamp; a repeated signature returns 401 INVALID_SIGNATURE.
  • Missing headers return 401 UNAUTHENTICATED. An unknown or revoked key, a wrong signature or a timestamp more than 5 minutes off return 401 INVALID_SIGNATURE. An IP address outside the key's allowlist, or a suspended account, return 403 FORBIDDEN.
  • Keep your server clock synchronised (NTP).

Signing a request · curl

# Bash 4+, GNU date, OpenSSL and curl. Export TWOPAY_KEY and TWOPAY_SECRET first.
twopay() {
  local method=$1 path=$2 body=${3-}
  local ts body_hash signature
  ts=$(date +%s%3N)  # Unix time in milliseconds
  body_hash=$(printf '%s' "$body" | openssl dgst -sha256 -r | cut -d' ' -f1)
  signature=$(printf '%s\n%s\n%s\n%s' "$ts" "$method" "$path" "$body_hash" \
    | openssl dgst -sha256 -hmac "$TWOPAY_SECRET" -r | cut -d' ' -f1)
  curl -sS -X "$method" "https://api.2pay.az$path" \
    -H "Content-Type: application/json" \
    -H "X-Api-Key: $TWOPAY_KEY" \
    -H "X-Timestamp: $ts" \
    -H "X-Signature: $signature" \
    ${body:+--data-raw "$body"}
}

twopay GET /v1/account
twopay POST /v1/quotes '{"side":"buy","amount":"1700.00","network":"TRC20"}'

Requests & responses

  • Send JSON in UTF-8 with Content-Type: application/json. A body can be at most 64 KB; a bigger one returns 400 VALIDATION_ERROR. Unknown fields are ignored.
  • Money is a decimal string, never a number: AZN with 2 decimals ("1700.00"), USDT with 6 ("969.252507"). Send amount as a string too. A Money object is {"amount": "…", "currency": "AZN" | "USDT"}.
  • Rates are AZN per 1 USDT. platformRate is a fraction: "0.010000" is 1%.
  • Times are ISO 8601 in UTC with milliseconds, e.g. 2026-10-15T09:31:05.120Z.
  • Ids are UUIDs; order references look like 2P-7KQ4M9XD. Empty fields are null, not left out (except in paymentInstructions).

Errors come in one envelope with a stable code and an English message for people. Program against the code: messages can change. VALIDATION_ERROR messages start with the field name, e.g. amount: AZN amount with at most 2 decimals. See Errors.

Error · 409

{
  "error": {
    "code": "QUOTE_EXPIRED",
    "message": "The quote has expired. Request a new one."
  }
}

Idempotency

  • POST /v1/orders takes clientOrderId, your own unique id for the order (1–64 characters of A–Z a–z 0–9 . _ : -). Use an id from your system, such as a payment or invoice id, not a new random one per attempt.
  • Sending it again with the same clientOrderId and the same quoteId returns the existing order with 200 OK and the header Idempotent-Replayed: true instead of 201 Created, even after the quote has expired. Two calls at the same moment still make one order.
  • The same clientOrderId with a different quoteId returns 409 IDEMPOTENCY_CONFLICT.
  • walletAddress is not compared on a replay: the order keeps the address it was created with.
  • A quote backs one order: using it with a new clientOrderId returns 409 QUOTE_ALREADY_USED.
  • clientOrderId works in place of the order id in every /v1/orders/{id} path.
  • Reporting a payment or cancelling changes an order once: repeating it returns 409 ORDER_NOT_AWAITING_PAYMENT or 409 ORDER_NOT_CANCELLABLE.

Rate limits

  • 120 requests a minute per API key, counting the calls that pass the signature check.
  • 300 requests a minute per client IP address, counting every signed call before it is checked. GET /v1/openapi.yaml is not limited.
  • Each 2Pay gateway process counts on its own, in one-minute windows that start with the first request.
  • Over a limit you get 429 RATE_LIMITED without a Retry-After header. Back off, then retry with a new signature.

Limits

  • Minimum order value: 10 AZN. The value of an order is in AZN: what you pay (buy) or what 2Pay pays you (sell).
  • Maximum per order: your maxOrderAzn (100,000.00 AZN unless your agreement sets another). Checked when you ask for a quote, and again when you place a sell order: 422 AMOUNT_OUT_OF_RANGE. A quote too small to cover the fees gets the same code.
  • Daily limit: your dailyLimitAzn (500,000.00 AZN unless your agreement sets another) per UTC day, 00:00–24:00 UTC. Checked when you place an order: the value of today's orders, not counting rejected, expired and cancelled ones, plus the new order must fit, otherwise 422 LIMIT_EXCEEDED.
  • Quote validity: 5 minutes (expiresAt).
  • Payment window: 2 hours from the order (paymentDeadline); then an unpaid order becomes expired.
  • GET /v1/account shows your limits.

Order lifecycle

An order has one of six statuses. completed, rejected, expired and cancelled are final. A webhook is sent when the order is created and at every change.

Order statuses and what moves an order from one to the next
FromToWhat makes it happen
Createdpending_paymentPOST /v1/orders
pending_paymentpayment_submittedPOST /v1/orders/{id}/payment
pending_paymentexpiredpaymentDeadline passes
pending_paymentcancelledPOST /v1/orders/{id}/cancel
payment_submittedcompletedA 2Pay operator confirms (two of them from 20,000 AZN)
payment_submittedrejectedA 2Pay operator rejects
StatusMeaning
pending_paymentCreated. Waiting for your AZN transfer (buy) or USDT deposit (sell) and for you to report it, until paymentDeadline.
payment_submittedYou reported the payment. A 2Pay operator is checking it; from 20,000 AZN two different staff members approve the order, and it stays in this status until the second one.
completedFinalThe operator confirmed the payment and 2Pay sent the USDT (buy) or paid the AZN (sell).
rejectedFinalThe operator could not confirm the payment, or rejected a buy whose walletAddress Tether froze after the order was placed; rejectionReason says why. Contact 2Pay about any funds you sent.
expiredFinalThe payment window (2 hours) ended before you reported a payment. Expired orders are not reopened: place a new one.
cancelledFinalYou cancelled the order before paying.

Buy flow

  1. Quote: POST /v1/quotes with side "buy", amount in AZN and network, one of buyNetworks in GET /v1/rates. receive.amount is the USDT you get after fees.
  2. Order: POST /v1/orders with quoteId, clientOrderId and walletAddress, your USDT address on that network. The address is checked first.
  3. Pay: transfer exactly paymentInstructions.amount AZN to paymentInstructions.bankAccount with paymentInstructions.reference (the order reference) in the transfer description, before paymentDeadline.
  4. Report: POST /v1/orders/{id}/payment with reference, your bank's reference for the transfer. The order becomes payment_submitted.
  5. Settlement: a 2Pay operator matches the transfer and sends the USDT to walletAddress. The order becomes completed, with payoutTxHash when recorded, or rejected with rejectionReason.

Example: 1,700.00 AZN at a rate of 1.7346 with a 1% platform fee and a 1 USDT network fee gives (1700 ÷ 1.7346) × 0.99 − 1 = 969.252507 USDT.

Sell flow

  1. Networks: sellNetworks in GET /v1/rates lists where you can sell now.
  2. Quote: POST /v1/quotes with side "sell", amount in USDT and network. receive.amount is the AZN for that amount; the order asks for its own amount close to it (see below), and the order's send and receive are what you send and what 2Pay pays.
  3. Order: POST /v1/orders with quoteId and clientOrderId, without walletAddress. Your account needs a payout bank account registered by 2Pay (a new payout account takes effect once a second 2Pay staff member approves it); without one the call returns 400 VALIDATION_ERROR.
  4. Send: transfer exactly paymentInstructions.amount USDT on paymentInstructions.network to paymentInstructions.address before paymentDeadline. It is 2Pay's deposit address for that network, shared and not made for your order: send only USDT, only on that network. Do not send while paymentInstructions.paused is true.
  5. Own amount: the order asks for its own USDT amount, the quote's cut to whole cents plus 0.01–0.99 USDT (below the quote's near the top of your limits), so a transfer to the shared address matches one order only. send and receive on the order show that amount and the AZN for it, at the quote's rate. Send it to the cent; if every amount near the quote's is taken by other open sales on the network, 409 AMOUNT_BUSY.
  6. Report: POST /v1/orders/{id}/payment with txHash, the hash of your transaction. One transaction backs one order (409 TX_ALREADY_USED).
  7. Settlement: 2Pay looks your transaction up on its blockchain (that 2Pay's deposit address received the order's USDT in it and that it is final), then a 2Pay operator pays the AZN to your payout account. The order becomes completed, or rejected with rejectionReason.

Example: 1,000 USDT at a rate of 1.6984 with a 1% platform fee and a 1 USDT network fee gives (1000 − 1) × 1.6984 × 0.99 = 1,679.73 AZN.

Endpoints

Paths are relative to https://api.2pay.az. Besides its own errors, every signed endpoint can return VALIDATION_ERROR, UNAUTHENTICATED, INVALID_SIGNATURE, FORBIDDEN, RATE_LIMITED, INTERNAL_ERROR and SERVICE_UNAVAILABLE.

The order object

Order

idstring
2Pay's order id (UUID).
referencestring
2Pay's order reference, e.g. 2P-7KQ4M9XD. Buy: write it in the transfer description.
clientOrderIdstring
Your id from POST /v1/orders.
sidestring
buy or sell.
statusstring
See Order lifecycle.
networkstring
Network id.
walletAddressstring | null
Buy: where the USDT goes. Sell: null.
sendMoney
What you pay: AZN (buy) or USDT (sell).
receiveMoney
What you get after fees.
ratestring
AZN per 1 USDT, from the quote.
feesobject
network (Money, USDT) and platformRate (fraction).
paymentInstructionsobject
How to pay. Buy: method bank_transfer, amount, bankAccount and reference. Sell: method crypto_deposit, amount, network and address. Fields that do not apply are left out.
paymentInstructions.pausedboolean
Sell only, and only while it is true: Tether has frozen 2Pay's deposit address on this order, or may have while 2Pay confirms it, so selling into it is paused. Do not send USDT to paymentInstructions.address; the status does not change. When the pause ends the field goes away.
paymentReferencestring | null
Buy: the bank reference you reported.
depositTxHashstring | null
Sell: the transaction you reported.
payoutTxHashstring | null
Buy: the USDT transaction 2Pay sent, when the operator records it.
rejectionReasonstring | null
Set when the order is rejected.
paymentDeadlinestring
2 hours after the order was created.
createdAt, updatedAtstring
When the order was created and last changed.

Order (buy)

{
  "id": "6f1d2c8e-4b7a-4e0f-9a51-3c2d8e7f1a90",
  "reference": "2P-7KQ4M9XD",
  "clientOrderId": "inv-20261015-0042",
  "side": "buy",
  "status": "pending_payment",
  "network": "TRC20",
  "walletAddress": "<your TRC20 address>",
  "send": {
    "amount": "1700.00",
    "currency": "AZN"
  },
  "receive": {
    "amount": "969.252507",
    "currency": "USDT"
  },
  "rate": "1.73460000",
  "fees": {
    "network": {
      "amount": "1.000000",
      "currency": "USDT"
    },
    "platformRate": "0.010000"
  },
  "paymentInstructions": {
    "method": "bank_transfer",
    "amount": {
      "amount": "1700.00",
      "currency": "AZN"
    },
    "bankAccount": "<2Pay bank account details>",
    "reference": "2P-7KQ4M9XD"
  },
  "paymentReference": null,
  "depositTxHash": null,
  "payoutTxHash": null,
  "rejectionReason": null,
  "paymentDeadline": "2026-10-15T11:31:05.120Z",
  "createdAt": "2026-10-15T09:31:05.120Z",
  "updatedAt": "2026-10-15T09:31:05.120Z"
}
GET/v1/ratesSigned

Current rates

Buy and sell rates, the platform fee, the network fee per network and the networks you can buy and sell on now. These figures are indicative: a quote fixes the price.

Request

GET /v1/rates HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>

Response · 200 OK

{
  "buy": {
    "rate": "1.7346",
    "platformRate": "0.01",
    "networkFees": {
      "TRC20": "1",
      "BEP20": "0.5",
      "SOL": "1",
      "ARBITRUM": "0.5",
      "AVAXC": "0.5",
      "TON": "0.5",
      "ERC20": "5",
      "MANTLE": "0.5"
    }
  },
  "sell": {
    "rate": "1.6984",
    "platformRate": "0.01",
    "networkFees": {
      "TRC20": "1",
      "BEP20": "0.5",
      "SOL": "1",
      "ARBITRUM": "0.5",
      "AVAXC": "0.5",
      "TON": "0.5",
      "ERC20": "5",
      "MANTLE": "0.5"
    }
  },
  "buyNetworks": [
    "TRC20",
    "BEP20",
    "SOL",
    "ARBITRUM",
    "AVAXC",
    "TON",
    "ERC20",
    "MANTLE"
  ],
  "sellNetworks": [
    "TRC20",
    "BEP20",
    "TON"
  ],
  "fetchedAt": "2026-10-15T09:30:58.000Z"
}

Response fields

buy.ratestring
AZN per 1 USDT when you buy.
buy.platformRatestring
Platform fee as a fraction: 0.01 is 1%.
buy.networkFeesobject
Network fee in USDT for each network id.
sell.ratestring
AZN per 1 USDT when you sell.
sell.platformRatestring
Platform fee as a fraction: 0.01 is 1%.
sell.networkFeesobject
Network fee in USDT for each network id.
buyNetworksstring[]
Networks you can buy on now. 2Pay can turn a network off for a while; networkFees still lists every network.
sellNetworksstring[]
Networks you can sell on now: open for selling, with a 2Pay deposit address, and not paused.
fetchedAtstring
When the market rate was read.

Errors

503 RATE_UNAVAILABLEand the common errors

POST/v1/quotesSigned

Create a quote

Prices a buy or a sell and locks the rate and fees for 5 minutes. A quote can back one order.

Buy: receive = (amount ÷ rate) × (1 − platformRate) − network fee, rounded down to 6 decimals. Sell: receive = (amount − network fee) × rate × (1 − platformRate), rounded down to 2 decimals.

Body

sidestringRequired
buy (you pay AZN, get USDT) or sell (you send USDT, get AZN).
amountstringRequired
What you send, as a decimal string. Buy: AZN with at most 2 decimals. Sell: USDT with at most 6 decimals. At most 12 digits before the point.
networkstringRequired
Network id, e.g. TRC20 (see Networks). Buy needs one of buyNetworks, sell one of sellNetworks.

Request

POST /v1/quotes HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>
Content-Type: application/json

{
  "side": "buy",
  "amount": "1700.00",
  "network": "TRC20"
}

Response · 201 Created

{
  "id": "3c8f5e2a-91d4-4b6e-a7f0-5d2e8c1b9a46",
  "side": "buy",
  "network": "TRC20",
  "send": {
    "amount": "1700.00",
    "currency": "AZN"
  },
  "receive": {
    "amount": "969.252507",
    "currency": "USDT"
  },
  "rate": "1.73460000",
  "fees": {
    "network": {
      "amount": "1.000000",
      "currency": "USDT"
    },
    "platformRate": "0.010000"
  },
  "expiresAt": "2026-10-15T09:35:58.410Z"
}

Response fields

idstring
Quote id (UUID) for POST /v1/orders.
sendMoney
What you send: AZN (buy) or USDT (sell).
receiveMoney
What you get after fees.
ratestring
AZN per 1 USDT, locked.
fees.networkMoney
Network fee, in USDT.
fees.platformRatestring
Platform fee as a fraction.
expiresAtstring
5 minutes after the quote was created.

Errors

422 UNSUPPORTED_NETWORK422 AMOUNT_OUT_OF_RANGE503 RATE_UNAVAILABLEand the common errors

POST/v1/ordersSigned

Create an order

Places an order from your quote. The reply is the order with its paymentInstructions; pay before paymentDeadline (2 hours).

Idempotent on clientOrderId: the same id with the same quote returns the existing order with 200 OK and Idempotent-Replayed: true (see Idempotency).

Body

quoteIdstringRequired
Id of your quote that has not expired or been used.
clientOrderIdstringRequired
Your unique id for the order: 1–64 characters of A–Z a–z 0–9 . _ : -.
walletAddressstringbuy only
Buy: required. Your USDT address on the quote's network, where 2Pay sends the USDT; at most 128 characters; checked (see Address validation). Sell: leave it out.

Request

POST /v1/orders HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>
Content-Type: application/json

{
  "quoteId": "3c8f5e2a-91d4-4b6e-a7f0-5d2e8c1b9a46",
  "clientOrderId": "inv-20261015-0042",
  "walletAddress": "<your TRC20 address>"
}

Response · 201 Created

{
  "id": "6f1d2c8e-4b7a-4e0f-9a51-3c2d8e7f1a90",
  "reference": "2P-7KQ4M9XD",
  "clientOrderId": "inv-20261015-0042",
  "side": "buy",
  "status": "pending_payment",
  "network": "TRC20",
  "walletAddress": "<your TRC20 address>",
  "send": {
    "amount": "1700.00",
    "currency": "AZN"
  },
  "receive": {
    "amount": "969.252507",
    "currency": "USDT"
  },
  "rate": "1.73460000",
  "fees": {
    "network": {
      "amount": "1.000000",
      "currency": "USDT"
    },
    "platformRate": "0.010000"
  },
  "paymentInstructions": {
    "method": "bank_transfer",
    "amount": {
      "amount": "1700.00",
      "currency": "AZN"
    },
    "bankAccount": "<2Pay bank account details>",
    "reference": "2P-7KQ4M9XD"
  },
  "paymentReference": null,
  "depositTxHash": null,
  "payoutTxHash": null,
  "rejectionReason": null,
  "paymentDeadline": "2026-10-15T11:31:05.120Z",
  "createdAt": "2026-10-15T09:31:05.120Z",
  "updatedAt": "2026-10-15T09:31:05.120Z"
}

Errors

404 QUOTE_NOT_FOUND409 QUOTE_EXPIRED409 QUOTE_ALREADY_USED409 IDEMPOTENCY_CONFLICT409 AMOUNT_BUSY422 INVALID_ADDRESS422 LIMIT_EXCEEDED422 AMOUNT_OUT_OF_RANGE422 UNSUPPORTED_NETWORK503 RATE_UNAVAILABLEand the common errors

GET/v1/ordersSigned

List orders

Your orders, newest first, a page at a time. Pass nextCursor as cursor for the next page; it is null on the last one.

Query parameters

statusstringOptional
Only orders with this status (see Order lifecycle).
limitintegerOptional
1–100, default 50.
cursorstringOptional
nextCursor of the previous page.

Request

GET /v1/orders?status=payment_submitted&limit=20 HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>

Response · 200 OK

{
  "data": [
    {
      "id": "b2e4a7c1-0d9f-4e3a-8c6b-1f5d7e9a2c48",
      "reference": "2P-H3TZ8RWE",
      "clientOrderId": "payout-20261015-0007",
      "side": "sell",
      "status": "payment_submitted",
      "network": "TRC20",
      "walletAddress": null,
      "send": {
        "amount": "1000.370000",
        "currency": "USDT"
      },
      "receive": {
        "amount": "1680.35",
        "currency": "AZN"
      },
      "rate": "1.69840000",
      "fees": {
        "network": {
          "amount": "1.000000",
          "currency": "USDT"
        },
        "platformRate": "0.010000"
      },
      "paymentInstructions": {
        "method": "crypto_deposit",
        "amount": {
          "amount": "1000.370000",
          "currency": "USDT"
        },
        "network": "TRC20",
        "address": "<2Pay TRC20 deposit address>"
      },
      "paymentReference": null,
      "depositTxHash": "9c1e0b5a7d3f42e6b8a1c4d7e2f05b9a3c6d8e1f4a7b0c3d6e9f2a5b8c1d4e7f",
      "payoutTxHash": null,
      "rejectionReason": null,
      "paymentDeadline": "2026-10-15T12:02:40.300Z",
      "createdAt": "2026-10-15T10:02:40.300Z",
      "updatedAt": "2026-10-15T10:09:12.870Z"
    }
  ],
  "nextCursor": "MjAyNi0xMC0xNVQwOTozMTowNS4xMlp8NmYxZDJjOGUtNGI3YS00ZTBmLTlhNTEtM2MyZDhlN2YxYTkw"
}

Response fields

dataOrder[]
Orders (see the order object).
nextCursorstring | null
Cursor of the next page.
GET/v1/orders/{id}Signed

Get an order

One order by 2Pay's id or your clientOrderId. An unpaid order past its paymentDeadline comes back expired.

Path parameters

idstringRequired
2Pay's order id (UUID) or your clientOrderId.

Request

GET /v1/orders/inv-20261015-0042 HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>

Response · 200 OK

{
  "id": "6f1d2c8e-4b7a-4e0f-9a51-3c2d8e7f1a90",
  "reference": "2P-7KQ4M9XD",
  "clientOrderId": "inv-20261015-0042",
  "side": "buy",
  "status": "pending_payment",
  "network": "TRC20",
  "walletAddress": "<your TRC20 address>",
  "send": {
    "amount": "1700.00",
    "currency": "AZN"
  },
  "receive": {
    "amount": "969.252507",
    "currency": "USDT"
  },
  "rate": "1.73460000",
  "fees": {
    "network": {
      "amount": "1.000000",
      "currency": "USDT"
    },
    "platformRate": "0.010000"
  },
  "paymentInstructions": {
    "method": "bank_transfer",
    "amount": {
      "amount": "1700.00",
      "currency": "AZN"
    },
    "bankAccount": "<2Pay bank account details>",
    "reference": "2P-7KQ4M9XD"
  },
  "paymentReference": null,
  "depositTxHash": null,
  "payoutTxHash": null,
  "rejectionReason": null,
  "paymentDeadline": "2026-10-15T11:31:05.120Z",
  "createdAt": "2026-10-15T09:31:05.120Z",
  "updatedAt": "2026-10-15T09:31:05.120Z"
}

Errors

404 ORDER_NOT_FOUNDand the common errors

POST/v1/orders/{id}/paymentSigned

Report a payment

Tells 2Pay you paid: the bank transfer reference (buy) or the USDT transaction hash (sell). The order becomes payment_submitted and a 2Pay operator checks it.

Path parameters

idstringRequired
2Pay's order id (UUID) or your clientOrderId.

Body

referencestringbuy only
Your bank's reference for the AZN transfer: 3–100 characters of letters, digits, spaces and . _ : / # -.
txHashstringsell only
Hash of your USDT transaction to the deposit address, in the form of the quote's network: TRC20 64 hex characters without 0x; BEP20, ERC20, ARBITRUM, AVAXC and MANTLE 0x and 64 hex characters; SOL the base58 signature (86–88 characters); TON 64 hex characters or 44 in base64. Another network's hash, a wallet address or a link gets 400 VALIDATION_ERROR. Hex hashes are compared without case.

Request

POST /v1/orders/inv-20261015-0042/payment HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>
Content-Type: application/json

{
  "reference": "FT26288QX7Z1"
}

Response · 200 OK

{
  "id": "6f1d2c8e-4b7a-4e0f-9a51-3c2d8e7f1a90",
  "reference": "2P-7KQ4M9XD",
  "clientOrderId": "inv-20261015-0042",
  "side": "buy",
  "status": "payment_submitted",
  "network": "TRC20",
  "walletAddress": "<your TRC20 address>",
  "send": {
    "amount": "1700.00",
    "currency": "AZN"
  },
  "receive": {
    "amount": "969.252507",
    "currency": "USDT"
  },
  "rate": "1.73460000",
  "fees": {
    "network": {
      "amount": "1.000000",
      "currency": "USDT"
    },
    "platformRate": "0.010000"
  },
  "paymentInstructions": {
    "method": "bank_transfer",
    "amount": {
      "amount": "1700.00",
      "currency": "AZN"
    },
    "bankAccount": "<2Pay bank account details>",
    "reference": "2P-7KQ4M9XD"
  },
  "paymentReference": "FT26288QX7Z1",
  "depositTxHash": null,
  "payoutTxHash": null,
  "rejectionReason": null,
  "paymentDeadline": "2026-10-15T11:31:05.120Z",
  "createdAt": "2026-10-15T09:31:05.120Z",
  "updatedAt": "2026-10-15T09:44:20.016Z"
}

Errors

404 ORDER_NOT_FOUND409 ORDER_EXPIRED409 ORDER_NOT_AWAITING_PAYMENT409 TX_ALREADY_USEDand the common errors

POST/v1/orders/{id}/cancelSigned

Cancel an order

Cancels an order that is still pending_payment. Send no body (sign the hash of the empty string). Do not cancel after you paid: report the payment instead.

Path parameters

idstringRequired
2Pay's order id (UUID) or your clientOrderId.

Request

POST /v1/orders/inv-20261015-0042/cancel HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>

Response · 200 OK

{
  "id": "6f1d2c8e-4b7a-4e0f-9a51-3c2d8e7f1a90",
  "reference": "2P-7KQ4M9XD",
  "clientOrderId": "inv-20261015-0042",
  "side": "buy",
  "status": "cancelled",
  "network": "TRC20",
  "walletAddress": "<your TRC20 address>",
  "send": {
    "amount": "1700.00",
    "currency": "AZN"
  },
  "receive": {
    "amount": "969.252507",
    "currency": "USDT"
  },
  "rate": "1.73460000",
  "fees": {
    "network": {
      "amount": "1.000000",
      "currency": "USDT"
    },
    "platformRate": "0.010000"
  },
  "paymentInstructions": {
    "method": "bank_transfer",
    "amount": {
      "amount": "1700.00",
      "currency": "AZN"
    },
    "bankAccount": "<2Pay bank account details>",
    "reference": "2P-7KQ4M9XD"
  },
  "paymentReference": null,
  "depositTxHash": null,
  "payoutTxHash": null,
  "rejectionReason": null,
  "paymentDeadline": "2026-10-15T11:31:05.120Z",
  "createdAt": "2026-10-15T09:31:05.120Z",
  "updatedAt": "2026-10-15T09:40:02.550Z"
}

Errors

404 ORDER_NOT_FOUND409 ORDER_NOT_CANCELLABLEand the common errors

GET/v1/accountSigned

Your account

Your company as 2Pay set it up: limits, webhook URL, payout account and API keys. A good first call to check your signing.

Request

GET /v1/account HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>

Response · 200 OK

{
  "id": "0b7c6a2e-5d1f-4c3b-8e9a-2f4d6b8c0a1e",
  "name": "Example Bank OJSC",
  "status": "active",
  "payoutAccount": "<your AZN bank account>",
  "maxOrderAzn": "100000.00",
  "dailyLimitAzn": "500000.00",
  "webhookUrl": "https://example.com/webhooks/2pay",
  "createdAt": "2026-10-13T08:00:00.000Z",
  "kind": "bank",
  "taxId": "1234567890",
  "contactEmail": "treasury@example.com",
  "keys": [
    {
      "id": "tp_live_ed698ee2317701eaaccfacf9",
      "label": "Production",
      "allowedIps": [
        "203.0.113.10/32"
      ],
      "createdAt": "2026-10-13T08:05:00.000Z",
      "lastUsedAt": "2026-10-15T09:31:05.000Z",
      "revokedAt": null
    }
  ]
}

Response fields

statusstring
active (a suspended account gets 403 FORBIDDEN on every call).
payoutAccountstring | null
Bank account AZN is paid to on sell orders. A new one takes effect after a second 2Pay staff member approves it; until then this shows the account in use (null while a first one waits).
maxOrderAznstring
Maximum AZN value of one order.
dailyLimitAznstring
Maximum AZN value of your orders per UTC day.
webhookUrlstring | null
Where events are sent.
kindstring | null
bank, fintech, payment_provider, crypto_business or other.
taxIdstring | null
Company number (VÖEN or another country's).
contactEmailstring | null
Who 2Pay writes to about the account.
keys[].allowedIpsstring[]
IP ranges allowed for the key, as CIDR. Empty: any address.
keys[].lastUsedAtstring | null
Last signed call with the key (updated at most once a minute).
keys[].revokedAtstring | null
When the key was revoked; it no longer works.
POST/v1/webhooks/testSigned

Send a test webhook

Sends a signed ping event ({"id":"evt_test_…","type":"ping","createdAt":"…","data":{}}) to your webhook URL right away, once, without retries, and tells you how it went. Send no body.

Request

POST /v1/webhooks/test HTTP/1.1
Host: api.2pay.az
X-Api-Key: tp_live_ed698ee2317701eaaccfacf9
X-Timestamp: 1792056665120
X-Signature: <hex HMAC-SHA256 of the canonical string>

Response · 200 OK

{
  "delivered": true,
  "status": 204,
  "error": null
}

Response fields

deliveredboolean
Your endpoint answered 2xx.
statusinteger | null
Its HTTP status, if it answered.
errorstring | null
Why it failed, e.g. No webhook URL is configured for this account.
GET/v1/openapi.yamlNo signature

OpenAPI document

This API as an OpenAPI 3.1 document in YAML, for code generators and API tools. No signature needed; not rate limited; cached for 5 minutes.

Request

GET /v1/openapi.yaml HTTP/1.1
Host: api.2pay.az

Response · 200 OK · application/yaml

openapi: 3.1.0
info:
  title: 2Pay API
  …

Webhooks

2Pay POSTs an event to your webhook URL when one of your orders is created, every time its status changes, and when a sell order's deposit address is paused or resumed. 2Pay sets the URL (https only) on your account; the signing secret (whsec_…) is shown once.

Event

{
  "id": "evt_1842",
  "type": "order.updated",
  "createdAt": "2026-10-15T09:44:20.016Z",
  "data": {
    "status": "payment_submitted",
    "order": {
      "id": "6f1d2c8e-4b7a-4e0f-9a51-3c2d8e7f1a90",
      "reference": "2P-7KQ4M9XD",
      "clientOrderId": "inv-20261015-0042",
      "side": "buy",
      "status": "payment_submitted",
      "network": "TRC20",
      "walletAddress": "<your TRC20 address>",
      "send": {
        "amount": "1700.00",
        "currency": "AZN"
      },
      "receive": {
        "amount": "969.252507",
        "currency": "USDT"
      },
      "rate": "1.73460000",
      "fees": {
        "network": {
          "amount": "1.000000",
          "currency": "USDT"
        },
        "platformRate": "0.010000"
      },
      "paymentInstructions": {
        "method": "bank_transfer",
        "amount": {
          "amount": "1700.00",
          "currency": "AZN"
        },
        "bankAccount": "<2Pay bank account details>",
        "reference": "2P-7KQ4M9XD"
      },
      "paymentReference": "FT26288QX7Z1",
      "depositTxHash": null,
      "payoutTxHash": null,
      "rejectionReason": null,
      "paymentDeadline": "2026-10-15T11:31:05.120Z",
      "createdAt": "2026-10-15T09:31:05.120Z",
      "updatedAt": "2026-10-15T09:44:20.016Z"
    }
  }
}

Event fields

idstring
evt_…, the same on every attempt. Deduplicate on it.
typestring
order.updated; ping for test events.
createdAtstring
When the status changed.
data.statusstring
The status this event reports.
data.orderOrder
The order as 2Pay read it when it queued the event, seconds after the change; if the order moved on in between, it can already show a later status. Retries send the same body.
data.changestring
Only on a sell order's pause events: deposit_paused or deposit_resumed. data.status is then the order's current status.

Headers

Content-Typestring
application/json
User-Agentstring
2Pay-Webhooks/1
X-2Pay-Event-Idstring
The event id, as in the body.
X-2Pay-Timestampstring
Unix time in milliseconds when this attempt was signed.
X-2Pay-Signaturestring
Hex HMAC-SHA256 of "<X-2Pay-Timestamp>.<raw body>", keyed with your webhook secret.

Verify every delivery

  1. Read the raw body before you parse it.
  2. Compute HMAC-SHA256 of the timestamp, a dot and the raw body with your webhook secret, in hex.
  3. Compare it with X-2Pay-Signature in constant time, after checking the lengths match (some constant-time functions throw on different lengths).
  4. Reject timestamps more than 5 minutes from your clock.
  5. Answer 2xx within 10 seconds, then do the work.

Verifying a webhook · curl

# Check a saved delivery with OpenSSL: body.json holds the raw body exactly as received.
TIMESTAMP=1792056665120  # the X-2Pay-Timestamp header
SIGNATURE=0c5f…          # the X-2Pay-Signature header

expected=$({ printf '%s.' "$TIMESTAMP"; cat body.json; } \
  | openssl dgst -sha256 -hmac "$TWOPAY_WEBHOOK_SECRET" -r | cut -d' ' -f1)
if [ "$expected" = "$(printf '%s' "$SIGNATURE" | tr 'A-F' 'a-f')" ]; then echo valid; else echo invalid; fi

# Ask 2Pay to send a signed "ping" event to your webhook URL now.
twopay POST /v1/webhooks/test

Delivery and retries

  • A 2xx answer counts as delivered. Any other status, a timeout (10 seconds) or a connection error is a failure. Redirects are not followed: a 3xx is a failure.
  • A failed delivery is retried after the waits below: 10 retries after the first try, about 28 hours in all. After the last one it is marked failed and not sent again; use GET /v1/orders to catch up.
  • Each attempt is signed again: X-2Pay-Timestamp and X-2Pay-Signature change, the event id and body do not.
  • Events can arrive more than once and out of order. Deduplicate on the id and compare data.order.updatedAt, or read the order with GET /v1/orders/{id}.
  • Events are queued only while your account has a webhook URL; changes made while it has none are not sent later.
  • While your account is suspended, deliveries pause; they resume when it is active again.
  • POST /v1/webhooks/test sends a signed ping right away to check your endpoint.
RetryWaitSince the first try
0130 s0:00:30
021 min0:01:30
035 min0:06:30
0415 min0:21:30
0530 min0:51:30
061 h1:51:30
072 h3:51:30
084 h7:51:30
098 h15:51:30
1012 h27:51:30

Address validation

For a buy order 2Pay checks walletAddress before it creates the order. A failed check returns 422 INVALID_ADDRESS with a message that says what is wrong, and no order is made. An address over 128 characters returns 400 VALIDATION_ERROR.

  • Format for the quote's network: TRON base58check, EVM 0x and 40 hex characters, Solana base58 public key, TON user-friendly or raw form.
  • Checksum: EIP-55 for mixed-case EVM addresses (all lower or all upper case passes), base58check for TRON, CRC16 for TON. A typo fails here.
  • Network: an address of another chain family is refused, e.g. a Solana address on TRC20, a TRON address on SOL, an EVM address on TON or a Bitcoin address anywhere.
  • TON testnet and masterchain addresses are refused.
  • Token contracts and system addresses are refused, e.g. the USDT contracts themselves, the zero address or the Solana token programs.
  • 2Pay's own deposit addresses are refused.
  • On the blockchain these are refused: a TRON smart contract; a Solana program, token mint, data account, program-derived address with no account yet or token account (the message names the wallet that owns it); a TON jetton, NFT, SBT or DNS contract and a frozen TON account; an EVM smart-contract wallet that exists on another EVM network but not on this one.
  • A wallet address on which Tether has frozen USDT. 2Pay checks it again before it sends; if Tether froze it after the order was placed, 2Pay does not send the USDT: an operator rejects the order with a reason that says so, and 2Pay refunds the AZN by bank transfer, outside the API. Neither happens by itself.

A new address without transactions, a smart-contract wallet and an EVM address active on another network are accepted: they can receive USDT, but make sure they are right. On EVM networks and TON, any smart contract not refused above counts as a smart-contract wallet. When the blockchain cannot be reached, the offline checks decide.

CheckMessage (English)
EmptyEnter a wallet address
Link, email or nameEnter the wallet address itself, not a link, email or name
FormatThis is not a valid TRON (TRC20) address
ChecksumThis TRON (TRC20) address has a typo (its checksum doesn't match). Copy it again from your wallet.
Wrong networkThis is a Solana address, but you chose TRON (TRC20). Choose the matching network or enter your TRON (TRC20) address.
TON testnetThis is a TON testnet address. Enter your mainnet address.
TON masterchainThis TON address is on the masterchain. Enter your wallet's regular address.
Token contract or system addressThis is a token contract or system address, not a wallet. Enter your own wallet address.
2Pay deposit addressThis is a 2Pay deposit address. Enter your own wallet address.
Contract or program (on-chain)This address belongs to a smart contract or program, not a wallet.
Contract wallet missing on this network (on-chain)This smart-contract wallet does not exist on Ethereum (ERC20).
Solana token account (on-chain)This is a USDT token account, not a wallet. Enter the wallet that owns it: <owner address>
Frozen TON account (on-chain)This TON account is frozen and can't receive USDT.
Frozen by Tether (on-chain)Tether has frozen USDT on this address, so 2Pay won't send USDT to it. Enter another wallet address.

Error · 422

{
  "error": {
    "code": "INVALID_ADDRESS",
    "message": "This is a Solana address, but you chose TRON (TRC20). Choose the matching network or enter your TRON (TRC20) address."
  }
}

Errors

Every error is {"error": {"code": "…", "message": "…"}} with the HTTP status below. Retry 429, 500 and 503 with backoff and a new signature; do not retry other 4xx unchanged.

CodeHTTPMeaning
VALIDATION_ERROR400A field is missing or invalid; the message starts with the field name (amount: …). Also: a body that is not JSON or is over 64 KB, walletAddress on a sell order, a sell order without a registered payout account (a new payout account takes effect once a second 2Pay staff member approves it).
UNAUTHENTICATED401X-Api-Key, X-Timestamp or X-Signature is missing.
INVALID_SIGNATURE401Unknown or revoked key, wrong signature, a timestamp that is not in milliseconds or is more than 5 minutes off, or a signature that was already used.
FORBIDDEN403The caller's IP address is not on the key's allowlist, or your account is suspended.
NOT_FOUND404, 405No such path (404), or the path exists with another method (405).
QUOTE_NOT_FOUND404No quote with this id belongs to your account.
ORDER_NOT_FOUND404No order with this id or clientOrderId belongs to your account.
QUOTE_EXPIRED409The quote is older than 5 minutes. Ask for a new one.
QUOTE_ALREADY_USED409The quote already backs another order.
IDEMPOTENCY_CONFLICT409The clientOrderId was already used with a different quote.
ORDER_EXPIRED409The payment window ended; the order can no longer be paid.
ORDER_NOT_AWAITING_PAYMENT409A payment was already reported, or the order is final.
ORDER_NOT_CANCELLABLE409Only pending_payment orders can be cancelled.
TX_ALREADY_USED409The transaction hash was already reported for another order that was not rejected.
AMOUNT_BUSY409Sell: every USDT amount near the quote's is taken by another open sale on the network. Ask for a slightly different amount.
AMOUNT_OUT_OF_RANGE422The order value is under 10 AZN or over your per-order maximum, or too small to cover the fees.
LIMIT_EXCEEDED422The order would take you over your daily limit (UTC day).
UNSUPPORTED_NETWORK422Unknown network id, or buying or selling on a network that is not offered right now, selling on a paused network included (see buyNetworks and sellNetworks).
INVALID_ADDRESS422walletAddress failed a check; the message says which (see Address validation).
RATE_LIMITED429Too many requests per key or per IP address. Wait for the next minute.
INTERNAL_ERROR500Unexpected error on 2Pay's side. Retry with backoff and a fresh signature.
RATE_UNAVAILABLE503No live rate right now. Retry shortly.
SERVICE_UNAVAILABLE503A 2Pay service is temporarily unavailable. Retry shortly.

Security

  • API keys: the key id (tp_live_…) names the key; the secret (tps_…) signs requests. 2Pay shows the secret once and stores it encrypted (AES-256-GCM). Keep it on your servers, in a secrets manager: never in a browser, a mobile app, logs or source control.
  • IP allowlist: a key can be limited to IP addresses or CIDR ranges (IPv4 and IPv6, up to 50 entries). Calls from other addresses get 403 FORBIDDEN. A key without a list accepts any address, so ask 2Pay to set one.
  • Rotation: an account can have several active keys. To rotate, ask 2Pay for a new key, switch to it, then ask 2Pay to revoke the old one; a revoked key stops working on the next request. Do this at once if a secret may have leaked.
  • Replay protection: signatures are single-use and timestamps must be within 5 minutes.
  • Webhook secret: whsec_…, shown once and stored encrypted. 2Pay can replace it on request; the new secret signs the next delivery, so update your endpoint promptly. Verify every webhook and reject unsigned or stale ones.
  • Suspension: 2Pay can suspend an account; its calls then get 403 FORBIDDEN and webhooks pause.
  • Transport: HTTPS only, for the API and for webhook URLs.

Networks

USDT is the only asset, on the networks below. You can buy on the networks in buyNetworks and sell on those in sellNetworks (GET /v1/rates); 2Pay can turn a network off for a while. Network fees differ per network: see networkFees. 2Pay also pauses selling on a network while Tether has frozen its deposit address, or may have while 2Pay confirms it: the network leaves sellNetworks, an open sell order on it gets paymentInstructions.paused: true and a webhook with data.change: "deposit_paused"; do not send USDT to its address.

IdNetworkAddress
TRC20TRON (TRC20)Base58, 34 characters starting with T
BEP20BNB Chain (BEP20)0x and 40 hex characters (EVM)
SOLSolanaBase58 public key, 32–44 characters
ARBITRUMArbitrum One0x and 40 hex characters (EVM)
AVAXCAvalanche C-Chain0x and 40 hex characters (EVM)
TONTONUser-friendly (EQ…, UQ…) or raw (0:…)
ERC20Ethereum (ERC20)0x and 40 hex characters (EVM)
MANTLEMantle0x and 40 hex characters (EVM)

Changelog

    • Buy orders: walletAddress is refused when Tether has frozen USDT on it. If Tether freezes it after the order is placed, 2Pay does not send the USDT: an operator rejects the order with a reason, and 2Pay refunds the AZN by bank transfer, outside the API; neither happens by itself, and no status or webhook follows the refund. Selling on a network is paused while Tether has frozen 2Pay's deposit address there, or may have while 2Pay confirms it (sellNetworks, paymentInstructions.paused, webhook data.change).
    • Sell orders ask for their own USDT amount: the quote's cut to whole cents, plus 0.01–0.99 USDT (minus, near the top of your limits), so less than 1 USDT from the quote's. The order's send, receive and paymentInstructions.amount show it: send exactly paymentInstructions.amount. POST /v1/orders can answer 409 AMOUNT_BUSY, and for a sell 422 AMOUNT_OUT_OF_RANGE when your limits changed since the quote.
    • An order of 20,000 AZN or more is approved by two different 2Pay staff members, so it may take a little longer. Nothing changes in the API: the order stays payment_submitted until the second approval, its updatedAt does not move in between, and no webhook is sent before it is decided.
    • GET /v1/rates also returns buyNetworks. 2Pay can turn a network off for buying or selling for a while: quotes and orders on it get UNSUPPORTED_NETWORK, also for a quote made before.
    • walletAddress is checked more strictly: checksums, the network the address belongs to, TON testnet and masterchain addresses, token contracts, 2Pay's own deposit addresses and an on-chain lookup (INVALID_ADDRESS).
    • GET /v1/account also returns kind, taxId and contactEmail.
    • v1 launch: rates, quotes, orders, payments, cancellation, account, webhooks.