A webhook is a message our platform sends to your server when something happens: a deposit is credited, a payout is confirmed, a card is topped up. It saves you from asking "did anything change?" every few seconds.
But a webhook is also a door into your system. Anyone on the internet can send a request to that URL. The same event can arrive twice. An event can arrive late, or not at all while your server is down. If your handler is careless, any of these can turn into money credited to the wrong person.
This guide shows how to receive crypto payment events safely, step by step, with the mistakes we see most often.
#What a webhook is (and is not)
Think of a webhook as a doorbell. It tells you that someone may be at the door. It does not tell you who it is for certain, and it does not open the door for you.
On our platform every delivery is an HTTPS POST to the endpoint you registered. It carries two headers:
x-cryptoinfra-event— the event type, for exampledeposit.credited;x-cryptoinfra-signature— a hex HMAC-SHA256 of the exact request body, made with your endpoint's secret.
Delivery is at-least-once. That means we promise to try until you answer with a success code, and as a result you may get the same event more than once. Plan for that from day one.
#The events you can expect
The main money events for a TRON wallet business are:
| Event | Fires when |
|---|---|
deposit.credited | A deposit has enough confirmations and the user's balance went up |
withdrawal.confirmed | An on-chain payout is confirmed on TRON |
withdrawal.failed | A payout could not complete |
card.topup | A card load finished |
card.status | A card changed status |
card.transaction | A card was used to pay |
The full list, with payload examples for each, is in the webhooks docs. One thing to know early: swaps emit no webhook. If you offer USDT and TRX swaps, poll the swap order until it reaches a final status.
#Step 1: register a clean endpoint
Create the endpoint from the dashboard or the developer API. A few rules make life easier later:
- Use HTTPS and a public hostname.
- Register the final URL. Redirects are not followed, and a redirect counts as a failed delivery.
- Use a path that is only for this purpose, like
/webhooks/cryptoinfra. - Save the secret the moment you see it. It is shown once and never again.
Treat that secret like a password. Keep it in your secrets store, not in your code or your git history. If it ever leaks, regenerate it. The old secret stops working at once, so do it at a time you can watch.
#Step 2: verify the signature before anything else
This is the most important step. Without it, anyone who learns your URL can send a fake deposit.credited and get credited for money they never sent.
To verify:
- Read the raw bytes of the request body, before any JSON parser touches it.
- Compute HMAC-SHA256 of those bytes with your endpoint secret.
- Compare your result with the
x-cryptoinfra-signatureheader using a constant-time comparison. - If they do not match, or the header is missing, reject the request and stop.
The most common bug is step 1. Many web frameworks parse JSON automatically. If you sign the parsed object turned back into text, the bytes change — key order, spaces, number format — and the signature never matches. Developers then "fix" this by turning verification off. Do not do that. Configure your route to give you the raw body instead. The docs have ready examples for Node.js and Python.
#Step 3: answer fast, work later
Each delivery waits a short time for your answer. If your handler is slow — because it calls another service, sends an email or writes to many tables — the delivery is marked failed and retried. Now you process the same event twice.
The safe pattern is:
- Verify the signature.
- Put the event on a queue or into a jobs table.
- Reply with a
2xxcode right away. - Let a background worker do the real work.
Your answer only means "I received it". It does not mean "I finished handling it".
#Step 4: deduplicate every event
Because delivery is at-least-once, you will see duplicates. A timeout that actually succeeded, a brief network problem, or an error on your side all cause a retry.
There is no separate delivery id. Deduplicate on the pair of event type and entity id — the deposit id, withdrawal id, card id or order id in the payload. Keep a small table with that pair as its primary key. When an event arrives, try to insert it. If the row already exists, you have handled it before: reply with success and stop.
One exception: status events such as card.status can fire many times for the same card, once per change. Key those on event type, entity id and the new status.
Then make the work itself safe to repeat. For example, crediting an order should check "is this order already paid?" before it adds anything. Two layers of protection beat one.
#Step 5: treat the webhook as a hint, not the truth
Even a valid, signed webhook only says that something may have changed. Before you credit a customer, refund someone or tell support that money moved, read the record from the API:
- the withdrawal read for payouts;
- the user balances read for deposits;
- the card order read for card loads;
- the swap order read for swaps.
This matters most when your server was down for a long time. Retries do stop after a while. A webhook that never arrived must not leave money stuck in your system. Run a regular sweep that looks at every record that is not yet final and checks it against the API, whether a webhook came or not. Our guide on how crypto deposits are confirmed on TRON explains why a deposit is only final after it is credited.
#Step 6: test the real path before going live
Do not wait for real money to test your handler. Use the sandbox and the test button for your endpoint. It sends a signed test event through the real delivery system, so it checks your actual signature code, not a simulation. You can pick the event type you want to rehearse.
Test at least these cases:
- a valid event is accepted and queued;
- a changed body is rejected;
- a missing signature header is rejected;
- the same event sent twice is handled once;
- a slow worker does not slow down the reply.
#A safe webhook checklist
- [ ] The endpoint is HTTPS, public, and the final URL (no redirects).
- [ ] The secret is stored like a credential and can be rotated.
- [ ] The raw body is verified with HMAC-SHA256 in constant time.
- [ ] Missing or wrong signatures are rejected, never "logged and processed".
- [ ] The handler replies with
2xxquickly and queues the work. - [ ] Events are deduplicated on event type and entity id.
- [ ] Crediting and refunding are idempotent on their own.
- [ ] Money decisions are confirmed with an API read.
- [ ] A reconciliation sweep runs on records that are not yet final.
- [ ] Swaps are polled, because they send no webhook.
#Common mistakes
| Mistake | What goes wrong | Fix |
|---|---|---|
| No signature check | Anyone can fake a payment | Verify every request, reject failures |
| Signing parsed JSON | Valid events fail, so checks get turned off | Verify the raw bytes |
| Normal string compare | Timing leaks can help an attacker | Use constant-time compare |
| Heavy work before replying | Timeouts cause repeats | Reply first, work in the background |
| No deduplication | A deposit is credited twice | Unique key on event type and entity id |
| Trusting the payload alone | Stale or missing events strand money | Confirm with the API, run a sweep |
| Waiting for a swap webhook | The flow waits forever | Poll the swap order |
#In summary
A safe webhook handler does four things: it checks the signature on the raw body, it answers fast, it ignores duplicates, and it confirms money decisions against the API. Do these well and webhooks become a reliable, real-time feed for your wallets and payouts, instead of a risk.
Want signed webhooks, per-user TRON addresses and on-chain payouts under your own brand? Sign up free and send your first test event from the sandbox today.