1. Introduction
The UnGateway API lets you accept cryptocurrency payments, read your balances, convert holdings into stablecoins, and receive webhooks for payment events. It is a typed REST API that returns predictable JSON.
The base URL for all requests is:
https://api.ungateway.ccAll monetary amounts are decimal strings, never numbers — precision matters for money, so "50.00" is sent and returned as a string.
2. Authentication
Authenticated endpoints require your secret API key. Generate and manage keys in the developer dashboard. Only a hash of each key is stored — keep the secret safe, and treat it like a password.
Send it on either header:
Authorization: Bearer <API_KEY>
# or
X-API-Key: <API_KEY>| Endpoint | Auth |
|---|---|
GET /health | none |
GET /v1/currencies | none |
POST /v1/payments | required |
GET /v1/payments/{id} | required |
GET /v1/balances | required |
POST /v1/conversions | required |
GET /v1/conversions/{id} | required |
POST /v1/payouts | required |
GET /v1/payouts/{id} | required |
Errors use standard status codes: 401 (missing/invalid key), 400 (bad request), 404 (not found), 429 (rate limit or too many pending).
3. List currencies
/v1/currenciesno authLists the currencies you can transact in (active and backed by a configured chain). No auth required. Use each id as the pay_asset_id when creating a payment.
curl https://api.ungateway.cc/v1/currenciesResponse — 200
[
{
"id": "eth",
"symbol": "ETH",
"name": "Ethereum",
"chain": "ethereum",
"decimals": 18,
"min_confirmations": 12,
"image": "https://storage.ungateway.cc/currencies/eth.svg"
},
{
"id": "usdt-trc20",
"symbol": "USDT",
"name": "Tether (TRC-20)",
"chain": "tron",
"decimals": 6,
"min_confirmations": 19,
"image": "https://storage.ungateway.cc/currencies/usdt-trc20.svg"
}
]| Field | Type | Description |
|---|---|---|
id | string | Use as pay_asset_id. |
symbol | string | Ticker (e.g. ETH). |
name | string | Human name. |
chain | string | Underlying chain (ethereum, tron, bitcoin, …). |
decimals | int | Base-unit decimals for the asset. |
min_confirmations | int | Confirmations before a deposit counts as paid. |
image | string | URL of the currency logo (SVG). Use it to display the coin in your checkout. |
4. Create a payment
/v1/paymentsauth requiredPrices a USD amount into crypto and allocates a single-use deposit address. Redirect the buyer to the returned payment_url for the hosted checkout, or build your own UI from address and pay_amount (and tag if present).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
pay_asset_id | string | yes | Asset the buyer pays in (an id from /v1/currencies). |
amount_usd | string (decimal) | yes | Base amount you want, in USD, before fees. Must be > 0. |
merchant_order_id | string | no | Your reference. Idempotent: repeating it returns the existing payment. |
fallback_url | string | no | An http(s) URL the buyer returns to after paying. |
Auto-exchange, settlement asset, and commission are account settings configured by the operator on your merchant record — they are not request fields.
Example request
curl -X POST https://api.ungateway.cc/v1/payments \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pay_asset_id": "usdt-trc20",
"amount_usd": "50.00",
"merchant_order_id": "order-9001"
}'Response — 201 Created
{
"id": "b6f1c2de-1111-2222-3333-444455556666",
"status": "created",
"payment_url": "https://www.ungateway.cc/payments/b6f1c2de-1111-2222-3333-444455556666",
"pay_asset_id": "usdt-trc20",
"pay_amount": "53.000000",
"address": "TXk...9aB",
"tag": null,
"expires_at": "2026-06-18T12:34:56Z",
"price": {
"amount": "50.00",
"currency": "USD",
"total": "53.00",
"rate": "1.00"
},
"fees": {
"base": "50.00",
"platform_fee": "0.50",
"merchant_commission": "0",
"network_fee": "3.00"
},
"created_at": "2026-06-18T11:34:56Z"
}| Field | Type | Description |
|---|---|---|
id | uuid | Payment id (use with GET /v1/payments/{id} and in webhooks). |
status | string | Lifecycle status. |
payment_url | string | Hosted checkout page for this payment — redirect the buyer here instead of building your own payment UI. |
pay_amount | string | Exact amount the buyer must send, in the pay asset. |
address | string | Single-use deposit address to show the buyer. |
tag | string | null | Memo/destination tag, if the asset needs one. |
expires_at | datetime | null | Pay before this or the payment expires. |
price.total | string | Total USD the buyer pays (base + fees). |
fees.* | string | USD breakdown: base, platform_fee, merchant_commission, network_fee. |
Errors
| Code | When |
|---|---|
400 | Unknown/inactive pay_asset_id; amount below the minimum; unsupported chain; invalid fallback_url. |
401 | Missing/invalid API key. |
429 | Rate limit exceeded, or too many pending payments. |
5. Get a payment
/v1/payments/{id}auth requiredFetch a payment's current state. Same body shape as the create response, with status and the received amount reflected. Poll this, or rely on webhooks (recommended). Returns 404 if the id doesn't exist for your merchant.
curl https://api.ungateway.cc/v1/payments/b6f1c2de-1111-2222-3333-444455556666 \
-H "Authorization: Bearer $API_KEY"6. Payment statuses
A payment moves through these statuses. Webhooks fire only on the meaningful transitions.
| Status | Meaning |
|---|---|
created | Address assigned, awaiting funds. |
detected | A deposit was seen (unconfirmed). |
confirming | On-chain, below min_confirmations. |
paid | Confirmed, full expected amount. |
overpaid | Confirmed, more than expected. |
underpaid | Confirmed, less than expected. |
exchanging | Routed to auto-exchange. |
completed | Net amount credited to balance. |
expired | No payment before expires_at. |
failed / cancelled | Terminal failure / cancelled. |
7. Get balances
/v1/balancesauth requiredYour current balances, aggregated by currency symbol. Chain variants of the same currency (e.g. USDT on Tron, Ethereum, Solana) are pooled into one line. Only non-zero balances are returned.
curl https://api.ungateway.cc/v1/balances \
-H "Authorization: Bearer $API_KEY"Response — 200
{
"balances": [
{ "asset": "BTC", "balance": "0.031000000000000000" },
{ "asset": "USDT", "balance": "1250.500000000000000000" }
]
}8. Create a conversion
/v1/conversionsauth requiredConvert a balance you already hold into a stablecoin (USDT or USDC), charged a 0.5% platform fee. The request is queued: it reserves the balance and returns status: "pending". Poll GET /v1/conversions/{id} for the outcome.
v1 supports converting into USDT/USDC only (selling a volatile asset into a stablecoin).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from | string | yes | Currency symbol to convert from (e.g. BTC). Case-insensitive. |
to | string | yes | Stablecoin symbol to convert to: USDT or USDC. |
amount | string (decimal) | yes | Amount of from to convert. Must be > 0. |
Assets are named by symbol, not chain-specific id. Constraints: minimum value amount × rate ≥ $5; max 10 pending conversions per merchant; from and to must differ.
Example request
curl -X POST https://api.ungateway.cc/v1/conversions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "from": "BTC", "to": "USDT", "amount": "0.05" }'Response — 201 Created
{
"id": "c1d2e3f4-aaaa-bbbb-cccc-000011112222",
"status": "pending",
"from": "BTC",
"amount": "0.05",
"to": "USDT",
"to_amount": null,
"platform_fee": null,
"rate": null,
"created_at": "2026-07-13T10:00:00Z",
"completed_at": null
}to_amount, platform_fee, and rate are null until the conversion fills. Your net credit is to_amount − platform_fee.
9. Get a conversion
/v1/conversions/{id}auth requiredFetch a conversion's current state. Same shape as the create response, with the filled amounts reflected once complete. Returns 404if the id doesn't exist for your merchant.
curl https://api.ungateway.cc/v1/conversions/c1d2e3f4-aaaa-bbbb-cccc-000011112222 \
-H "Authorization: Bearer $API_KEY"| Status | Meaning |
|---|---|
pending | Reserved and queued; awaiting execution on the exchange. |
filled | Sold and credited; net amount added to your balance. |
failed | Terminal failure; reserved amount returned unless it may have executed. |
10. Create a payout
/v1/payoutsauth requiredWithdraw a held balance to your verified payout address for an asset. The destination is not supplied in the request. It's the address you have on file for the asset, and it must be verified. The request reserves the balance (debiting your ledger immediately) and queues the withdrawal.
Payout addresses are chain-specific, so payouts are named by the concrete asset_id (e.g. usdt-trc20), not a symbol. There is one payout address per merchant and asset.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
asset_id | string | yes | Concrete asset id to pay out (e.g. usdt-trc20, usdc-erc20). |
amount | string (decimal) | yes | Gross amount to withdraw, in the asset, before the payout fee. Must be > 0. |
Example request
curl -X POST https://api.ungateway.cc/v1/payouts \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "asset_id": "usdt-trc20", "amount": "100.00" }'Response — 201 Created
{
"id": "9d0e1f2a-3333-4444-5555-666677778888",
"status": "requested",
"asset_id": "usdt-trc20",
"amount": "100.00",
"fee": "2.25",
"net": "97.75",
"destination_address": "TXk...9aB",
"destination_tag": null,
"txid": null,
"created_at": "2026-07-27T10:00:00Z",
"completed_at": null
}| Field | Type | Description |
|---|---|---|
id | uuid | Payout id (use with GET /v1/payouts/{id}). |
status | string | requested, processing, sent, confirmed, or failed. |
amount | string | Gross amount debited from your balance. |
fee | string | Payout fee deducted from the gross. |
net | string | Amount that will arrive at the destination (amount - fee). |
destination_address | string | Your verified address for this asset. |
destination_tag | string | null | Memo/destination tag, if the chain uses one. |
txid | string | null | On-chain tx hash, once the withdrawal is confirmed. |
Errors
| Code | When |
|---|---|
400 | amount ≤ 0; no payout address on file; address not verified; asset not payable; amount below the fee; net below the exchange minimum; payouts temporarily unavailable. |
401 | Missing/invalid API key. |
429 | Rate limit exceeded. |
The balance debit is atomic with the payout: if your balance can't cover the gross, the payout isn't created.
11. Get a payout
/v1/payouts/{id}auth requiredFetch a payout's current state. Same body shape as the create response, with status, txid, and completed_at reflected as it progresses. Returns 404if the id doesn't exist for your merchant.
curl https://api.ungateway.cc/v1/payouts/9d0e1f2a-3333-4444-5555-666677778888 \
-H "Authorization: Bearer $API_KEY"Payout statuses
| Status | Meaning |
|---|---|
requested | Created; balance reserved, queued for execution. |
processing | Being submitted to the exchange. |
sent | Withdrawal submitted; awaiting on-chain confirmation. |
confirmed | Sent and confirmed on-chain; txid populated. |
failed | Terminal failure; the reserved balance is re-credited to your ledger. |
12. Webhooks
Webhooks push payment status changes to your server so you don't have to poll. Register and manage endpoints in the webhooks dashboard, where a signing secret is generated for you.
Event types
Events are payment.<status>. Only meaningful transitions are sent (intermediate detected/confirming are not):
payment.paid,payment.overpaid,payment.underpaidpayment.completed,payment.expired,payment.failed
Delivery
POST to your URL with a JSON body. 10s timeout; any 2xx = success. On failure, redelivered with backoff (≈30s, 2m, 10m, 1h, 6h), up to 6 attempts. Delivery is at-least-once — make handling idempotent (key on payment_id + status).
Headers
| Header | Description |
|---|---|
X-Ungateway-Event | The event type, e.g. payment.paid. |
X-Ungateway-Signature | sha256=<hex> — HMAC-SHA256 of the raw request body, keyed by your secret. |
X-Ungateway-Timestamp | RFC3339 time the delivery was attempted. |
Payload
{
"event": "payment.paid",
"payment_id": "b6f1c2de-1111-2222-3333-444455556666",
"merchant_order_id": "order-1234",
"status": "paid",
"pay_asset_id": "eth",
"pay_amount": "0.034812",
"received_amount": "0.034812",
"credited_amount": "102.00",
"credited_asset_id": "usdt-trc20",
"address": "0x9858EfFD232B4033E47d90003D41EC34EcaEda94",
"txids": ["0xabc...123"],
"timestamp": "2026-06-18T11:40:02Z"
}credited_amount and credited_asset_id are null until the payment is credited (populated on payment.completed).
Verifying the signature
Compute HMAC-SHA256(secret, raw_body) and compare (constant-time) to the hex in X-Ungateway-Signature after stripping the sha256= prefix. Use the exact raw bytes received — do not re-serialize the JSON first.
const crypto = require("crypto");
function verify(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const got = (signatureHeader || "").replace(/^sha256=/, "");
return got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
// Express: capture the raw body, e.g. app.use(express.raw({ type: "application/json" }))
app.post("/webhooks/ungateway", (req, res) => {
if (!verify(req.body, req.get("X-Ungateway-Signature"), process.env.WHSEC)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
// ... handle idempotently by event.payment_id + event.status ...
res.status(200).end();
});import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
got = (signature_header or "").removeprefix("sha256=")
return hmac.compare_digest(got, expected)13. Health check
/healthno authLiveness probe. No auth. Returns 200 with body ok.
curl https://api.ungateway.cc/health