Going Live
Account verification, production credentials, webhook hardening, and the checklist to clear before your first live invoice.
Moving from a testnet run to real money changes three things: the account has to be verified, the credentials you deploy are the ones that can actually spend, and every failure now costs somebody something. This guide covers what to check, in the order it matters.
Get the account verified
A merchant record carries two flags. isActive starts true; isVerified
starts false. Registration gives you a working account immediately โ you
can sign in, create points, and run testnet payments โ but verification is
handled by Havala out of band, and you should confirm the account is verified
before you point production traffic at it.
Individual users have their own state. UserStatus is PENDING, ACTIVE or
SUSPENDED, and a PENDING user is refused at login with 401 EMAIL_NOT_VERIFIED. Make sure everyone who will need dashboard access on launch
day has actually completed that, rather than discovering it at 2am.
Failed logins are counted per email and IP: by default, five bad attempts lock
that pair out for fifteen minutes with 401 TOO_MANY_ATTEMPTS. A deploy script
that retries a login in a loop will lock out the account it is trying to use.
Production credentials
Credentials belong to a merchant point, so "going live" means deciding which points exist in production and who holds their secrets.
Use a separate merchant point for production, not the one you tested with. Each point has its own key pair, its own webhook URL and its own payment history, so a separate point gives you clean reconciliation and lets you revoke staging access without touching production.
Two properties of the credential model to plan around before launch:
apiSecretis visible in full to anyone who can view the point. The dashboard does not hide it after issuing, so dashboard access that can list merchant points is access to every gateway signing secret the account holds. Scope it accordingly:MerchantUserRoleisOWNER,MANAGER,ACCOUNTANTorVIEWER, and most people who need the dashboard do not needOWNER.- Deactivating a point disables its gateway API key. Clearing the point's active flag in the dashboard is a kill switch for that point's traffic โ including your own ability to read back what happened to payments already in flight. Use it deliberately.
Deployment hygiene, in short: secrets in your secret manager, never in a repo, never in a browser bundle, never in a log line next to the canonical string. Rotate on staff changes rather than on a calendar.
Rotating credentials
Rotation is a dashboard action, and there are two shapes of it. Both take effect immediately, with no overlap window:
| Rotation | Changes | Also breaks |
|---|---|---|
| Regenerating the merchant point's credentials | apiKey and apiSecret | Webhook verification, and anything that addresses the point by apiKey |
| Rotating the webhook signing secret | apiSecret only | Gateway request signing โ it is the same stored value |
That second row is the one that surprises people. The webhook signing secret and the gateway signing secret are not two secrets; they are one column. Rotating "the webhook secret" invalidates your request signing at the same instant.
There is no previous-secret grace period in either direction. The moment the
rotation lands, requests signed with the old secret fail with
SIGNATURE_INVALID and deliveries are signed with the new one. A freshly
rotated secret is shown to you at the moment it is minted, and some dashboard
views show only its first few characters afterwards โ capture it when it
appears rather than assuming you can read it back.
So: deploy the new values to every process that signs or verifies, in one window, and expect a gap. Do it during low traffic, not during a checkout peak.
Configure the point for production
All of this is dashboard configuration. None of it has a public endpoint, which means none of it can be scripted from your deploy โ go through it once, deliberately, before you take real money.
Webhook URL. Absolute HTTPS, no redirect, and it must return 2xx within
ten seconds. Set it before your first live invoice โ a payment that completes
while the point has no URL fires nothing, and there is no replay.
allowedDomains. A CORS allow-list on the point, capped at ten entries.
Set it to the origins that actually call Havala from a browser, and no more.
Blockchain settings. Enabled blockchains, enabled currencies and confirmation overrides are one group of merchant-level settings, stored and written together rather than one field at a time. Set the whole group when you change any of it, and offer only the chains you are prepared to reconcile.
Expiry. paymentExpirationMinutes on the merchant settings sets your
default invoice window. Short windows reduce your price-drift exposure and the
number of buyers paying dead addresses; too short and slow chains cannot finish.
Bitcoin needs three confirmations at roughly ten minutes each โ a fifteen-minute
window on Bitcoin is a window that expires mid-payment.
Commission and fee payer. commissionPercentage and feePayerMethod
(CUSTOMER โ the buyer covers it; MERCHANT โ you absorb it) are set by Havala
on the point, not by you, and nothing on the gateway API lets you change them.
Read yours off the merchant point in the dashboard and confirm your own pricing
math matches what is actually configured before you take money against it.
Withdrawal addresses. Settlement leaves through a whitelisted address book,
and entries have a lifecycle: WithdrawalAddressStatus is PENDING, ACTIVE,
REJECTED or LOCKED. Add and get your production addresses approved before
you need to move funds, not on the day you do.
Harden the webhook endpoint
Your webhook handler is a public URL that mutates order state. Treat it as an attack surface.
- Verify every delivery against the raw body with a constant-time compare. An unverified handler will happily fulfil an order for anyone who can guess the shape of your payload.
- Bound the timestamp. The signature does not cover
X-Webhook-Timestamp, so a valid old body stays valid forever. Reject anything outside a few minutes. - Return
2xxfast. Ten seconds is the whole budget, and the running system makes one attempt. Acknowledge, then queue the work. - Deduplicate. The same
payment.completedcan arrive more than once, and your reconciliation job will re-discover payments the webhook already delivered. Key onevent + data.payment.id + data.payment.transaction_hashand enforce it with a unique constraint in the same transaction as the side effect. - Check the amount. A completed payment means confirmations were reached,
not that the buyer sent the right amount. Compare
received_amountagainstamountbefore you ship.
The mechanics of all five are in Webhooks.
Reconciliation is mandatory
Webhook delivery is best effort: one attempt, ten-second timeout, 2xx or
nothing, and a failure is logged and dropped. A rolling restart during a delivery
loses that settlement permanently unless something else finds it.
Run a job that sweeps anything you were expecting and did not hear about, and
re-reads
GET /api/v1/payments/{id}/status:
// Every few minutes. Anything still open past a grace period gets re-read
// from Havala rather than waited on.
for (const row of await db.payments.awaitingSettlement({ olderThanMinutes: 5 })) {
const status = await havala('GET', `/api/v1/payments/${row.paymentId}/status`);
if (status.status === 'COMPLETED') {
// Same path the webhook handler takes, including the dedupe key, so a
// late webhook for this payment is a no-op.
await settle(row, status);
} else if (status.status === 'FAILED' || status.status === 'EXPIRED') {
await close(row, status.status);
}
}This is not a belt-and-braces nicety; without it your ledger drifts from Havala's, quietly, and you find out from a customer.
Rate limits
The service-wide buckets are 10 requests per second, 50 per ten seconds and 1000 per minute. Individual public checkout routes are throttled more tightly on top of that โ the per-route numbers are in The Hosted Checkout Page.
Poll status endpoints on a timer, not in a tight loop, and back off on failure.
The cheap projections
(/api/v1/invoices/{id}/status,
/api/v1/payments/{id}/status)
exist so that polling costs little; the full reads are not for that.
Monitoring
Monitor your own integration rather than probing Havala. Havala's liveness
probes are operator-facing and are not part of the public API, so the signal you
should actually be watching is the one you already generate: the outcome of the
calls you make and the deliveries you receive. Instrument the client function
that signs requests โ status code, error code, and latency, tagged by route โ
and you get an availability picture that reflects your traffic rather than a
green tick that tells you nothing about your own credentials or clocks.
Alert on things that mean money is stuck, not just on 5xx:
- Payments sitting in
PENDINGpastexpiresAtโ buyers seeing an address and not paying it. - Payments in
CONFIRMINGfor far longer than the chain's arithmetic suggests. NO_AVAILABLE_WALLETon checkout โ the pool for a chain is exhausted, and every buyer picking that chain is being turned away.- Webhook handler non-2xx rate. Each one is a settlement you now have to find by reconciliation.
- A rise in
SIGNATURE_EXPIRED, which almost always means clock drift on a host that signs.
Launch checklist
Account
- Merchant is verified, and the users who need the dashboard can log in.
- Dashboard roles are scoped; not everyone is
OWNER.
Credentials
- A dedicated production merchant point exists, separate from staging.
-
apiKeyandapiSecretare in a secret manager and out of the repo. - No secret reaches a browser bundle or a log line.
- Staging credentials were rotated in the dashboard if anyone outside the team ever held them.
- You have a written rotation runbook โ who rotates in the dashboard, and who deploys both halves in the same window.
Point configuration โ all of this is set in the dashboard
-
webhookUrlis HTTPS, non-redirecting, and returns2xxin under ten seconds. - The webhook was verified end to end against the production URL โ a test delivery from the dashboard for reachability, and a real testnet payment for the parsing and verification path.
-
allowedDomainslists only the origins that need it. - Enabled chains and currencies are the ones you can reconcile.
-
paymentExpirationMinutesis long enough for the slowest chain you offer. -
commissionPercentageandfeePayerMethodwere read back and match your pricing. - Withdrawal addresses are added and
ACTIVE.
Integration
- Signing works for both a
GETand aPOSTโ the idempotency line differs. - Idempotency keys are one per logical operation, and retries reuse the key.
- Hosts that sign requests run NTP.
- Webhook signature verification runs against the raw body.
- Handler is idempotent under duplicate delivery, proven by replay.
- Handler compares
received_amountagainstamount. - A reconciliation job runs on a timer and closes the gap.
-
PAYMENT_ALREADY_IN_PROGRESS,INVOICE_EXPIREDandNO_AVAILABLE_WALLETare handled as states with buyer-facing copy, not as unhandled errors.
Operations
- Your own gateway client emits status codes and error
codes as metrics, so an outage or a bad credential is visible without probing Havala. - Alerts exist for stuck payments, webhook failures and
NO_AVAILABLE_WALLET. - Someone owns the manual path for underpayments and overpayments.
- You have run the whole flow on a testnet, including a failure, within the last week.
| Next | What it covers |
|---|---|
| Webhooks | The verification and idempotency details behind the checklist |
| Testing on Testnets | Rehearsing failures before they cost anything |
| API reference | Every endpoint, request and response shape |