Apps and keys

One app, one key, one switch. How authentication works here and why an app is the unit everything 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:

  • one API key, created with the app
  • a mode, test or live, that you change with a button
  • its own webhook URL and signing secret
  • its own ledger and its own balance

One key, and it never changes when you go live

xpay_a3f19c2e_••••••••••••••••••••••••••••••••
│    │        └── secret   32 random characters. Never leaves your server.
│    └─────────── prefix   Public. How we find the record to check against.
└──────────────── product  So a key found in a log is identifiable as ours.

Three parts, and no fourth. There is no mode in it, because the mode is not the key’s: it is the app’s, and you move it in the dashboard.

Yours is on your app’s page in the dashboard, once, when the app is created. The one above is a shape, not a key: the secret half is blanked out because a real one has no business being in a document anybody can read.

So going live is not a deploy. You build the integration with this key, you press Switch to live on the app once your business is verified, and the exact same key starts moving real money. Nothing in your code, your config or your secret manager changes.

What this replaced, and why

There used to be two keys per app, xpay_test_… and xpay_live_…, and one of them always sat in the dashboard labelled “not in use”. The idea was that a key announcing its own mode makes the expensive mistake visible. What it actually produced was a failed deploy whose cause you had to reason about, and a go-live that meant swapping a production secret at the exact moment you could least afford to get it wrong. There is one key now. There is nothing to pick wrong.

We store a hash, not the key

A key is shown once, at creation. After that we hold a SHA-256 of the secret and nothing else. Nobody at Xendly can read your key back to you, including when you ask nicely. Lose it and you rotate it.

Test mode and live mode

TestLive
Payments areReal prompts, real moneyReal prompts, real money
Who you can chargeOnly numbers on your test listAnybody
BooksSeparate ledger and balanceYour real ledger
WithdrawableNoYes, once you are verified
Needs verificationNoYes

Test mode is not a sandbox

A test payment puts a real prompt on a real phone and really takes the money. What makes it test mode is that it can only reach numbers you registered, and that what it collects stays on separate books you cannot withdraw from. Read this before your first one.

Sending it

Either header. Both work, because both are what people actually send:

Authorization: Bearer xpay_a3f19c2e_YOUR_SECRET

X-Api-Key: xpay_a3f19c2e_YOUR_SECRET

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. That is what the payment widget exists for: it hands the browser a session token that is good for one payment of one amount, and never your key.

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: 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 hands them a list of our merchants. It is less helpful to you and much less helpful to them.

A 403 is different and worth reading: the key was fine but the action was not. A live app on an unverified business, or a disabled app.

Housekeeping

  • One app per service, not one per company. Rotating then affects one deployment instead of all of them.
  • Rotating is immediate. The new key exists and the old one stops working in the same instant, so copy it somewhere before you press the button. That is deliberate: the reason to rotate is usually that a key might have leaked, and an overlap where both still work is exactly what you do not want then.
  • Watch last-used. The app’s page shows when its key last authenticated. A key nobody has used in months belongs to an app you can delete.