Webhook API
TRON address monitoring and transaction callbacks
Add an address to the watch list and every incoming transaction arrives at your server as an HTTP POST, signed with HMAC.
Service overview
The TRON monitoring service watches transaction activity on the addresses you register. When a watched address receives a TRX transfer, we send an HTTP POST to your callback URL in real time.
https://api.rolthe.com/v1/tron-monitor/tron-monitor/statsGet statistics
Monitoring statistics for the current account: how many addresses are watched, quota usage and so on.
Response example
{
"success": true,
"code": 0,
"message": "ok",
"data": {
"total_addresses": 25,
"active_addresses": 23,
"total_callbacks": 156,
"success_callbacks": 150,
"failed_callbacks": 6
}
}Response fields
| Field | Type | Description |
|---|---|---|
total_addresses | number | Total addresses added to monitoring |
active_addresses | number | Addresses currently being monitored |
total_callbacks | number | Total callbacks sent |
success_callbacks | number | Successful callbacks |
failed_callbacks | number | Failed callbacks |
/tron-monitor/addressesList monitored addresses
Paginated list of every address this account monitors.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page | number | No | Page number, 1 by default |
page_size | number | No | Page size, 20 by default, 100 maximum |
status | string | No | Filter by status: active, inactive |
Response example
{
"success": true,
"code": 0,
"message": "ok",
"data": {
"total": 25,
"addresses": [
{
"address": "TExampleAddress1234567890abcdef",
"label": "hot-wallet-1",
"tokens": ["TRX"],
"is_active": true,
"total_callbacks": 10,
"last_callback_at": "2024-01-02T15:45:00Z",
"created_at": "2024-01-02T10:30:00Z"
}
]
}
}/tron-monitor/addressesAdd a monitored address
Add a TRON address to the watch list. From then on, every incoming TRX transfer to it triggers a callback.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
address | string | Yes | The TRON address to monitor (starts with T, 34 characters) |
webhook_url | string | Yes | Callback URL (unless one is already configured on the API key) |
label | string | No | A label or note, to make the address easy to recognise |
tokens | array | No | Which tokens to watch, ["TRX"] by default |
Request example
curl -X POST "https://api.rolthe.com/v1/tron-monitor/addresses" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Timestamp: 1704067200" \
-H "X-Signature: YOUR_SIGNATURE" \
-H "Content-Type: application/json" \
-d '{
"address": "TExampleAddress1234567890abcdef",
"webhook_url": "https://your-server.com/webhook",
"label": "hot-wallet-1",
"tokens": ["TRX"]
}'Response example
{
"success": true,
"code": 0,
"message": "address added to monitoring",
"data": {
"address": "TExampleAddress1234567890abcdef",
"status": "active",
"webhook_url": "https://your-server.com/webhook"
}
}/tron-monitor/addresses/{address}Delete a monitored address
Remove an address from the watch list. No further callbacks are sent for it.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
address | string | Yes | The TRON address to remove (path parameter) |
Response example
{
"success": true,
"code": 0,
"message": "address removed from monitoring",
"data": null
}Webhook callback format
When a monitored address receives a TRX transfer, we send an HTTP POST to your configured callback URL.
{
"event": "tron.transfer",
"timestamp": 1735689600,
"data": {
"tx_hash": "abc123def456789012345678901234567890abcd",
"from": "TNcA4u3b8RhaCsdtq57Zgigx46MGhtaxAr",
"to": "TExampleAddress1234567890abcdef",
"amount": "100.000000",
"token": "TRX",
"block_number": 12345678,
"block_timestamp": 1735689600000
}
}Response fields
| Field | Type | Description |
|---|---|---|
event | string | Event type: tron.transfer |
timestamp | number | When the callback was sent (Unix seconds) |
data.tx_hash | string | Transaction hash (64 hex characters) |
data.from | string | Sender address |
data.to | string | Recipient address (the monitored one) |
data.amount | string | Formatted amount (TRX) |
data.token | string | Token symbol: TRX |
data.block_number | number | Block height |
data.block_timestamp | number | Block timestamp (milliseconds) |
Response requirements
Your server must return HTTP 200 within 5 seconds. The response body may be empty, or {"success": true}. Any non-2xx status triggers a retry.
Retry schedule
When a callback fails, we retry automatically on this schedule:
| Attempt | Delay |
|---|---|
| Retry 1 | after 10 seconds |
| Retry 2 | after 60 seconds |
| Retry 3 | after 5 minutes |
Verifying the callback
So you can prove a callback really came from Rolthe, every request carries a signature. The signing key is the address-level webhook_secret; if that address has none of its own, we fall back to the API-key-level webhook_secret.
The API-key-level webhook_secret is returned alongside api_key and api_secret when you create the key, and you can view or copy it any time from User portal → API keys. It is not the same key as api_secret: api_secret signs the requests you send us, webhook_secret verifies the callbacks we send you. Do not mix them up.
X-Signature: abc123def456...
X-Timestamp: 1704067200
Content-Type: application/jsonVerification steps
# 1. Read the signature from the request headers
received_signature = headers['X-Signature']
# 2. Take the raw request body (the JSON string — do not deserialise and re-serialise)
request_body = request.get_data(as_text=True)
# 3. Compute the expected signature
# note: sign with webhook_secret, not api_secret
expected_signature = HMAC_SHA256(request_body, webhook_secret)
# 4. Compare (must be a constant-time function)
if not hmac.compare_digest(received_signature, expected_signature):
return 403 # invalid signatureNote
Compare signatures with a constant-time function such as hmac.compare_digest. Using == returns early and leaks how far the comparison got, which in theory lets an attacker brute-force the signature byte by byte.
Receiver examples
from flask import Flask, request, jsonify
import hmac, hashlib
app = Flask(__name__)
# Address-level webhook_secret; falls back to the API-key-level one when unset
WEBHOOK_SECRET = "your_webhook_secret"
processed_txs = set() # use a database in production — an in-memory set is lost on restart
def verify_signature(req):
signature = req.headers.get('X-Signature', '')
if not signature or not WEBHOOK_SECRET:
return True # no secret configured, skip verification
# The raw body is required: re-serialising changes whitespace and key order, and the signature stops matching
body = req.get_data(as_text=True)
expected = hmac.new(
WEBHOOK_SECRET.encode(), body.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhook', methods=['POST'])
def webhook():
if not verify_signature(request):
return jsonify({"error": "invalid signature"}), 403
payload = request.get_json()
tx_hash = payload['data']['tx_hash']
# Idempotency: retries can deliver the same transaction more than once
if tx_hash in processed_txs:
return jsonify({"success": True})
processed_txs.add(tx_hash)
# Your business logic goes here, and it must be quick — 200 within 5 seconds
print(f"received {payload['data']['amount']} {payload['data']['token']}")
return jsonify({"success": True})const express = require('express');
const crypto = require('crypto');
const app = express();
// Keep the raw body for verification: express.json() parses it, and re-serialising changes the bytes
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString(); } }));
const WEBHOOK_SECRET = 'your_webhook_secret';
const processedTxs = new Set();
function verifySignature(req) {
const signature = req.headers['x-signature'] || '';
if (!signature || !WEBHOOK_SECRET) return true;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.rawBody)
.digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/webhook', (req, res) => {
if (!verifySignature(req)) return res.status(403).json({ error: 'invalid signature' });
const { tx_hash, amount, token } = req.body.data;
if (processedTxs.has(tx_hash)) return res.json({ success: true });
processedTxs.add(tx_hash);
console.log(`received ${amount} ${token}`);
res.json({ success: true });
});
app.listen(3000);Callback never arrived?
The console shows the response code and body of every delivery, and lets you resend by hand.