Real-Time Blockchain Notifications with Tatum: Stop Polling, Start Listening


%20(62).jpg)
A practical guide to event-driven blockchain architecture using webhooks
If you've ever built a blockchain application, you know the pain. You spin up a loop, poll the chain every few seconds, parse raw blocks, filter transactions, and pray you don't miss anything. It's expensive, fragile, and scales poorly.
There's a better way. Blockchain notifications event-driven webhooks that push updates to your server the moment something happens on-chain.
In this guide, I'll walk through how blockchain notifications work, why they matter, and how to set them up using Tatum's Notification API (v4) with practical code examples you can run today.
A blockchain notification is a webhook, an HTTP POST request fired to your server when a specific on-chain event occurs. That event could be:
Instead of your application constantly asking "did anything happen yet?", the blockchain tells you.
Think of it like the difference between refreshing your inbox every 10 seconds vs. getting a push notification when a new email arrives.
You could run your own node, subscribe to new blocks, decode every transaction, and maintain state yourself. Some teams do. But here's what that actually looks like:
Notifications abstract all of this away. You subscribe once, and the platform handles the monitoring, parsing, and delivery.
Here's the lifecycle of a blockchain notification.
You tell the notification service: "Watch this address on this chain, and POST to my webhook URL when something happens."
curl -X POST \
'https://api.tatum.io/v4/subscription?type=mainnet' \
-H 'Content-Type: application/json' \
-d '{
"type": "ADDRESS_EVENT",
"attr": {
"address": "0xF64E82131BE01618487Da5142fc9d289cbb60E9d",
"chain": "ethereum-mainnet",
"url": "https://your-app.com/webhook"
}
}'{
"id": "5e68c66581f2ee32bc354087"
}The response returns an object with a id field, a string identifier for the subscription. Store it.
The platform begins watching the blockchain. For EVM chains, this means tracking every block for transactions involving your address as sender, receiver, or through internal contract calls. You don't need to do anything here.
When a matching event is detected, your webhook receives an HTTP POST with a JSON payload:
{
"address": "0xF64E82131BE01618487Da5142fc9d289cbb60E9d",
"amount": "0.001",
"asset": "ETH",
"blockNumber": 2913059,
"counterAddress": "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990",
"txId": "0x062d236ccc044f68194a04008e98c3823271dc26…",
"type": "native",
"chain": "ethereum-mainnet",
"subscriptionType": "ADDRESS_EVENT"
}Key fields to note:
type tells you the asset category: native, token (ERC-20), erc721, erc1155, internal, or feeaddress and counterAddress Identify the parties involved (more on this below)txId lets you look up the full transaction on-chainWhen you no longer need monitoring, delete the subscription using the ID you stored earlier:
curl -X DELETE \
'https://api.tatum.io/v4/subscription/YOUR_SUBSCRIPTION_ID' \
-H 'Content-Type: application/json'This trips up a lot of developers, so let's be explicit.
For native asset transfers (ETH, MATIC, etc.):
counterAddress = the sender (from)address = the receiver (to)For token transfers (ERC-20, ERC-721, etc.):
address = the sender (from)counterAddress = the receiver (to)Yes, they're reversed depending on the asset type. Here's a quick reference:
Native (ETH): address = Receiver (To) · counterAddress = Sender (From)
Token (ERC-20): address = Sender (From) · counterAddress = Receiver (To)
NFT (ERC-721): address = Sender (From) · counterAddress = Receiver (To)
Tron exception: TRC-10 and TRC-20 tokens follow the native asset pattern counterAddress is the sender, address is the receiver.
Notifications work across a wide range of blockchains. Here are the major ones supported for ADDRESS_EVENT (the broadest type).
Each chain is referenced using its network identifier (e.g. ethereum-mainnet, polygon-amoy, bitcoin-testnet, solana-devnet). Both mainnet and testnet variants are available for most chains.
Not all subscription types are available on every chain. For example, internal transaction subscriptions (INCOMING_INTERNAL_TX, OUTGOING_INTERNAL_TX) are only available on EVM chains.
ADDRESS_EVENT is the catch-all, but you can be more specific:
INCOMING_NATIVE_TX Incoming native currency (ETH, MATIC, SOL, etc.)OUTGOING_NATIVE_TX Outgoing native currencyINCOMING_FUNGIBLE_TX Incoming ERC-20 / fungible tokensOUTGOING_FUNGIBLE_TX Outgoing ERC-20 / fungible tokensINCOMING_NFT_TX Incoming ERC-721 NFTsOUTGOING_NFT_TX Outgoing ERC-721 NFTsINCOMING_MULTITOKEN_TX Incoming ERC-1155 multi-tokensOUTGOING_MULTITOKEN_TX Outgoing ERC-1155 multi-tokensINCOMING_INTERNAL_TX Incoming internal (trace-level) transfersOUTGOING_INTERNAL_TX Outgoing internal (trace-level) transfersPAID_FEE Gas fee paymentsOUTGOING_FAILED_TX Failed outgoing transactionsFAILED_TXS_PER_BLOCK All failed transactions in a blockCONTRACT_ADDRESS_LOG_EVENT Smart contract log eventsUsing specific types reduces noise and is more efficient than filtering a broad ADDRESS_EVENT subscription.
Each subscription must be unique within your API key. Uniqueness depends on the type:
type + chain + address + urltype + chain + contractAddress + event + urltype + chain + urlInstead of receiving every event and filtering in your application code, you can define conditions at the subscription level. Only events matching all conditions trigger a webhook.
Example: Only notify for transfers above 1 ETH
{
"type": "ADDRESS_EVENT",
"attr": {
"chain": "ethereum-mainnet",
"address": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"url": "https://your-app.com/webhook",
"conditions": [{ "field": "value", "operator": ">", "value": "1000000000000000000" }]
}
}Example: Only notify for incoming USDT above 1,000 USDT
{
"type": "INCOMING_FUNGIBLE_TX",
"attr": {
"chain": "ethereum-mainnet",
"address": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"url": "https://your-app.com/webhook",
"conditions": [
{
"field": "contractAddress",
"operator": "==",
"value": "0xdAC17F958D2ee523a2206206994597C13D831ec7"
},
{ "field": "value", "operator": ">=", "value": "1000000000" }
]
}
}Available filter fields: value, contractAddress, from, to, tokenId, tokenMetadata.type, tokenMetadata.symbol.
Important: Amount values are always in the chain's smallest unit. 1 ETH = "1000000000000000000" (18 decimals). 1 USDT = "1000000" (6 decimals). Always pass as a string.
You can control when a notification fires relative to block confirmation:
confirmed Fires as soon as the transaction is confirmed. Optimised for speed.final Waits for full block confirmation depth. Optimised for settlement confidence.Use confirmed for real-time UX updates. Use final for payment processing or treasury operations where you can't afford a reorg reversal.
By default, webhooks use a legacy payload format. But Tatum provides a built-in enriched template that returns structured, human-readable fields with token metadata, and it's the recommended default.
You choose a template by passing templateId when creating a subscription. Available options:
enriched Structured payload with token metadata (recommended)enriched_with_raw_data Enriched data plus raw transaction datalegacy Original flat format (default if omitted)Custom ID Your own template (see below)Example: Subscribe with the enriched template
curl -X POST \
'https://api.tatum.io/v4/subscription?type=mainnet' \
-H 'Content-Type: application/json' \
-d '{
"type": "ADDRESS_EVENT",
"attr": {
"address": "0xF64E82131BE01618487Da5142fc9d289cbb60E9d",
"chain": "ethereum-mainnet",
"url": "https://your-app.com/webhook"
},
"templateId": "enriched"
}'Enriched payload example:
{
"data": {
"kind": "transfer",
"blockHash": "0x1234567890abcdef…",
"blockNumber": 18500000,
"blockTimestamp": 1699123456,
"txId": "0xabcdef1234567890…",
"currency": "ETH",
"txTimestamp": 1699123456,
"from": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"to": "0x8ba1f109551bD432803012645Ac136Ddd64DBA72",
"value": "1000000000000000000",
"contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"tokenId": "12345",
"additionalData": { "gasUsed": "21000", "gasPrice": "20000000000" },
"tokenMetadata": {
"type": "nft",
"decimals": 0,
"symbol": "NFT",
"name": "My NFT",
"uri": "https://api.example.com/metadata/12345"
},
"subscriptionId": "64f1a2b3c4d5e6f7g8h9i0j1",
"subscriptionType": "ADDRESS_EVENT"
}
}Notice how the enriched payload gives you from/to directly (no more address/counterAddress confusion), includes tokenMetadata with symbol and decimals, and provides additionalData with gas details.
Creating a custom template:
If you need full control over the payload shape, create your own:
curl -X POST \
'https://api.tatum.io/v4/subscription/template' \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"format": "json",
"keys": {
"amount": "value",
"sentFrom": "from",
"sentTo": "to"
}
}'This returns a template ID you can then reference in any subscription.
A webhook endpoint is a publicly accessible URL. Anyone who discovers it could send fake payloads. Two defenses:
When HMAC is enabled, every webhook includes an x-payload-hash header containing a cryptographic digest. Your server reconstructs the hash using your shared secret and compares. If they match:
This is the recommended approach.
Tatum publishes its IP ranges at ips.tatum.com/ips.json. You can whitelist these in your WAF. This works, but HMAC is more robust since IP ranges can change.
This should go without saying, but always use HTTPS for your webhook URLs to encrypt payloads in transit.
Webhooks fail. Servers restart. Networks hiccup. Tatum handles this with automatic retries:
The retry interval follows an exponential backoff formula:
delay (seconds) = 15 × 2.9516^(retryCount - 1)Which produces intervals of roughly: 15s, 44s, 2min, 6min, 19min, 56min, 2hr, 8hr, 24hr.
You can also query past webhook deliveries, including failures, to audit what was sent and what your server responded:
curl -X GET \
'https://api.tatum.io/v4/subscription/webhook?pageSize=10'Each record includes the HTTP response code, response body, retry count, and whether the delivery ultimately failed.
Here's how this typically fits into a production system:
Blockchain (ETH, BTC, MATIC…) → Tatum Notification Service → Your Webhook Endpoint (POST /webhook) → Message Queue (SQS, Redis, RabbitMQ) → Worker Service (process event, update DB, notify user)
Best practice: Your webhook endpoint should do minimal work to validate the HMAC, enqueue the payload, and return 200 OK immediately. Heavy processing (database writes, user notifications, business logic) happens asynchronously in a worker. This keeps your webhook responsive and prevents timeouts that trigger unnecessary retries.
Blockchain notifications replace the fragile poll-parse-filter pattern with a clean, event-driven architecture. You define what you care about, which address, which chain, which event type, under what conditions, and the platform handles the rest.
The result is less infrastructure, fewer missed events, and an application that reacts to the blockchain in real time instead of lagging behind it.
If you're building anything that needs to know when something happens, on-chain payment confirmations, wallet activity monitoring, NFT tracking, and DeFi position management notifications are the foundation to build on.
Building with blockchain notifications? I'd love to hear about your use case. Drop a comment or connect with me.
Build blockchain apps faster with a unified framework for 60+ blockchain protocols.