Safe to embed in client apps. Used by the SDKs to fetch offerings and start
purchases. Cannot read customer data or perform privileged operations.
One per app, with no test/live split. Ship the same value in every build,
TestFlight and Play internal testing included. See Test and live
keys.
Secret keycsk_sk_live_… / csk_sk_test_…
Full server-side access. Used by your backend and the CLI.
Never ship a secret key in a client app.
Restricted keycsk_rk_live_… / csk_rk_test_…
A secret key narrowed to the resources you pick: transactions:read,
customers:read, and so on. Anything outside the list returns 403 insufficient_scope. Use one for a support tool, a dashboard widget or a
vendor integration that has no business reading everything.
web_sessions:write (Create customer web sessions in the dashboard) lets
your backend issue and revoke web customer sessions and open
the billing portal for a customer. A web, Chrome extension or direct Mac backend
needs it together with customers:read.
User-token secretcsk_uts_…
Not a key, and it never goes in an Authorization header. It is the HS256
secret your backend signs X-CashSDK-User-Token with, so CashSDK can tell
which of your users a device request is for. Reveal it under App settings →
API keys. One per app, no mode: your backend signs a token for a user, and
it has no way to know whether that user is on a TestFlight build or the App
Store release.
MCP tokencsk_mcp_…
The durable credential for the MCP server, and what you
paste into Claude Code, Cursor or Codex. Workspace-scoped (or narrowed to one
app), does not expire, and revocable from Settings → MCP. It can never do
more than the member who created it, and billing, membership, SSO, store
credentials and workspace deletion are refused outright.
Setup tokencsk_st_…
A scoped, short-lived token for onboarding and provisioning flows, handed out
in the dashboard's integration prompt. One app, setup routes only, 24-hour
expiry, and minting a new one revokes the last, so use an MCP token for
anything that lives in a config file.
Treat secret keys like passwords. If one leaks, roll it from Settings → API
keys, with cashsdk, or with the rotate_api_key MCP tool. Rolling issues
the replacement immediately and lets you choose when the old value stops
working (now, 1 hour, 24 hours, or 7 days), so a rotation does not have to be
an outage.
csk_sk_test_… reaches Sandbox data and csk_sk_live_… reaches Production data.
Both are created with your app and sit side by side under the test/live tabs in
App settings → API keys.
A server key cannot reach the other environment. If a caller asks for it (via
?environment= or X-CashSDK-Environment) it gets a 400 environment_mismatch
rather than a quiet switch, because silently returning an empty Sandbox result to
something that asked for Production looks exactly like your customers vanishing.
bash
# a test key asking for production data
curl https://api.cashsdk.com/v1/transactions?environment=Production \
-H "Authorization: Bearer csk_sk_test_..."
{ "error": { "code": "environment_mismatch",
"message": "this is a test key, which can only read Sandbox data..." } }
Pointing a staging server at real customers then takes deploying a visibly
different key, which shows up in a config diff, instead of a flag nobody re-reads.
There is one csk_pk_… per app and it works in every environment. Put it in
every build configuration you have.
That is not a shortcut, it is the only correct answer. TestFlight distributes the
exact binary you submit to the App Store, and purchases made through TestFlight
run in Apple's sandbox while App Store purchases run in production. Google Play
internal testing works the same way. One key is compiled in for both, so a key
pinned to one environment would lock out every user of the other, and there is no
build setting that avoids it.
Sandbox and production data stay separate anyway, by something stronger than a
key prefix:
On write, CashSDK reads the environment out of the store's own signed
payload. A client cannot spoof it, and it is the same field whether the
purchase came from your simulator, a TestFlight tester or a paying customer.
On read, the SDK asks for the environment the purchase resolved into. iOS
takes it from Transaction.environment; Android takes it from the verify
response, because Play Billing does not tell the client. Both send it as
X-CashSDK-Environment.
Entitlements and consumable balances are resolved per environment, so a
Sandbox purchase never grants Production access, and sandbox coins are never
spendable in the App Store build, for the same user.
Verification itself is deliberately never refused on environment grounds: a real
purchase submitted by a mis-built app is still a purchase your customer paid for,
so it is recorded under the environment the store attests.
Holding a csk_pk_test_… or csk_pk_live_…? Publishable keys were briefly
split by mode. Those keys still work and are still pinned to their own
environment, so nothing breaks while you are shipping one. Roll it under App
settings → API keys to get a key that serves both, and ship that in your next
release. Same for a csk_uts_test_… / csk_uts_live_… pair.
Keys issued before modes existed at all have no mode segment
(csk_sk_3DXP…). They keep working exactly as before, resolving the
environment from your app's default setting.