Docs
GuidesGoing Live

Going Live

Intermediate15 min

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:

  • apiSecret is 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: MerchantUserRole is OWNER, MANAGER, ACCOUNTANT or VIEWER, and most people who need the dashboard do not need OWNER.
  • 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:

RotationChangesAlso breaks
Regenerating the merchant point's credentialsapiKey and apiSecretWebhook verification, and anything that addresses the point by apiKey
Rotating the webhook signing secretapiSecret onlyGateway 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 2xx fast. Ten seconds is the whole budget, and the running system makes one attempt. Acknowledge, then queue the work.
  • Deduplicate. The same payment.completed can arrive more than once, and your reconciliation job will re-discover payments the webhook already delivered. Key on event + data.payment.id + data.payment.transaction_hash and 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_amount against amount before 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:

TypeScript
// 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 PENDING past expiresAt โ€” buyers seeing an address and not paying it.
  • Payments in CONFIRMING for far longer than the chain's arithmetic suggests.
  • NO_AVAILABLE_WALLET on 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.
  • apiKey and apiSecret are 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

  • webhookUrl is HTTPS, non-redirecting, and returns 2xx in 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.
  • allowedDomains lists only the origins that need it.
  • Enabled chains and currencies are the ones you can reconcile.
  • paymentExpirationMinutes is long enough for the slowest chain you offer.
  • commissionPercentage and feePayerMethod were read back and match your pricing.
  • Withdrawal addresses are added and ACTIVE.

Integration

  • Signing works for both a GET and a POST โ€” 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_amount against amount.
  • A reconciliation job runs on a timer and closes the gap.
  • PAYMENT_ALREADY_IN_PROGRESS, INVOICE_EXPIRED and NO_AVAILABLE_WALLET are 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.
NextWhat it covers
WebhooksThe verification and idempotency details behind the checklist
Testing on TestnetsRehearsing failures before they cost anything
API referenceEvery endpoint, request and response shape