Website
Dashboard
Website
Dashboard
  • πŸ‘‰ Welcome
  • 🧐 How to set up an account
  • πŸ“ˆ Monitoring and analytics
  • πŸ” Introducing coins vs tokens
  • πŸ‘¨β€πŸ’Ό The meaning of owning crypto
  • πŸ’° Wallets
  • πŸ“œ Supported Blockchains
  • GITHUB
    • 🐘 SDK PHP
    • πŸ“ SDK Python
    • πŸ‘¨β€πŸ’» SDK GOLANG
    • ✳️ SDK NODE JS
    • ✳️ PHP Wallet
  • API REFERENCE
    • πŸ”‘ Authentication & base URL
    • Bitcoin
      • πŸ” Info
        • 🧐 Check BTC node health
        • πŸ“œ Get latest Bitcoin block
      • πŸ†Ž Block
        • πŸ“ˆ Get Bitcoin block by height
        • πŸ‘¨β€πŸ’» Get Bitcoin block by hash
        • πŸ“‘ Get transaction count in block by height
        • πŸ“‘ Get transaction count in block by hash
      • πŸ’° Transactions
        • πŸ‘¨β€πŸ’» Get Bitcoin transaction by hash
        • πŸ“ Send pre-signed raw Bitcoin transaction
      • πŸ‘¨β€πŸ’Ό Account
        • πŸ’° List BTC addresses managed by node wallet
        • πŸ†Ž Get BTC received amount for address
      • πŸ” Watch-only addresses
        • πŸ” Register an account zpub
        • πŸ“œ List registered extended public keys
        • πŸ†Ž Issue the next BTC address
        • πŸ“‘ History of issued BTC addresses
        • ❌ Delete a registered zpub
      • πŸ” JSON-RPC
        • πŸ” JSON-RPC
    • Ethereum
      • πŸ” Info
        • 🧐 Check ETH node health
        • πŸ“œ Get latest Ethereum block
      • πŸ†Ž Block
        • πŸ“ˆ Get Ethereum block by height
        • πŸ‘¨β€πŸ’» Get Ethereum block by hash
        • πŸ“‘ Get transaction count in block by height
        • πŸ“‘ Get transaction count in block by hash
      • πŸ’° Transactions
        • πŸ‘¨β€πŸ’» Get Ethereum transaction by hash
        • πŸ‘¨β€πŸ’» Get Ethereum transaction receipt by hash
        • πŸ“ Send pre-signed raw Ethereum transaction
      • πŸ‘¨β€πŸ’Ό Account
        • πŸ” List ETH accounts managed by node
        • πŸ†Ž Get ETH balance for address
        • πŸ†Ž Get transaction count (nonce) for address
        • πŸ‘‰ Estimate gas for transaction
        • πŸ“‘ Read-only contract call (eth_call)
        • πŸ‘¨β€πŸ’Ό Send transaction from node-managed account
      • πŸ” Watch-only addresses
        • πŸ” Register an account xpub
        • πŸ“œ List registered extended public keys
        • πŸ†Ž Issue the next ETH address
        • πŸ“‘ History of issued ETH addresses
        • ❌ Delete a registered xpub
      • πŸ” JSON-RPC
        • πŸ” JSON-RPC
    • Tron
      • πŸ” Info
        • πŸ“‘ Get TRX node info
        • 🧐 Check Tron node health
        • πŸͺ™ List supported TRC20 tokens
      • πŸ†Ž Block
        • πŸ“œ Get latest Tron block
        • πŸ“œ Get latest N blocks
        • 🟠 Get block by number
        • πŸ‘¨β€πŸ’» Get block by hash
      • πŸ’° Transactions
        • πŸ‘¨β€πŸ’» Get transaction by hash
        • πŸ†Ž Get all transactions for an address
        • πŸ“ Create an unsigned transaction
        • πŸ“‘ Broadcast a signed transaction
      • πŸ‘¨β€πŸ’Ό Account
        • πŸ†Ž Validate a Tron address
        • πŸ” Get account information
        • πŸ’° Get account balance
        • πŸ’° Get all asset balances
        • πŸ’΅ Get a single TRC20 balance
        • ⚠️ Create a new Tron address (deprecated)
        • ⚠️ Generate a private key from a mnemonic (deprecated)
        • ⚠️ Send TRX from one address to another (deprecated)
        • ⚠️ Send USDT (TRC20) from one address to another (deprecated)
      • πŸ“Š Schemas
      • πŸ” JSON-RPC
    • Webhooks
      • ✳️ Management
        • πŸ‘¨β€πŸ“œ List webhooks
        • 🧐 Create webhook
        • πŸ‘‰ Get webhook
        • πŸ‘¨β€πŸ” Update webhook
        • ❌ Delete webhook
        • πŸ”‘ Rotate signing secret
      • πŸ“¨ Receiving notifications

πŸ“¨ Receiving notifications

The management endpoints only describe how a subscription is created. This page describes the other half: what the platform actually sends to webhook_url, when it sends it, and what a receiver has to handle.

The request your endpoint receives

POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Settla-Webhook/1.0
X-Settla-Timestamp: 1757419541
X-Settla-Signature: v1=6f3a0e1d…

The two X-Settla-* headers let you prove the request came from us β€” see Verifying that a request came from us.

{
  "type": "new_transaction",
  "address": "TLWKepDBC8RZjAhtWyNqASyzgyVyHUuS7o",
  "transaction": {
    "txID": "aa451147fe9bc4514ebc6adc58582eabd480c2869a649571a9262d2e95dae9cd",
    "from": "TJbUDLSLdiuh1TdZ5hUEKMuboJ3X41vwxG",
    "to": "TLWKepDBC8RZjAhtWyNqASyzgyVyHUuS7o",
    "value": "7975000000",
    "currency": "USDT",
    "block_number": 75784535,
    "timestamp": "2025-09-16T09:05:30.000000Z"
  },
  "timestamp": "2025-09-16T09:05:41.000000Z"
}

πŸ› οΈ Properties

NameTypeDescription
typestringEvent type. new_transaction is the only value the platform currently sends.
addressstringThe address from your subscription that this transaction touched. transaction.to is checked first and transaction.from second, so if both sides of the transfer are yours you get the recipient.
transaction.txIDstringTransaction hash. Tron and Bitcoin use the txid as-is, Ethereum uses the hash field of the transaction.
transaction.fromstringSender address.
transaction.tostringRecipient address. See the per-coin table below: for token transfers this is not always the address you expect.
transaction.valuestringAmount in the minimal units of currency, decimal string. Never a number β€” amounts exceed the range of a IEEE-754 double.
transaction.currencystringAsset symbol, see below. Falls back to the contract address when the token is not in the platform's list.
transaction.block_numberintegerHeight of the block the transaction was included in.
transaction.timestampstringBlock time, ISO 8601.
timestampstringWhen the notification was generated, ISO 8601. Differs from the block time by the scanner lag.

There is no confirmations field, no signature header and no unique event id. All three are covered in their own sections below.

Amounts and the currency field

Coincurrencyvalue is inDecimals
TRX, native transferTRXSUN6
TRX, TRC-20 transferToken symbol (USDT) or, for tokens outside the platform list, the contract address in Base58Minimal token unitsPer token; USDT on Tron has 6
TRX, TRC-10 transferThe asset name as returned by the node (hex-encoded token id)Minimal token unitsPer token
BTCBTCSatoshi8
ETHETHWei18

Which transactions match your addresses

Addresses are matched as exact strings against the sender and the recipient of every transfer in a scanned block. There is no normalisation, so the representation matters:

  • ETH β€” the scanner lower-cases addresses before matching. Store your addresses in lower case; a checksummed 0xAbC… address never matches.
  • TRX β€” Base58 (T…). Hex (41…) addresses are converted by the scanner, so subscribe with the Base58 form.
  • BTC β€” the address as it appears in scriptPubKey, i.e. bech32 in lower case for bc1… outputs.

What triggers a notification per coin:

ChainIncomingOutgoing
TRX, nativeβœ…βœ…
TRX, TRC-20 (USDT included)βœ… β€” the recipient is decoded from the contract call, value is the token amountβœ…
TRX, TRC-10βœ…βœ…
BTCβœ… β€” one notification per matching output⚠️ only if your address is the first input of the transaction
ETH, nativeβœ…βœ…
ETH, ERC-20 (USDT, USDC)❌ not delivered β€” see below⚠️ delivered, but to is the token contract and value is "0"

Notes:

  • Tron contract calls. Only the standard transfer(address,uint256) call (and one proxy variant) is decoded. Any other contract call made by one of your addresses is still reported, but with the contract as to and "0" as value β€” check for a non-zero value before treating a notification as a payment.
  • Bitcoin. A transaction is reported per output: one notification for each output paying one of your addresses, with value being that output's amount. Outputs paying back an input address (change) are skipped. On the sending side only the first input address is reported as from, so an outgoing payment whose first input is not yours produces no notification.
  • Ethereum tokens. For an ERC-20 transfer the transaction's recipient is the token contract, not the beneficiary, and matching runs on that field. As a result an incoming USDT/USDC payment on Ethereum does not trigger a webhook. If you need USDT notifications today, use USDT on Tron (coin=TRX), where token transfers are decoded in full. Support for ERC-20 recipients is a known gap and is being worked on.

Addresses start being watched from the next block the scanner processes. There is no backfill: transactions that happened before you created or updated the subscription are not replayed. To recover the past on Tron use GET /api/v1/trx/address/{address}/transactions; on Bitcoin and Ethereum read the blocks you need through the block endpoints.

Confirmations and reorgs

The notification is sent as soon as the block containing the transaction is processed, which is normally a few seconds after the block is produced. In practice this means one confirmation. The payload carries no confirmations field β€” track depth yourself by comparing transaction.block_number with the current height from GET /api/v1/{trx,btc,eth}/block/last, and release goods only at the depth your risk model requires.

Consequences to design for:

  • A chain reorganisation is not reported. There is no cancellation or transaction_failed event; a notification for a transaction that later drops out of the chain is never retracted.
  • On Ethereum the scanner reads block bodies without receipts, so a reverted transaction is reported like a successful one. Check the status with GET /api/v1/eth/transactions/{hash}/receipt before crediting an ETH payment. On Tron only transactions with contractRet: SUCCESS are reported, and Bitcoin has no equivalent case.

Delivery, retries and the circuit breaker

PropertyValue
MethodPOST, body application/json
SuccessAny 2xx response
Timeout15 seconds
RedirectsNot followed β€” a 3xx counts as a failure
Retries5 attempts total, with 10 s, 30 s, 60 s and 300 s between them
After the last attemptThe delivery is counted as failed

Every attempt β€” successful or not β€” is recorded and visible in the dashboard under Webhook statistics, including the HTTP status, the response time and the response body of successful deliveries.

After 20 consecutively failed deliveries the subscription is switched off: is_active becomes 0, the owner gets an email, and nothing is delivered until you re-enable it with a PUT request. A single failed attempt does not count β€” only a delivery that has exhausted all its retries. One successful delivery resets the counter.

A subscription is also switched off immediately, without retries, if its URL stops being publicly reachable β€” for example when the host starts resolving into a private or loopback range. The reason is shown in the dashboard.

Duplicates and idempotency

The same transaction may be delivered more than once, and the payload carries no unique event id. Make the receiver idempotent: key your processing on transaction.txID + address (add transaction.block_number if you want to observe re-deliveries), and treat a repeat as a no-op rather than a second payment.

Respond 200 as soon as you have stored the event and do the heavy work asynchronously β€” the 15-second timeout applies to your whole response.

Verifying that a request came from us

Every delivery is signed with an HMAC-SHA256 keyed on a secret that belongs to that one subscription. Two headers carry it:

HeaderExample
X-Settla-Signaturev1=6f3a…c1 β€” the scheme version and the signature in lowercase hex
X-Settla-Timestamp1757419541 β€” Unix seconds, when the request was signed

What is signed is not the body alone. The signed string is the timestamp, a literal dot, and the raw request body:

<X-Settla-Timestamp> "." <raw body bytes>

The timestamp is inside the signature on purpose: signing only the body would let anyone replay a delivery captured months ago with a fresh-looking header.

Getting the secret

The secret is created together with the subscription and comes back in the secret field of every webhook object β€” POST /api/v1/webhooks, GET /api/v1/webhooks, GET /api/v1/webhooks/{id} and PUT /api/v1/webhooks/{id}. It looks like whsec_ followed by 64 hex characters, and the whole string, prefix included, is the HMAC key.

Subscriptions created before signing was introduced were given a secret automatically β€” you do not have to recreate them, just read it off the list:

curl -sS https://settla.net/api/v1/webhooks \
  -H "X-API-KEY: $SETTLA_API_KEY"
{
  "data": [
    {
      "id": 1387,
      "coin": "trx",
      "webhook_url": "https://example.com/hooks/settla",
      "secret": "whsec_4f8c1a9d7e2b6035c4a1f8d9e0b3c7a25f6e1d4c8b0a9372e5d6c1f804b3a7e29",
      "addresses": ["TLWKepDBC8RZjAhtWyNqASyzgyVyHUuS7o"],
      "is_active": 1
    }
  ]
}

The value is stable β€” it is the same key on every read, so there is no one-time-display step to miss, unlike an API key. It is only ever returned to the owner of the subscription, authenticated with that owner's API key.

To roll the key over:

curl -sS -X POST https://settla.net/api/v1/webhooks/1387/secret \
  -H "X-API-KEY: $SETTLA_API_KEY"

The response is the webhook object with the new secret. The old one stops working immediately β€” there is no overlap window, so deploy the new value to your receiver before rotating, or accept a short gap.

Checking the signature

Verify against the raw request body, before any JSON parsing: re-serialising the payload changes the bytes and the signature will not match.

// PHP
$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_SETTLA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SETTLA_SIGNATURE'] ?? '';

// НС пускаСм ΠΏΡ€ΠΎΡ‚ΡƒΡ…ΡˆΠΈΠ΅ доставки: подпись Π±Π΅Π· ΠΎΠΊΠ½Π° свСТСсти Π·Π°Ρ‰ΠΈΡ‰Π°Π΅Ρ‚ ΠΎΡ‚
// ΠΏΠΎΠ΄Π΄Π΅Π»ΠΊΠΈ, Π½ΠΎ Π½Π΅ ΠΎΡ‚ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π° ΠΊΠΎΠ³Π΄Π°-Ρ‚ΠΎ ΠΏΠ΅Ρ€Π΅Ρ…Π²Π°Ρ‡Π΅Π½Π½ΠΎΠ³ΠΎ запроса.
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);

if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}
// Node.js (Express: express.raw({ type: 'application/json' }))
const crypto = require('crypto');

function verify(req, secret) {
  const timestamp = req.get('X-Settla-Timestamp') || '';
  const signature = req.get('X-Settla-Signature') || '';

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected =
    'v1=' +
    crypto
      .createHmac('sha256', secret)
      .update(timestamp + '.' + req.body) // req.body is a Buffer, not an object
      .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Compare in constant time (hash_equals, timingSafeEqual) rather than with ===, and reject anything whose X-Settla-Signature does not start with v1= β€” a future scheme will use a different prefix, and silently accepting it would defeat the point.

The dashboard's Test button signs its delivery with the same key, so you can check your verification code without waiting for a real transaction.

Still worth doing

  1. Keep the webhook_url unguessable β€” a random path segment over HTTPS.
  2. Treat the payload as a hint, not as truth: re-read the transaction with GET /api/v1/{trx,btc,eth}/transactions/{hash} before crediting anything. A valid signature proves the message came from us, not that the deposit is still confirmed after a reorg.

The β€œTest” button in the dashboard

The test delivery in the dashboard sends a different, synthetic payload, kept for backwards compatibility with the original integration:

{
  "event": "transaction_confirmed",
  "coin": "TRX",
  "transaction_id": "test_5f2c9a7d3b1",
  "from_address": "test_sender_address",
  "to_address": "TLWKepDBC8RZjAhtWyNqASyzgyVyHUuS7o",
  "amount": "1000000",
  "confirmations": 6,
  "block_height": 12345678,
  "timestamp": "2025-09-16T12:00:00.000000Z",
  "test": true
}

Use it to check reachability and TLS, not to validate your parser: a payload with "test": true never has the shape of a real notification. Build against the new_transaction format documented above and ignore or explicitly skip anything carrying test: true.

How many addresses can one webhook hold?

addresses must contain at least one entry, and each address is limited to 255 characters. There is no enforced upper bound on the number of entries, but matching is linear in the size of the list, so keep a subscription under about 1000 addresses and split larger pools across several subscriptions. Nothing prevents several subscriptions of the same coin from pointing at the same URL.

PUT replaces the whole list rather than merging: send the complete set of addresses on every update, including the ones that are not changing.

Watching addresses you generated yourself

A subscription needs nothing but the address β€” the platform never asks for a private key, a mnemonic or an xpub in order to watch it. Addresses generated in your own wallet are watched exactly like addresses generated through the API.

This is the recommended replacement for the deprecated key-handling endpoints on Tron (POST /api/v1/trx/account, POST /api/v1/trx/account/privatekey): derive the address in your wallet at the standard Tron path m/44'/195'/0'/0/i, subscribe it with POST /api/v1/webhooks, and sign outgoing transfers on your side with POST /api/v1/trx/transactions/create + POST /api/v1/trx/transactions/broadcast.

Prev
✳️ Management