Chromia Pay

Going live

The five checks that separate a working sandbox from a store that can take real money.

A verified payout wallet on live, on every rail you accept

Live and sandbox wallets are separate rows. Proving one does not prove the other, and a rail without a proven address is simply not offered at checkout.

A live API key, server-side only

If a key has ever been in a browser bundle, a CI log, a screenshot or a chat message, revoke it. A replacement key is one click; a leaked live key is somebody else's payments API.

A live webhook endpoint over https

Subscribed to at least payment.confirmed and payment.overpaid — both mean paid.

Signature verification and idempotency, both tested

Not "implemented" — tested. Send a delivery with a mangled signature and confirm you refuse it. Send a valid one twice and confirm you fulfil once. See test mode.

One real payment, for a real small amount, on each rail

Test mode cannot tell you that your live wallet address is the one you think it is. Nothing else can either.

Three settings worth deciding before the first real payment

All three are in the dashboard under Settings → Store preferences. None is required, and the defaults are the safe ones — but each has a first payment where you would rather have chosen already.

Underpayment tolerance. An exchange withdrawal usually arrives a little short, because the exchange takes its fee out of the amount sent rather than on top. At the default of 0 those payments end as underpaid and wait for a person. Set a tolerance in basis points — 50 is 0.5%, enough for a typical exchange fee — and a shortfall inside it confirms, firing payment.confirmed as normal. The maximum is 500. It is a property of the store, so it applies to live and to every sandbox alike, and it changes what your webhook receives: with a tolerance set, amount_paid on a confirmed payment can be slightly below amount. Fulfil on the status, not on your own comparison of the two figures.

Branding. A logo URL (https, hosted by you) and a hex accent colour. The colour fills the pay button; the ink on it is chosen for contrast, so a pale brand does not produce unreadable white text. Chromia Pay's own line stays above your mark — a payer needs to recognise the store and see who is carrying the money.

Notifications. Email to whoever asks for it, for the two things a webhook cannot resolve on its own: CHR that could not be matched to an order, and an order that expired holding a part payment. Each person turns it on for themselves. Nothing is sent for payments that simply worked, and nothing is sent for simulate_payment — an integrator driving the underpaid path in a loop is testing a handler, not asking anyone to look at a payment.

Environment variables

.env
CPAY_SECRET_KEY=cpay_sk_live_...      # server-side only
CPAY_WEBHOOK_SECRET=whsec_...         # per endpoint, shown once at creation
CPAY_API_URL=https://pay-api.chromia.com

Check that your .gitignore actually covers the env file before writing real values into it. A tracked .env is the most common way a live key leaves a building.

When a payment does not confirm

Work down this list; it is ordered by how often each one is the answer.

  1. No verified payout wallet on that rail. Checkout could not offer it.
  2. The endpoint is not subscribed to that event. payment.overpaid missing is the classic.
  3. Your consumer returned non-2xx, so the delivery is being retried and the endpoint is heading for disabled. Developers → Deliveries has every attempt, with its status code and duration — three 502s is a deploy, three ten-second timeouts is a handler doing work before it answers.
  4. Signature verification is failing because the body was parsed before it was verified. Symptom: every delivery rejected, and the secret is definitely correct.
  5. Wrong environment. A live payment against a sandbox endpoint, or the reverse. Check livemode on the event.
  6. The customer underpaid. Status underpaid, money in your wallet, payer identified. Reconcile page.
  7. The link expired before they finished. Create a new payment; a checkout URL is not reusable.

On this page