# Suawave API documentation (full) # Overview Source: https://suawave.com/docs/ Accept crypto payments with one HTTP call. There is no registration, no account and no API key: you send a payout address and a callback URL, you get a permanent deposit address back, and every deposit to it is forwarded to your payout address while your server receives signed callbacks. Base URL: `https://api.suawave.com`. All endpoints use `GET` and return JSON. There is no version prefix in the path. ## How it works 1. Call `GET /{ticker}/create/` with your payout `address` and a `callback` URL. The response carries `address_in`, the deposit address to show to the payer, and `minimum_transaction_coin`, the smallest deposit that is forwarded. 2. The payer sends the asset to `address_in`. 3. The gateway detects the deposit, waits for finality, forwards the amount minus fees to your payout address and calls your callback URL. See [Callbacks](callbacks.md). 4. Your server verifies the `x-ca-signature` header with the public key from `GET /pubkey/`, checks its own order data, deduplicates on `uuid` and answers `*ok*`. See [Verify callback signatures](verify-signature.md). Start with the [Quick start](quickstart.md) for a complete walk-through with commands you can run. ## The order model - The pair of ticker and callback URL identifies an order. The same pair always returns the same `address_in`. Treat the callback URL as your order ID and put your own order reference and an unguessable nonce in its query string. Custom query parameters are echoed back in every callback. - The payout address is frozen for a pair. To change it, use a new callback URL, for example by adding `&v=2`. - `address_in` is permanent. Every deposit of at least the minimum is forwarded on its own, with its own fees, forwarding transaction and callbacks, even when several deposits arrive close together. - Deposits below the minimum are not forwarded, trigger no callback and are not paid out. Always show the minimum to the payer. - Tokens other than the ticker's asset that are sent to the address are not forwarded and trigger no callback. ## Where to go next - [Quick start](quickstart.md): the whole flow in five steps. - [API reference](api-reference.md): every endpoint with parameters, examples and responses. - [Callbacks](callbacks.md): payloads, transports, retries and acknowledgement. - [Verify callback signatures](verify-signature.md): working code in Go, C#, JavaScript and PHP. - [Tickers and networks](tickers.md): which assets you can accept and their minimums. - [Fees and minimums](fees.md): how the amount you receive is computed. - [Price conversion](conversion.md): fiat and crypto conversion and the convert fields in callbacks. - [Errors](errors.md): status codes and what to do about them. ## For AI agents Every page is available as raw markdown under `raw/` (for example `raw/quickstart.md`). `llms.txt` lists the pages and `llms-full.txt` contains all of them in one file. The OpenAPI definition is published as `openapi.yaml`. --- # Quick start Source: https://suawave.com/docs/quickstart.html This walk-through creates a deposit address, shows it to a payer, and handles the callbacks. Replace the payout address and callback URL with your own. Use URL encoding for every query value; `curl -G --data-urlencode` does this for you. ## 1. Check the ticker and its minimum Token tickers contain a slash: `trc20/usdt`. See [Tickers and networks](tickers.md) for the list. ```bash curl "https://api.suawave.com/trc20/usdt/info/" ``` ```json { "status": "success", "coin": "USDT", "ticker": "trc20/usdt", "minimum_transaction_coin": "10.000000" } ``` ## 2. Create the deposit address Generate an unguessable nonce for the order, store it on your server, and put it in the callback URL together with your own order reference. Add `pending=1` if you also want a callback when the deposit is first detected. ```bash curl -G "https://api.suawave.com/trc20/usdt/create/" \ --data-urlencode "address=TXyzPayoutAddressExample0000000000000" \ --data-urlencode "callback=https://shop.example/hook?order=1001&nonce=7f3a9c" \ --data-urlencode "pending=1" ``` ```json { "status": "success", "address_in": "TDepositAddressExample0000000000", "address_out": "TXyzPayoutAddressExample0000000000000", "callback_url": "https://shop.example/hook?order=1001&nonce=7f3a9c", "minimum_transaction_coin": "10.000000" } ``` > [!NOTE] > Calling it again with the same ticker and callback returns the same `address_in`. Use HTTPS for the callback URL; private and loopback destinations are refused. ## 3. Show the address to the payer Display `address_in`, the asset, the network and `minimum_transaction_coin`. A deposit below the minimum is not forwarded and not paid out. To render a QR code, call `GET /{ticker}/qrcode/` with the `address_in`: ```bash curl -G "https://api.suawave.com/trc20/usdt/qrcode/" \ --data-urlencode "address=TDepositAddressExample0000000000" \ --data-urlencode "value=25.5" ``` The response contains `qr_code`, a `data:image/png;base64,` URI you can use as an image source, and the exact `payment_uri` that is encoded. Wallet support for amounts differs by network; see the [API reference](api-reference.md). ## 4. Receive the callbacks When a deposit is detected your callback URL receives a pending callback (only with `pending=1`), and after the deposit was forwarded a confirmed callback. By default callbacks are `GET` requests with the fields in the query string. A confirmed callback looks like this, shown as JSON for readability (`price` is added when a fresh price is available): ```json { "uuid": "8c4e2a61-5b3d-47f9-a0e2-1d6c9b7f3e45", "address_in": "TDepositAddressExample0000000000", "address_out": "TXyzPayoutAddressExample0000000000000", "txid_in": "9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9", "coin": "trc20_usdt", "pending": "0", "txid_out": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809", "confirmations": "21", "value_coin": "100.000000", "value_forwarded_coin": "98.200000", "fee_coin": "1.800000" } ``` In your handler: 1. Verify the `x-ca-signature` header ([Verify callback signatures](verify-signature.md)). 2. Compare the `nonce` and `order` query values with what you stored, and check `address_in`, `coin` and `value_coin` against the order. 3. Save the `uuid` before doing anything else. If you have seen it, skip the work but still answer `*ok*`. 4. Fulfill the order only on a confirmed callback (`pending` is `"0"`). 5. Answer HTTP 200 with the body `*ok*` within 30 seconds. Any other answer is retried. ## 5. Poll the logs as a fallback If a callback did not arrive, read the history of the order: ```bash curl -G "https://api.suawave.com/trc20/usdt/logs/" \ --data-urlencode "callback=https://shop.example/hook?order=1001&nonce=7f3a9c" ``` Logs list each forwarded deposit with its forwarding transaction, fees and the last delivery attempts. Verified callbacks remain the source of truth for accepting a payment; logs are a polling fallback. ## Next - [API reference](api-reference.md) for all parameters and responses. - [Callbacks](callbacks.md) for the retry schedule and transports (`post=1`, `json=1`). - [Fees and minimums](fees.md) to know what you will receive. --- # API reference Source: https://suawave.com/docs/api-reference.html Every endpoint uses `GET`, needs no authentication and answers JSON with `Cache-Control: no-store`. Token tickers contain a slash and go in the path as they are, for example `/trc20/usdt/create/`. Tickers are lowercase and case-sensitive. Amounts are decimal strings in whole asset units, never JSON numbers. Request URLs may be at most 8192 bytes. Parameters that an endpoint does not list are refused. Use HTTPS and URL-encode every query value. Request limits apply per client IP and callback host. Errors use `{"status":"error","error":""}`; see [Errors](errors.md). ## Create and track a deposit address ### Allocate or retrieve a permanent public deposit address `GET /{ticker}/create/` One payout per canonical ticker and callback pair; payout and callback options are immutable. Unknown parameters are rejected. Maximum URL length is 8192 bytes. #### Parameters | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ticker` | path | yes | string | Lowercase registry token ticker including slash, for example trc20/usdt or bep20/usdt. The server accepts both path segments as one ticker; clients must retain the slash. Native-only tickers are not currently resolved. | | `callback` | query | yes | string | URL-encoded public HTTPS callback URL, exactly once. Scheme and host case, trailing host dot, default port and percent-escape case are canonicalized; path and query order are preserved. | | `address` | query | yes | string | Valid single payout address; gateway-controlled addresses are refused. | | `pending` | query | no | string, one of `0`, `1`, default `0` | 1 also sends a detection (pending) callback in addition to the confirmed callback. | | `post` | query | no | string, one of `0`, `1`, default `0` | 1 delivers callbacks as an application/x-www-form-urlencoded POST instead of GET. | | `json` | query | no | string, one of `0`, `1`, default `0` | JSON POST takes precedence over form POST; otherwise GET is used. | | `convert` | query | no | string, one of `0`, `1`, default `0` | 1 adds value_coin_convert and value_forwarded_coin_convert to confirmed callbacks. Refused with 400 when price conversion is disabled. | | `coupon` | query | no | string | Optional fee coupon code, case insensitive. A valid code is pinned to the created address and fixes its fee for every later deposit, so a coupon that ends afterwards never changes a promised fee. Refused with 400 when the code is unknown, outside its validity window, restricted to another ticker, or past its usage limit. Ignored when the ticker and callback pair already exists, which keeps the fee that pair was promised. | #### Example request ```bash curl -G "https://api.suawave.com/trc20/usdt/create/" \ --data-urlencode "callback=https://shop.example/hook?order=1001&nonce=7f3a9c" \ --data-urlencode "address=TXyzPayoutAddressExample0000000000000" ``` #### Response `200` Existing or newly allocated address. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `address_in` | string | yes | Permanent deposit address for this ticker and callback pair. | | `address_out` | string | yes | Payout address bound to the pair. | | `callback_url` | string | yes | Canonicalized callback URL. | | `minimum_transaction_coin` | string | yes | Minimum deposit that is forwarded, in whole asset units. | | `fee_percent` | string | yes | Service fee percentage pinned to this address, as a decimal string (0.50 is 0.5%). | | `fee_fixed_coin` | string | yes | Fixed part of the service fee pinned to this address, in whole asset units. | | `fee_network_coin` | string | yes | Network fee charged on forwarding, in whole asset units. A promotion or coupon never lowers it. | | `fee_promotion` | string | no | Name of the promotion that produced this fee; absent when the standard fee applies. | | `coupon` | string | no | Coupon code pinned to this address; absent when no coupon applies. | ```json { "status": "success", "address_in": "TDepositAddressExample0000000000", "address_out": "TXyzPayoutAddressExample0000000000000", "callback_url": "https://shop.example/hook?order=1001&nonce=7f3a9c", "minimum_transaction_coin": "10.000000", "fee_percent": "0.10", "fee_fixed_coin": "0.100000", "fee_network_coin": "1.000000", "coupon": "LAUNCH10" } ``` #### Errors Statuses `400`, `429`, `503` use the standard error body described in [Errors](errors.md). Notes for `create`: - The canonical pair of ticker and callback identifies the order permanently. Canonicalization lowercases the scheme and host, removes a trailing host dot and a default port, and uppercases percent escapes. Path and query order are kept. - `json=1` takes precedence over `post=1`. Without either, callbacks are delivered as `GET`. - All options are fixed by the first call for a pair; later calls return the original options. - Only one payout address is supported. Split payouts, caller-set confirmations, priority selection and multi-token acceptance are not available. ### Read deposit forwarding and callback history `GET /{ticker}/logs/` Callback is canonicalized and syntax-validated as in create, without DNS resolution. At most the 100 most recent eligible deposits are returned. Only eligible deposits to the assigned address and ticker asset are listed. Held, rejected, orphaned and below-minimum transfers are excluded. Existing orders without deposits have an empty callbacks array. Only the callback query parameter is accepted. #### Parameters | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ticker` | path | yes | string | Lowercase registry token ticker including slash, for example trc20/usdt or bep20/usdt. The server accepts both path segments as one ticker; clients must retain the slash. Native-only tickers are not currently resolved. | | `callback` | query | yes | string | URL-encoded public HTTPS callback URL, exactly once. Scheme and host case, trailing host dot, default port and percent-escape case are canonicalized; path and query order are preserved. | #### Example request ```bash curl -G "https://api.suawave.com/trc20/usdt/logs/" \ --data-urlencode "callback=https://shop.example/hook?order=1001&nonce=7f3a9c" ``` #### Response `200` Read-only deposit and delivery history of the 100 most recent deposits, oldest of those first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `address_in` | string | yes | | | `address_out` | string | yes | | | `callback_url` | string | yes | | | `notify_pending` | boolean | yes | | | `notify_confirmations` | integer | yes | Configured confirmations, or 1 for provider-finality policies. Does not override chain finality. | | `priority` | string, always `default` | yes | | | `callbacks` | array of object | yes | | | `callbacks[].txid_in` | string | yes | | | `callbacks[].txid_out` | string | yes | Empty until forwarding confirms. | | `callbacks[].value_coin` | string | yes | Inbound whole-asset decimal amount. | | `callbacks[].value_forwarded_coin` | string | yes | Net amount; zero until forwarding confirms. | | `callbacks[].confirmations` | integer | yes | | | `callbacks[].last_update` | string | yes | UTC transfer/settlement/delivery update timestamp. | | `callbacks[].result` | string, one of `pending`, `sent`, `done` | yes | Deposit seen but not forwarded; forwarding confirmed but confirmed callback not acknowledged; forwarding confirmed and confirmed callback acknowledged. | | `callbacks[].fee_percent` | string | yes | Snapshotted service fee percentage; zero without a settlement. | | `callbacks[].fee_coin` | string | yes | Service and charged network fees; zero until forwarding confirms. | | `callbacks[].logs` | array of object | yes | Newest first by attempt start time then ID. Only stored response excerpts are returned. | | `callbacks[].logs[].request_url` | string | yes | Reconstructed immutable callback URL including GET fields only for GET transport. | | `callbacks[].logs[].response_status` | integer | yes | Zero if no HTTP response. | | `callbacks[].logs[].responses` | string | yes | At most the stored 1024-byte excerpt; invalid UTF-8 bytes are removed. | | `callbacks[].logs[].next_try` | string or null | yes | Current delivery retry time in UTC; null for terminal delivery. Historical retry schedules are not stored. | | `callbacks[].logs[].pending` | integer, one of `0`, `1` | yes | | | `callbacks[].logs[].confirmed` | integer, one of `0`, `1` | yes | | ```json { "status": "success", "address_in": "TDepositAddressExample0000000000", "address_out": "TXyzPayoutAddressExample0000000000000", "callback_url": "https://shop.example/hook?order=1001&nonce=7f3a9c", "notify_pending": true, "notify_confirmations": 19, "priority": "default", "callbacks": [ { "txid_in": "9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9", "txid_out": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809", "value_coin": "100.000000", "value_forwarded_coin": "98.200000", "confirmations": 21, "last_update": "2026-10-25T12:00:00Z", "result": "done", "fee_percent": "0.50", "fee_coin": "1.800000", "logs": [ { "request_url": "https://shop.example/hook?order=1001&nonce=7f3a9c&address_in=TDepositAddressExample0000000000&coin=trc20_usdt&pending=0", "response_status": 200, "responses": "*ok*", "next_try": null, "pending": 0, "confirmed": 1 } ] } ] } ``` #### Errors Statuses `400`, `429`, `503` use the standard error body described in [Errors](errors.md). | Status | Meaning | | --- | --- | | `404` | Callback not found. | Notes for `logs`: | `result` | Meaning | | --- | --- | | `pending` | Deposit seen; forwarding has not confirmed. | | `sent` | Forwarding confirmed; the confirmed callback has not been acknowledged yet (including exhausted retries). | | `done` | Forwarding confirmed and the confirmed callback was acknowledged with `*ok*`. | Entries are listed oldest first, at most the 100 most recent eligible deposits. Each entry has at most ten attempt logs, newest first. Deposits below the minimum are not listed. ## Utilities ### Get the public ticker minimum `GET /{ticker}/info/` No global info endpoint. Info query parameters are currently ignored. #### Parameters | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ticker` | path | yes | string | Lowercase registry token ticker including slash, for example trc20/usdt or bep20/usdt. The server accepts both path segments as one ticker; clients must retain the slash. Native-only tickers are not currently resolved. | #### Example request ```bash curl "https://api.suawave.com/trc20/usdt/info/" ``` #### Response `200` Ticker information. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `coin` | string | yes | Asset symbol. | | `ticker` | string | yes | Ticker path value. | | `minimum_transaction_coin` | string | yes | Minimum deposit that is forwarded, in whole asset units. | | `fee_percent` | string | yes | Service fee percentage an address created now would pay, as a decimal string (0.50 is 0.5%). Coupons are not reflected here. | | `fee_fixed_coin` | string | yes | Fixed part of that service fee, in whole asset units. | | `fee_network_coin` | string | yes | Network fee charged on forwarding, in whole asset units. A promotion never lowers it. | | `fee_promotion` | string | no | Name of the active promotion; absent when the standard fee applies. | ```json { "status": "success", "coin": "USDT", "ticker": "trc20/usdt", "minimum_transaction_coin": "10.000000", "fee_percent": "0.20", "fee_fixed_coin": "0.300000", "fee_network_coin": "1.000000", "fee_promotion": "spring sale" } ``` #### Errors Statuses `400`, `429`, `503` use the standard error body described in [Errors](errors.md). ### Quote the configured native forwarding fee ceiling `GET /{ticker}/estimate/` Returns a conservative ceiling of the forwarding fee in the chain's native coin (for example TRX or BNB), not in token units. It is a configured fee cap, not a live estimate, and has no fiat conversion. On EVM chains it caps gas cost; on resource-based chains it covers contract energy fees, while bandwidth, account activation and preparatory funding are separate. Returns 503 when no ceiling is available. #### Parameters | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ticker` | path | yes | string | Lowercase registry token ticker including slash, for example trc20/usdt or bep20/usdt. The server accepts both path segments as one ticker; clients must retain the slash. Native-only tickers are not currently resolved. | | `addresses` | query | no | string, one of `1`, default `1` | Only one payout is supported. Other values and duplicate parameters are rejected. | | `priority` | query | no | string, one of `default`, default `default` | | #### Example request ```bash curl "https://api.suawave.com/trc20/usdt/estimate/" ``` #### Response `200` Configured cap in native coin, excluding service fees. estimated_cost_currency is omitted. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `estimated_cost` | string | yes | | ```json { "status": "success", "estimated_cost": "15.000000" } ``` #### Errors Statuses `400`, `429`, `503` use the standard error body described in [Errors](errors.md). > [!NOTE] > The estimate is a conservative configured fee ceiling for the forwarding transaction, in the chain's native coin (for example TRX for `trc20/usdt`), not the amount deducted from your deposit. See [Fees and minimums](fees.md) for what is charged. ### Encode an existing public deposit address as a payment QR `GET /{ticker}/qrcode/` The address must be the address_in of an existing order for this ticker. Unknown, duplicate, empty and malformed parameters are rejected; maximum URL length is 8192 bytes. EVM tokens with value use an EIP-681 token transfer request with the token contract, chain ID and integer base units. Without value the payload is only the address. TRON always uses the plain address and omits the validated value, because wallet amount URI support is not assumed. Wallet support varies. #### Parameters | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ticker` | path | yes | string | Lowercase registry token ticker including slash, for example trc20/usdt or bep20/usdt. The server accepts both path segments as one ticker; clients must retain the slash. Native-only tickers are not currently resolved. | | `address` | query | yes | string | Exact existing public order address_in for this ticker. | | `value` | query | no | string | Positive decimal token units, at most asset precision and uint256 base units; no signs or exponents. TRON validates but omits it from the QR. | | `size` | query | no | integer, default `512` | Square PNG dimensions in pixels. | #### Example request ```bash curl -G "https://api.suawave.com/trc20/usdt/qrcode/" \ --data-urlencode "address=TDepositAddressExample0000000000" ``` #### Response `200` PNG data URI and its exact encoded payment text. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `qr_code` | string | yes | | | `payment_uri` | string | yes | EIP-681 token transfer request or plain deposit address. | ```json { "status": "success", "qr_code": "data:image/png;base64,iVBORw0KGgo...", "payment_uri": "TDepositAddressExample0000000000" } ``` #### Errors Statuses `400`, `429`, `503` use the standard error body described in [Errors](errors.md). | Status | Meaning | | --- | --- | | `404` | Address not found for this ticker. | Payment URI by network family: | Family and request | QR payload | | --- | --- | | EVM token with `value` | EIP-681 token transfer request: `ethereum:@/transfer?address=&uint256=`. Support depends on the wallet and the payer may be able to change the amount. | | EVM token without `value` | The plain `address_in`. | | TRON token, with or without `value` | The plain `address_in`. No interoperable TRC-20 amount URI exists, so `value` is validated but not encoded. | The PNG is square, black on white, with a quiet zone of at least four modules. Its decoded text is exactly `payment_uri`. ### Convert an amount between fiat and priced asset currencies `GET /convert/` Informational CoinGecko prices cached for a short time; never used for credited, fee or payout amounts. Fiat results use 2 decimals, token results the asset's decimals, half up. A missing or stale price returns 503, never a guessed value. #### Parameters | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `value` | query | yes | string | Positive plain decimal, at most 30 integer and 18 fraction digits. | | `from` | query | yes | string | Fiat code or priced asset symbol, case-insensitive. | | `to` | query | yes | string | Fiat code or priced asset symbol, case-insensitive. | #### Example request ```bash curl -G "https://api.suawave.com/convert/" \ --data-urlencode "value=100" \ --data-urlencode "from=eur" \ --data-urlencode "to=usd" ``` #### Response `200` Converted amount and the rate used. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `value_coin` | string | yes | | | `exchange_rate` | string | yes | | ```json { "status": "success", "value_coin": "108.695652", "exchange_rate": "1.086956521739" } ``` #### Errors Statuses `400`, `429`, `503` use the standard error body described in [Errors](errors.md). ### Convert an amount into the ticker's asset `GET /{ticker}/convert/` Same rules as /convert/ with the ticker asset as target and its registry decimals as precision. #### Parameters | Name | In | Required | Type | Description | | --- | --- | --- | --- | --- | | `ticker` | path | yes | string | Lowercase registry token ticker including slash, for example trc20/usdt or bep20/usdt. The server accepts both path segments as one ticker; clients must retain the slash. Native-only tickers are not currently resolved. | | `value` | query | yes | string | Positive plain decimal. | | `from` | query | yes | string | Fiat code or priced asset symbol, case-insensitive. | #### Example request ```bash curl -G "https://api.suawave.com/trc20/usdt/convert/" \ --data-urlencode "value=100" \ --data-urlencode "from=usd" ``` #### Response `200` Converted amount and the rate used. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `value_coin` | string | yes | | | `exchange_rate` | string | yes | | ```json { "status": "success", "value_coin": "108.695652", "exchange_rate": "1.086956521739" } ``` #### Errors Statuses `400`, `429`, `503` use the standard error body described in [Errors](errors.md). Conversion rules, rounding and supported currencies are on the [Price conversion](conversion.md) page. ### Get the RSA callback verification key `GET /pubkey/` Returns the PKIX PUBLIC KEY PEM used to verify callbacks, RSA with at least 2048 bits. The x-ca-signature header of a callback is a base64 RSA-SHA256 PKCS1v15 signature over the exact full GET URL or the raw POST body. Verify using your configured external origin, preserve query ordering, deduplicate uuid and acknowledge with HTTP 200 and `*ok*` within 30 seconds. Form and JSON POST are supported. Pending and confirmed events have distinct UUIDs. #### Example request ```bash curl "https://api.suawave.com/pubkey/" ``` #### Response `200` Public key only; never private key material. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string, always `success` | yes | | | `pubkey` | string | yes | | ```json { "status": "success", "pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----\n" } ``` #### Errors Statuses `429` use the standard error body described in [Errors](errors.md). | Status | Meaning | | --- | --- | | `503` | Public key unavailable. | > [!NOTE] > Cache the key and refresh it only when you are told about a key rotation. See [Verify callback signatures](verify-signature.md). --- # Callbacks Source: https://suawave.com/docs/callbacks.html The gateway calls your callback URL to tell you about a deposit. Each forwarded deposit produces its own confirmed callback, and, when the order was created with `pending=1`, its own pending callback. Custom query parameters of your callback URL (such as `order` and `nonce`) are sent back unchanged in every callback. ## Events | Event | `pending` | When | | --- | --- | --- | | Pending (detection) | `"1"` | A deposit of at least the minimum was first seen by the indexer. Only with `pending=1` at create. Do not fulfill an order on this event. | | Confirmed | `"0"` | The deposit was forwarded to your payout address and the forwarding transaction is confirmed. | Pending and confirmed callbacks have different `uuid` values. Detection uses indexed chain data rather than a separate mempool subscription. Deposits below the minimum, and transfers of other tokens, produce no callback. ## Transports | Create parameter | Method | Content | | --- | --- | --- | | none (default) | `GET` | Fields appended to the query string after your own parameters, sorted by field name. | | `post=1` | `POST` | `application/x-www-form-urlencoded` body. Your own query parameters stay in the URL. | | `json=1` | `POST` | `application/json` body with string values. Your own query parameters stay in the URL. `json=1` wins over `post=1`. | Avoid field names from the table below in your own query parameters. A default `GET` callback looks like this: ``` GET /hook?order=1001&nonce=7f3a9c&address_in=TDepositAddressExample0000000000&address_out=TXyzPayoutAddressExample0000000000000&coin=trc20_usdt&confirmations=21&fee_coin=1.800000&pending=0&price=1.0002&txid_in=9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9&txid_out=1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809&uuid=8c4e2a61-5b3d-47f9-a0e2-1d6c9b7f3e45&value_coin=100.000000&value_forwarded_coin=98.200000 x-ca-signature: ``` ## Fields All values are strings. The examples below are shown as JSON; with `GET` and `post=1` the same fields arrive as query or form parameters. ### Detection (pending) callback Sent to the order callback URL when a deposit of at least the ticker minimum is first seen and the order was created with pending=1. GET sends the fields in the query string after any custom parameters; post=1 sends a form body; json=1 sends a JSON body. All values are strings. Header x-ca-signature carries the base64 RSA-SHA256 signature. Acknowledge with HTTP 200 and the body `*ok*` within 30 seconds. Do not fulfill an order on a pending callback. | Field | Type | Required | Description | | --- | --- | --- | --- | | `uuid` | string | yes | Event identifier, stable across retries. Deduplicate on it. Pending and confirmed events have different uuids. | | `address_in` | string | yes | Deposit address of the order. | | `address_out` | string | yes | Payout address of the order. | | `txid_in` | string | yes | Hash of the inbound deposit transaction. | | `coin` | string | yes | Ticker with the slash replaced by an underscore, for example trc20_usdt. | | `pending` | string, always `1` | yes | "1" for a detection callback. | | `price` | string | no | USD price of one whole token, up to 8 decimals. Present only when a fresh price existed at event creation. | ```json { "uuid": "3f1b7c9e-8a52-4d0e-9c41-6b2e5f0a7d13", "address_in": "TDepositAddressExample0000000000", "address_out": "TXyzPayoutAddressExample0000000000000", "txid_in": "9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9", "coin": "trc20_usdt", "pending": "1", "price": "1.0002" } ``` ### Confirmed callback Sent after the deposit was forwarded to the payout address and the forwarding transaction is confirmed. Same transports, signature and acknowledgement as the pending callback. Each forwarded deposit produces its own confirmed callback with its own uuid. | Field | Type | Required | Description | | --- | --- | --- | --- | | `uuid` | string | yes | Event identifier, stable across retries. Deduplicate on it. | | `address_in` | string | yes | Deposit address of the order. | | `address_out` | string | yes | Payout address of the order. | | `txid_in` | string | yes | Hash of the inbound deposit transaction this callback is about. | | `coin` | string | yes | Ticker with the slash replaced by an underscore, for example trc20_usdt. | | `pending` | string, always `0` | yes | "0" for a confirmed callback. | | `price` | string | no | USD price of one whole token, up to 8 decimals. Present only when a fresh price existed at event creation. | | `txid_out` | string | yes | Hash of the forwarding transaction to the payout address. | | `confirmations` | string | yes | Confirmations of the inbound deposit at event creation; at least 1. | | `value_coin` | string | yes | Inbound amount before fees, in whole asset units. | | `value_forwarded_coin` | string | yes | Amount forwarded to the payout address after fees. | | `fee_coin` | string | yes | Service and charged network fees, in whole asset units. | | `value_coin_convert` | string | no | JSON object serialized as a string, keyed by upper-case fiat code with 2-decimal values. Only for orders created with convert=1 and only when a fresh price existed. | | `value_forwarded_coin_convert` | string | no | Same shape as value_coin_convert, for the forwarded amount. | ```json { "uuid": "8c4e2a61-5b3d-47f9-a0e2-1d6c9b7f3e45", "address_in": "TDepositAddressExample0000000000", "address_out": "TXyzPayoutAddressExample0000000000000", "txid_in": "9f2c1a0e5b7d4c3a8e6f1b2d3c4a5e6f7a8b9c0d1e2f30415263748596a7b8c9", "coin": "trc20_usdt", "pending": "0", "price": "1.0002", "txid_out": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809", "confirmations": "21", "value_coin": "100.000000", "value_forwarded_coin": "98.200000", "fee_coin": "1.800000", "value_coin_convert": "{\"EUR\":\"92.00\",\"USD\":\"100.02\"}", "value_forwarded_coin_convert": "{\"EUR\":\"90.34\",\"USD\":\"98.22\"}" } ``` ## Signature Every callback carries the header `x-ca-signature`. For `GET` it signs the full request URL, for `POST` the exact raw body. Verify it before trusting anything else in the request. See [Verify callback signatures](verify-signature.md). ## Acknowledge Answer with HTTP status `200` and the body `*ok*` (plain `ok` is accepted too, in any letter case) within 30 seconds. Any other status, body or a timeout counts as a failure and the callback is retried. Redirects are not followed, and destinations resolving to private addresses are refused at send time. ## Retries There are eight attempts: the initial send, then after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, each with up to 10% added jitter. The `uuid` stays the same across retries. Duplicates are possible, so store the `uuid` and ignore events you have processed. A confirmed callback whose retries are exhausted shows as `sent` in [the logs](api-reference.md), which you can poll to reconcile. ## Order The pending and the confirmed callback of a deposit can arrive in either order after retries. A pending callback never replaces a confirmed one: once you have processed `pending` equal to `"0"` for a deposit, ignore a late `"1"`. ## Key rotation Callbacks carry no key id and no timestamp, so a rotation is announced rather than negotiated. The operator creates a new key pair, installs the private key on the workers and the public key on the API, and restarts them together; from that moment every callback, including retries of older ones, is signed with the new key. After a failed verification, fetch `GET /pubkey/` once, replace your stored key, and verify again before rejecting the callback. A callback that still fails with the fresh key is not from the gateway. ## Checklist - Verify `x-ca-signature` against the key from `GET /pubkey/`. - Compare the nonce and order reference in the query string with your stored order. - Check `address_in`, `coin` and the amount against the order. - Deduplicate on `uuid` before doing work. - Fulfill only on `pending` equal to `"0"`. - Answer `*ok*`. --- # Verify callback signatures Source: https://suawave.com/docs/verify-signature.html Each callback carries `x-ca-signature`, a base64 RSA-SHA256 signature (PKCS#1 v1.5) made with the gateway's private key. Verify it with the public key from `GET /pubkey/`. ## What is signed | Callback method | Signed bytes | | --- | --- | | `GET` | The full request URL as it was sent: scheme, host, path and the original query string in its original order. | | `POST` (form or JSON) | The exact raw request body, before parsing. | > [!WARNING] > Do not re-encode the query string or re-serialize the body. Capture the raw bytes first and parse afterwards. > [!WARNING] > Behind a proxy or load balancer, rebuild the URL from your own configured external origin (scheme, host and port if present) plus the original request target. Never take the host from an untrusted `Host` or `X-Forwarded-*` header. ## Get the public key ```bash curl "https://api.suawave.com/pubkey/" ``` The response contains a PKIX `PUBLIC KEY` PEM in the `pubkey` field. Keys are RSA with at least 2048 bits. Fetch it over HTTPS once, store it, and refresh it only when you are told about a key rotation. Do not fetch it on every callback. Reject keys shorter than 2048 bits. ## Examples Each example takes the HTTP method, the request target (path and query, exactly as received), the raw body, the signature header, the public key PEM and your trusted origin, and returns whether the signature is valid. A failed check must answer HTTP 401 and do no work. Get the request target from your framework's raw request URI (Express `req.originalUrl`, PHP `$_SERVER['REQUEST_URI']`, ASP.NET Core `GetEncodedPathAndQuery()`), and the raw body before any JSON or form parsing (Express `express.raw`, PHP `file_get_contents('php://input')`). ### Go The Go example uses only the standard library and is compiled and tested against the gateway's own signer. ```go package verify import ( "bytes" "crypto" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "io" "net/http" ) func Callback(r *http.Request, publicPEM []byte, trustedOrigin string) error { block, _ := pem.Decode(publicPEM) if block == nil { return errors.New("invalid public key PEM") } parsed, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return err } key, ok := parsed.(*rsa.PublicKey) if !ok || key.N.BitLen() < 2048 { return errors.New("invalid RSA public key") } var signed []byte switch r.Method { case http.MethodGet: signed = []byte(trustedOrigin + r.RequestURI) case http.MethodPost: signed, err = io.ReadAll(io.LimitReader(r.Body, 65537)) if err != nil { return err } if len(signed) > 65536 { return errors.New("callback body too large") } r.Body = io.NopCloser(bytes.NewReader(signed)) default: return errors.New("unsupported callback method") } signature, err := base64.StdEncoding.DecodeString(r.Header.Get("x-ca-signature")) if err != nil { return err } digest := sha256.Sum256(signed) return rsa.VerifyPKCS1v15(key, crypto.SHA256, digest[:], signature) } ``` ### C# ```csharp using System; using System.Security.Cryptography; using System.Text; public static class CallbackVerifier { public static bool Verify(string method, string requestUri, byte[] rawBody, string signatureBase64, string publicKeyPem, string trustedOrigin) { byte[] signed; if (method == "GET") { signed = Encoding.UTF8.GetBytes(trustedOrigin + requestUri); } else if (method == "POST" && rawBody.Length <= 65536) { signed = rawBody; } else { return false; } byte[] signature; try { signature = Convert.FromBase64String(signatureBase64); } catch (FormatException) { return false; } using var rsa = RSA.Create(); rsa.ImportFromPem(publicKeyPem); return rsa.VerifyData(signed, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } } ``` ### JavaScript (Node.js) ```javascript const crypto = require('crypto'); function verifyCallback({ method, requestUri, rawBody, signature }, publicKeyPem, trustedOrigin) { let signed; if (method === 'GET') { signed = Buffer.from(trustedOrigin + requestUri, 'utf8'); } else if (method === 'POST') { if (!Buffer.isBuffer(rawBody) || rawBody.length > 65536) return false; signed = rawBody; } else { return false; } if (!signature || !/^[A-Za-z0-9+/]+={0,2}$/.test(signature)) return false; const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(signed); return verifier.verify(publicKeyPem, signature, 'base64'); } module.exports = { verifyCallback }; ``` ### PHP ```php 65536) { return false; } $signed = $rawBody; } else { return false; } $signature = base64_decode($signatureBase64, true); if ($signature === false) { return false; } return openssl_verify($signed, $signature, $publicKeyPem, OPENSSL_ALGO_SHA256) === 1; } ``` ## After verification A valid signature proves the request came from the gateway. It does not prove the payment is the one you expect. Also: - Compare the `nonce` and order reference in your callback URL with the stored order. - Check `address_in`, `coin` and `value_coin` against what you expect. - Persist the `uuid` and ignore repeats. - Answer `200` with `*ok*` within 30 seconds. --- # Tickers and networks Source: https://suawave.com/docs/tickers.html A ticker names the network and the asset in one path value. Tokens use `network/asset`, for example `trc20/usdt`, so the create endpoint is `/trc20/usdt/create/`. Tickers are lowercase and case-sensitive. In callbacks the `coin` field writes the same ticker with an underscore: `trc20_usdt`. Native-coin tickers such as a bare `trx` are not available. | Prefix | Network | Token standard | | --- | --- | --- | | `trc20` | TRON | TRC-20 | | `bep20` | BSC (BNB Smart Chain) | BEP-20 | The asset part is the lowercase symbol of the asset. The tables below list what the gateway ships for each environment. The minimum deposit shown here is the starting value; the value that applies to your order is `minimum_transaction_coin` from `GET /{ticker}/info/` and `GET /{ticker}/create/`. An unknown ticker answers `Invalid ticker`. > [!WARNING] > Send only the asset of the ticker to the deposit address. The native coin of the network (TRX, BNB) and tokens the gateway does not list for that network are not detected: no callback, no credit and no forwarding. Recovering them needs a manual support request with the deposit address and the transaction hash. ### Mainnet tickers | Ticker | Chain | Asset | Decimals | Minimum deposit | | --- | --- | --- | --- | --- | | `trc20/usdt` | TRON | USDT | 6 | 10.000000 | | `bep20/usdt` | BSC | USDT | 18 | 1.000000000000000000 | ### Testnet tickers | Ticker | Chain | Asset | Decimals | Minimum deposit | | --- | --- | --- | --- | --- | | `trc20/usdt` | TRON | USDT | 6 | 10.000000 | | `bep20/usdt` | BSC | USDT | 18 | 10.000000000000000000 | Each ticker works only for payout addresses of its own network. A payout address of another network, or an address controlled by the gateway, is refused. --- # Fees and minimums Source: https://suawave.com/docs/fees.html Each deposit is charged on its own, and the callback tells you exactly what happened: `value_coin` is the deposit, `fee_coin` is everything charged and `value_forwarded_coin` is what reached your payout address. ``` value_forwarded_coin = value_coin - fee_coin ``` ## What is charged - Service fee: a percentage of the deposit, rounded up to the smallest unit of the asset once, and never less than the minimum service fee. It is capped at the deposit. - Network fee: a fixed amount per forwarding transaction, charged in the asset itself. When the deposit is too small it is the network fee that shrinks first. - `fee_coin` in callbacks and logs is the sum of both. `fee_percent` in logs is the service fee rate as a percent. All arithmetic uses integer units of the asset, so amounts are exact. A deposit whose fees leave nothing to forward cannot be paid out automatically, which is why the minimum is always set above the maximum possible fee. Create and info answer `503` with `Ticker is temporarily unavailable` if a ticker's minimum does not cover its fees. ## Minimums Every ticker has a minimum deposit, returned as `minimum_transaction_coin` by create and info. Show it to the payer before they send. - A deposit of at least the minimum is forwarded, each deposit separately. - A deposit below the minimum is not forwarded, triggers no callback, does not appear in the logs and is not paid out. - The minimum that counts is the one in force when the deposit was first detected. A later change does not affect deposits already detected. ## Default fee schedule > [!NOTE] > The tables show the fee schedule the gateway ships with and what a deposit of 100 units would leave you. The schedule can be changed by the operator, and fees are resolved per deposit, so use the callback values for the amounts that actually applied. ### Mainnet default fees | Ticker | Service fee | Minimum service fee | Network fee | Fees on a deposit of 100 | Forwarded | | --- | --- | --- | --- | --- | --- | | `bep20/usdt` | 0.50% | 0.300000000000000000 | 0.100000000000000000 | 0.600000000000000000 | 99.400000000000000000 | | `trc20/usdt` | 0.50% | 0.300000 | 1.000000 | 1.500000 | 98.500000 | ### Testnet default fees | Ticker | Service fee | Minimum service fee | Network fee | Fees on a deposit of 100 | Forwarded | | --- | --- | --- | --- | --- | --- | | `bep20/usdt` | 0.50% | 0.300000000000000000 | 0.100000000000000000 | 0.600000000000000000 | 99.400000000000000000 | | `trc20/usdt` | 0.50% | 0.300000 | 1.000000 | 1.500000 | 98.500000 | To budget the network side, `GET /{ticker}/estimate/` returns a conservative ceiling of the forwarding cost in the chain's native coin. It is a fee cap, not a quote of what is deducted from your deposit. --- # Price conversion Source: https://suawave.com/docs/conversion.html Conversion is informational. It never changes what is credited, charged or paid out: those amounts are always exact integer units of the token. ## Endpoints - `GET /convert/?value=&from=&to=` converts between any two supported currencies. - `GET /{ticker}/convert/?value=&from=` converts into the ticker's own asset, using its decimals. Both are described with examples in the [API reference](api-reference.md). `from` and `to` are case-insensitive. `value` is a positive plain decimal with at most 30 integer and 18 fraction digits (no sign, exponent or separators). ## Supported currencies Fiat currencies: `usd`, `eur`, `gbp`, `cad`, `jpy`, `aed`, `myr`, `idr`, `thb`, `chf`, `sgd`, `rub`, `zar`, `try`, `lkr`, `ron`, `bgn`, `huf`, `czk`, `php`, `pln`, `ugx`, `mxn`, `inr`, `hkd`, `cny`, `brl`, `dkk`, `twd`, `aud`, `ngn`, `sek`, `nok`, `uah`, `vnd`. Priced assets: `bnb`, `btc`, `eth`, `trx`, `usdc`, `usdt`. ## Rounding - Fiat results are rounded half up to 2 decimals. Token results are rounded half up to the asset's decimals. - `exchange_rate` is the number of `to` units per one `from` unit, rounded half up to 12 decimals with trailing zeros trimmed. ## Freshness and failure Prices come from CoinGecko and are cached for a short time. If no fresh price exists, or conversion is disabled, the endpoints answer HTTP `503` with `{"status":"error","error":"Price data is temporarily unavailable"}`. They never return a stale or guessed value. ## In callbacks - Every callback carries `price`, the USD price of one whole token with up to 8 decimals, when a fresh price existed at event creation. - Confirmed callbacks of orders created with `convert=1` also carry `value_coin_convert` and `value_forwarded_coin_convert`. They are JSON objects serialized as strings, keyed by upper-case fiat code with 2-decimal values, for example `{"EUR":"90.00","USD":"100.02"}`. - Without a fresh price the callback is still sent on time, without `price` and the convert fields. They are never added later. - `convert=1` is refused with HTTP `400` at create when conversion is disabled. --- # Errors Source: https://suawave.com/docs/errors.html Endpoints and request limiters answer every error with the same JSON body: a `status` field that is always `error` and a short `error` message. ```json {"status": "error", "error": "Invalid ticker"} ``` > [!NOTE] > `status` is always `error` and `error` is a short message you can show or log. Do not parse the message to make decisions; use the HTTP status. | HTTP | Examples | What to do | | --- | --- | --- | | `400` | `Invalid ticker`, `Address is invalid`, `Callback URL is not a valid URL`, unsupported or duplicate parameters, a payout address that differs from the one already bound to the pair | Fix the request. Retrying unchanged will fail again. | | `404` | `Callback not found` from logs for an unknown pair, `Address not found` from qrcode | Check the ticker and callback or address. Create the order first. | | `429` | `Rate limit exceeded`, `New order creation limit exceeded` | Back off and retry later. Reusing an existing pair avoids the new-order limit but still counts toward the request limit. | | `503` | A paused network, a low pool of free addresses, `Ticker is temporarily unavailable`, `Network fee estimate unavailable`, `Price data is temporarily unavailable`, or a transient processing failure | Retry later with exponential backoff. | > [!NOTE] > Failures raised before a request reaches an endpoint, such as an unknown route, a method other than `GET`, an oversized request or a timeout, are answered by the gateway's middleware and may use a different body. ## Common causes - `Invalid ticker`: the ticker is not lowercase, is missing the token part, or is not in [Tickers and networks](tickers.md). - `Address is invalid`: the payout address does not belong to the ticker's network, or is controlled by the gateway. - Immutable payout: the pair already exists with another payout address. Use a new callback URL for a new payout address. - `Callback URL is not a valid URL`: the callback must be a public HTTPS URL, and private or loopback destinations are refused. ## Callback failures A callback that your server answers with anything other than `200` and `*ok*` is retried on the schedule in [Callbacks](callbacks.md). Failed deliveries are visible in the `logs` of the order. --- # Glossary Source: https://suawave.com/docs/glossary.html Short definitions of the terms used across these pages, each with a link to the page that explains it in full. ## Terms ### Deposit address The address in the `address_in` field that you show to the payer; it is permanent for a ticker, payout address and callback URL. See [API reference](api-reference.md). ### Payout address The address in the `address_out` field that receives every forwarded deposit, minus fees. See [Quick start](quickstart.md). ### Callback The request the gateway sends to your callback URL when a deposit is detected or forwarded. See [Callbacks](callbacks.md). ### Pending callback The early callback with `pending` set to `1`, sent when a deposit is first detected and only when the order was created with `pending=1`. See [Callbacks](callbacks.md). ### Confirmations The number of confirmations the inbound deposit had when the callback event was created, in the `confirmations` field. See [Callbacks](callbacks.md). ### Ticker The path value that names the network and the asset together, such as `trc20/usdt`; tickers are lowercase and case-sensitive. See [Tickers and networks](tickers.md). ### TRC20 The token standard of the TRON network, used by tickers that start with `trc20`. See [Tickers and networks](tickers.md). ### BEP20 The token standard of the BNB Smart Chain, used by tickers that start with `bep20`. See [Tickers and networks](tickers.md). ### USDT The Tether stablecoin, accepted on each network that lists a `usdt` ticker. See [Tickers and networks](tickers.md). ### Service fee A percentage of the deposit, never less than the minimum service fee, that is charged on every deposit. See [Fees and minimums](fees.md). ### Network fee A fixed amount per forwarding transaction, charged in the asset itself. See [Fees and minimums](fees.md). ### value_coin The callback field with the deposit amount in whole units of the asset. See [Fees and minimums](fees.md). ### fee_coin The callback field with everything that was charged on the deposit, service fee and network fee together. See [Fees and minimums](fees.md). ### value_forwarded_coin The callback field with the amount that reached your payout address, which is `value_coin` minus `fee_coin`. See [Fees and minimums](fees.md). ### uuid The callback field that identifies one callback; it stays the same across retries, so use it to skip repeats. See [Callbacks](callbacks.md).