Apps and keys
How authentication works here, and why an app is the unit everything else hangs off.
An app is one integration
Not one company, not one product line. One integration. Your storefront checkout is an app. The nightly job that pays your drivers is an app. Each one owns:
- a mode,
testorlive, fixed at creation and never changeable - its own API keys
- its own webhook URL and signing secret
- its own ledger and its own balance
Why mode is a property of the app, not the key
Two kinds of key
| App key | Account key | |
|---|---|---|
| Scope | One app | Every app in your company |
| Can move money | Yes | No, not a franc |
| Can administer apps | No | Yes: list and create |
| Endpoints | /v1/payments, /v1/payouts, /v1/balance | /v1/apps |
| Where it lives | The service that takes payments | Your provisioning script or internal tooling |
The split exists because breadth and power should not travel together. The key that spans your whole company cannot spend anything; the key that can spend is confined to one app in one mode. A leak of either is bad. A leak of one key that was both would be a different order of bad.
What a key looks like
xpay_live_a3f19c2e_7Kq2mVx9RtLp4WzB8sNyHdF6jC1gQaEu
xpay_test_9c2e41b7_Qm8xTv3NpZs6YdK1wLaB5rHgJc0FuEXoThe only part worth reading is the second word. A key that says live moves real money; a key that says test cannot. The rest is a public prefix we look the record up by and a random secret.
The mode is in the string on purpose. A key pasted into a chat, a log line or a config file announces which world it belongs to without anybody having to look it up. We check that the mode in the string matches the mode on the record too, so a hand-edited key is treated as a forgery rather than a typo.
We store a hash, not the key
Sending it
Either header. Both work, because both are what people actually send:
Authorization: Bearer xpay_live_a3f19c2e_7Kq2mVx9…
X-Api-Key: xpay_live_a3f19c2e_7Kq2mVx9…Over HTTPS, from your server. Never from a browser, a mobile app, or anything else a user can open the developer tools on. A key that reaches a client device is a key that has been published.
Every rejection looks the same
401 Unauthorized
{
"status": "error",
"message": "Invalid API key",
"timestamp": "2026-09-12T09:14:02.000Z"
}Malformed, unknown, revoked, wrong secret, mode mismatch: one message for all of them. Distinguishing them would turn the endpoint into an oracle: “invalid” versus “revoked” tells somebody guessing prefixes that they found a real one, which is a free enumeration of our merchants. It is less helpful to you and much less helpful to them.
A 403 is different and is worth reading: it means the key was fine but the action was not. An account key on a money endpoint, a live key on an unverified business, or a disabled app.
Housekeeping
- One key per service, not one per company. Rotating then affects one deployment instead of all of them.
- Rotate by overlap. Mint the new key, deploy it, confirm traffic, then revoke the old one. Both work at once, so there is no window where nothing does.
- Watch last-used. The dashboard shows when each key last authenticated. A key nobody has used in months is a key to revoke.