The sandbox is not a formality you click through. It's the only environment where a missing webhook handler costs you nothing.
Most failed go-lives we see don't fail because of crypto. They fail because someone tested the happy path in sandbox, flipped to live keys on a Friday, and discovered on Monday that nothing in their system reacts when an invoice gets overpaid. The deposit landed. The funds are real. The order never shipped.
This is the checklist we walk every new integration through before we'll bless a production cutover. It's ordered deliberately: each gate is cheap to clear in sandbox and expensive to discover in production. If you're starting from zero, read the how-to-set-up guide first, then come back here to harden it.
1. KYB before you write a line of code
Know-Your-Business verification is the one gate that doesn't care how good your engineering is. You can build a flawless integration against sandbox and still not be able to move a single dollar on live keys until KYB clears.
So start it on day one, in parallel with development. It is not the last box you tick; it's the long pole that runs alongside everything else. Have your business registration, beneficial-owner details, and settlement preferences ready before you open the dashboard. The getting-started FAQ spells out what's actually required, and it's less than most teams assume.
The mistake: treating KYB as a release-week task. It isn't a code change you can hotfix.
2. Scope your API keys to exactly what the service does
Every integration we've seen leak credentials had one thing in common: a single key that could do everything. Don't mint that key.
Your invoice-creation service needs to create invoices. It does not need to release payouts. Cut a separate key per concern, give each the narrowest permission set that lets it do its job, and keep the live and test keys in different secret stores so nobody copies the wrong one into the wrong environment. The only request header that ever carries the key is X-API-Key — there is no second auth header to get wrong, which means there's exactly one secret per service to protect.
The mistake: one omnipotent key in one .env file, shared across three services and a laptop.
3. Register the webhook and verify the signature
Polling the API to ask "is it paid yet?" works in a demo and falls over in production. The deposit confirms on-chain on the network's schedule, not yours; the only reliable signal that something happened is the webhook.
So register your endpoint in sandbox and wire a real handler — not a 200 OK stub. At minimum you handle invoice.paid. Then handle the events that mean almost paid: invoice.confirming, invoice.overpaid, invoice.underpaid, and invoice.expired. And before any of those handlers touch your database, verify the HMAC signature over the raw request bytes, not the parsed JSON. Parsing reorders keys and rewrites whitespace; the signature was computed over the bytes that arrived. If verification fails, you drop the event and move on.
We wrote a whole post on why this is the contract, not a feature — see designing webhooks that survive everything. The short version: an unsigned webhook handler is an open door, and a webhook you don't react to is a payment you didn't notice. The webhooks product page lists every event and its envelope.
The mistake: a handler that logs the body and returns 200, with the signature check "to be added later."
4. Put idempotency_key on every create
This is the cheapest insurance in the entire integration, and the one most teams skip because nothing breaks in the demo.
Networks retry. Load balancers retry. Your own code retries when a request times out but actually succeeded server-side. Without protection, a retried "create invoice" call mints two invoices, and a retried "create payout" call tries to move money twice.
The fix is one field. Every create — invoice or payout — takes an idempotency_key in the JSON request body (it is snake_case, in the body, not an Idempotency-Key header). Send the same key for the same logical action and the second call returns the first call's result instead of doing the work again. Generate it from something stable on your side — your order ID, not a fresh UUID per attempt, or you've defeated the point.
Test it in sandbox the obvious way: fire the same create twice and confirm you get one entity back, not two.
The mistake: generating a new key on every retry, which is the same as having no key at all.
5. Decide what underpaid and overpaid mean for your business
This is the gate that separates integrations that survive contact with real customers from the ones that page someone at 2 a.m.
A customer pays from an exchange that skims a withdrawal fee, and the amount lands a few cents short: that's underpayment, and you get invoice.underpaid. A customer fat-fingers an extra zero, or sends from a wallet that rounds up: that's overpayment, and you get invoice.overpaid. Both are real money sitting in your account against an invoice that isn't cleanly "paid."
You cannot punt on these. Decide the policy before go-live:
- Underpaid — do you ship anyway, request a top-up, or refund the partial? A small tolerance is reasonable; "always refund a 2-cent shortfall" is a support nightmare.
- Overpaid — do you credit the surplus, refund it, or hold it? Pick one and make the handler do it automatically.
Wire both event handlers in sandbox and force the conditions — most teams have never seen their own underpaid path run, because the happy path never triggers it. The invoicing product page covers how amounts, expiry, and rate lock interact, which is the context these two states live in.
The mistake: assuming every payment lands exactly on the invoiced amount. None of them do, eventually.
6. Only now, flip to live keys
If — and only if — gates 1 through 5 are green, you cut over. KYB cleared. Keys scoped. Webhook registered, signed, and verified. idempotency_key on every create. Underpaid and overpaid handlers wired and tested.
Then the flip itself is small: swap the test key for the live key in your secret store, point your webhook registration at production, and run one real low-value transaction end to end before you announce anything. Watch it travel invoice.confirming → invoice.paid through your own handlers, in production, with real funds. If payouts are in scope, remember they enter pending-approval and are released from the dashboard before money moves — confirm someone on your side knows they own that click.
The flip is the boring part. It should be boring. Everything that makes it boring happened in the five gates before it.
The order is the point
You could do these in any sequence and still ship. But run them in this order and the expensive bugs surface while they're still free — in sandbox, against test keys, with no real money and no customer waiting on an order that silently didn't trigger.
The API integration page is the reference for the request shapes behind each of these gates. This checklist is the order we hand it to you in.
S. Brandt, halfin solutions