Production-grade, version-aware Python SDK for the Tronado Public API (v5).
Project description
Tronado Python SDK
A production-grade, version-aware Python SDK for the Tronado Public API. Tronado lets a business accept TRON (TRX) payments: you create an order, the customer pays through Tronado's in-app/web payment UI, and Tronado notifies your server with a signed webhook.
- Sync & async clients on a single
httpxcore - Typed Pydantic v2 request/response models (amounts are
Decimal, neverfloat) - Idempotency-aware retries — order creation is never silently retried
- Webhook verification for the documented
X-Tronado-SigHMAC-SHA512 scheme - Designed for API versioning — v5 today, future versions plug in cleanly
Currently implements API v5 (the recommended version).
Installation
pip install tronado
From source (this repository):
pip install -e ".[dev]" # includes test/lint tooling
Requires Python 3.9+. Runtime dependencies: httpx and pydantic (v2).
Authentication
Every outbound endpoint requires your API key in the x-api-key header (this is the
documented scheme — there is no Authorization: Bearer). Request a key from
Tronado support.
from tronado import TronadoClient
client = TronadoClient(api_key="YOUR_API_KEY")
Or via environment variable (no argument needed):
export TRONADO_API_KEY="YOUR_API_KEY"
# optional: export TRONADO_BASE_URL="https://bot.tronado.cloud"
Configuration
| Option | Default | Description |
|---|---|---|
api_key |
TRONADO_API_KEY env |
API key for the x-api-key header |
base_url |
https://bot.tronado.cloud |
API base URL (TRONADO_BASE_URL honoured) |
timeout |
30.0 |
Per-request timeout (seconds) |
max_retries |
3 |
Max retries for idempotent operations |
backoff_factor |
0.5 |
Exponential-backoff base multiplier (seconds) |
default_version |
"v5" |
Version used by the order / price shortcuts |
default_headers |
{} |
Extra headers added to every request |
user_agent |
tronado-python/<ver> |
User-Agent header |
http_client |
None |
Inject your own httpx.Client/AsyncClient |
client = TronadoClient(
api_key="YOUR_API_KEY",
timeout=15.0,
max_retries=5,
backoff_factor=0.25,
default_headers={"X-Trace-Id": "abc123"},
)
You can also pass a fully built TronadoConfig:
from tronado import TronadoConfig, TronadoClient
config = TronadoConfig(api_key="YOUR_API_KEY", timeout=10)
client = TronadoClient(config=config)
Quickstart (sync)
from decimal import Decimal
from tronado import TronadoClient
with TronadoClient(api_key="YOUR_API_KEY") as tron:
# 1. Tronado's TRX price differs from exchanges — always price first.
price = tron.price.tron.get_price_to_toman()
print(price.tron_price_toman, "Toman per TRX")
# 2. Create the order and get a payment link.
order = tron.order.get_order_token(
payment_id="inv-1001", # your unique id
wallet_address="TXYZ12345abcdef...", # destination wallet
tron_amount=Decimal("12.123456"), # invoice amount in TRX
callback_url="https://your-domain.com/payment/callback",
wage_from_business_percentage=0, # who absorbs the fee (0–100)
)
print("Pay here:", order.full_payment_url)
# 3. Later, check status (by Tronado OrderId / TrndOrderID_x / TXID).
status = tron.order.get_status(id="TrndOrderID_55")
print("Paid?" , status.is_payment_accepted)
Quickstart (async)
import asyncio
from tronado import AsyncTronadoClient
async def main() -> None:
async with AsyncTronadoClient(api_key="YOUR_API_KEY") as tron:
price = await tron.price.tron.get_price_to_toman()
order = await tron.order.get_order_token(
payment_id="inv-1002",
wallet_address="TXYZ...",
tron_amount="8.5",
callback_url="https://your-domain.com/payment/callback",
)
print(order.full_payment_url)
asyncio.run(main())
Endpoint reference
The async client exposes the same methods with await.
Order — client.order
| Method | Endpoint | Returns |
|---|---|---|
get_order_token(payment_id, wallet_address, tron_amount, callback_url, wage_from_business_percentage=0) |
POST /api/v5/GetOrderToken |
OrderTokenData |
get_status(id) |
POST /Order/GetStatus |
OrderStatus (raises OrderNotFoundError if absent) |
get_status_by_payment_id(id) |
POST /Order/GetStatusByPaymentID |
OrderStatus |
Price — client.price
| Method | Endpoint | Returns |
|---|---|---|
price.tron.get_price_to_toman() |
POST /Tron/GetPriceToToman |
TronPrice |
price.tron.get_price_with_wage_to_toman(request_code, wallet_address, tron_amount) |
POST /Tron/GetPriceWithWageToToman |
PriceWithWage |
price.toman.convert_to_tron_wage_subtracted(toman, wallet) |
POST /Toman/ConvertToTronWageSubtracted |
TronConversion |
price.toman.get_price_to_toman() |
POST /Toman/GetPriceToToman |
DollarPrice |
price.dollar.convert_to_tron_wage_subtracted(dollar, wallet) |
POST /Dollar/ConvertToTronWageSubtracted |
TronConversion |
price.dollar.get_price_to_toman() |
POST /Dollar/GetPriceToToman |
DollarPrice |
Amount arguments (
tron_amount,dollar) acceptDecimal,int,float, orstr. They are coerced viastrto avoid binary-float rounding, and sent on the wire as JSON numbers.
Order statuses
OrderStatusID is documented as a fixed set, used identically by get_status and the
webhook. The SDK exposes them as tronado.OrderStatusCode:
OrderStatusID |
OrderStatusCode |
Meaning |
|---|---|---|
20 |
WAITING_FOR_PAYMENT |
Order created; awaiting the user's payment |
25 |
PHOTO_SENT_TO_ADMIN |
Payment proof submitted, sent to an admin |
27 |
READY_TO_TRANSFER |
Approved; queued for the on-chain TRX transfer |
30 |
PAYMENT_ACCEPTED |
Successful, final payment (IsPaid == true) |
40 |
PAYMENT_REJECTED |
Payment rejected |
200 |
CANCELLED |
Order cancelled |
Only 30 (PAYMENT_ACCEPTED) means money is settled — that's what
is_payment_accepted checks (alongside IsPaid). OrderStatusTitle is a Persian,
display-only label, so branch on the id, never the title.
from tronado import OrderStatusCode
status = tron.order.get_status(id="TrndOrderID_55")
if status.is_payment_accepted: # IsPaid or OrderStatusID == 30
fulfill()
elif status.order_status is OrderStatusCode.PAYMENT_REJECTED:
notify_rejected()
# status.order_status is None for any id Tronado may add in future (no crash).
Handling webhooks (IPN)
Tronado POSTs a JSON callback to your CallbackUrl on every order status change and
signs it with X-Tronado-Sig = HMAC_SHA512(raw_body, your_ipn_signing_key) (lowercase
hex). Get your IpnSigningKey from support. Always verify against the raw body — do
not re-serialize parsed JSON.
from tronado.webhook import construct_event
from tronado.exceptions import InvalidSignatureError
IPN_SIGNING_KEY = "YOUR_IPN_SIGNING_KEY"
def handle_webhook(raw_body: bytes, signature_header: str) -> int:
try:
event = construct_event(raw_body, signature_header, IPN_SIGNING_KEY)
except InvalidSignatureError:
return 401 # reject
# De-duplicate: the same (payment_id, status) may arrive more than once.
if already_processed(event.dedup_key):
return 200
if event.is_payment_accepted: # IsPaid == True or status 30
# Charge the user with what they actually paid (includes wage), per the docs.
credit_user(event.payment_id, event.user_paid_toman_amount)
return 200 # return 2xx to acknowledge, else Tronado retries
FastAPI example (reads the raw body before parsing):
from fastapi import FastAPI, Request, Response
from tronado.webhook import construct_event
from tronado.exceptions import InvalidSignatureError, TronadoWebhookError
app = FastAPI()
@app.post("/payment/callback")
async def callback(request: Request) -> Response:
raw = await request.body()
sig = request.headers.get("X-Tronado-Sig", "")
try:
event = construct_event(raw, sig, "YOUR_IPN_SIGNING_KEY")
except (InvalidSignatureError, TronadoWebhookError):
return Response(status_code=401)
# ... process event ...
return Response(status_code=200)
If you only need to verify or parse separately:
from tronado.webhook import verify_signature, parse_callback
if verify_signature(raw, sig, signing_key):
event = parse_callback(raw)
Error handling
All errors derive from TronadoError:
from tronado.exceptions import (
TronadoError, TronadoAuthenticationError, TronadoRateLimitError,
OrderNotFoundError, TronadoValidationError, TronadoServerError,
TronadoTimeoutError, TronadoConnectionError,
)
try:
status = client.order.get_status(id="maybe-missing")
except OrderNotFoundError:
status = None
except TronadoAuthenticationError:
... # bad/missing API key (HTTP 401 or envelope Code == -1)
except TronadoRateLimitError:
... # 429 / per-user limits
except (TronadoTimeoutError, TronadoConnectionError):
... # network problems
except TronadoServerError:
... # 5xx (already retried for idempotent calls)
except TronadoError:
... # catch-all
TronadoAPIError (and its subclasses) carry .status_code, .code, .message, and
.response for diagnostics.
Retries & idempotency
- Idempotent reads (price/status) are retried on timeouts, connection errors,
429, and5xx, using exponential backoff with jitter (andRetry-Afterwhen present). get_order_tokenis never auto-retried — it creates a transaction, so retrying on an ambiguous failure could create a duplicate order. Handle its failures explicitly.
Versioning
The Tronado API versions endpoints in the URL path (/api/v{version}/...). In v5 only
GetOrderToken is version-pathed; the price/status endpoints live at unversioned roots.
The SDK models this with per-version operation descriptors, so:
client.v5.order.get_order_token(...) # explicit version pin
client.version("v5").price.tron.get_price_to_toman()
client.order.get_order_token(...) # uses default_version
When Tronado ships a new version, it becomes a new tronado/versions/vN/ package and a
one-line registry entry — the transport, models, and error handling are reused unchanged.
from tronado import available_versions
print(available_versions()) # ('v5',)
The fee (wage) model
The default wage is 20% (configurable per business; minimum is the greater of 9,000 Toman
or $0.10). wage_from_business_percentage on get_order_token controls who absorbs it:
0(default): the whole fee is added on top of the user's payment; you receive the full invoice TRX.100: the whole fee is taken from your share; the user pays roughly the base value and your receivedTronAmountis the net after fee.- Values in between split the fee proportionally.
To charge your user correctly, use user_paid_toman_amount from the v5 webhook (what
the user actually paid) rather than the TRX you receive.
Development
pip install -e ".[dev]"
pytest # run the test-suite (sync + async)
ruff check . # lint
mypy src # type-check
Run the suite across every supported interpreter with tox (included in the dev
extra installed above):
tox # py39–py313 + a lint/type-check env
CI (GitHub Actions, .github/workflows/ci.yml) runs ruff and
mypy once, then the test-suite on Python 3.9, 3.10, 3.11, 3.12, and 3.13.
See examples/ for runnable scripts and docs/DESIGN.md
for the architecture and the full endpoint matrix.
License
MIT — see LICENSE.
Support: t.me/TronadoSupp
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tronado-0.1.1.tar.gz.
File metadata
- Download URL: tronado-0.1.1.tar.gz
- Upload date:
- Size: 40.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
013265d52842f8faf291820cf16287ec013c0cb5b0f72a1ab3db92a5447b9f6e
|
|
| MD5 |
3163eeb5122a84855ed1286d0347693f
|
|
| BLAKE2b-256 |
451184821adb0ff323d81ddd698cdb093b353e9d47f6cfc8bdd0a03f29744669
|
File details
Details for the file tronado-0.1.1-py3-none-any.whl.
File metadata
- Download URL: tronado-0.1.1-py3-none-any.whl
- Upload date:
- Size: 39.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6dea0c76d97cb598255d4d249d63b09a3ad89cb9336b3cf7996c486f5984ea1e
|
|
| MD5 |
9b8a8135b2a80a24f9aeb82861b609ad
|
|
| BLAKE2b-256 |
b6e9ca54674131676c18f89c442b84fc7413f882be2e373d93ff5d2e24b14be0
|