Credentials
Canopy issues two kinds of key: a secret key for your server and a publishable key for the browser. Nothing in your types stops you mixing them up, so this page shows where each one goes.
The quickstart's get-a-secret-key step links here. This page covers each credential in full.
The two credentials
Never put it in a browser, a client bundle, a log line, or a URL. If it ends up in any of those places, treat it as leaked.
- Secret key (
cnpy_sk_live_...): use it on your server only. Send it as anAuthorization: Bearertoken on every/api/v1/*request. It is the key that creates and lists intents. - Publishable key (
cnpy_pk_live_...): safe to use in a browser. It is meant to be visible in your page's source. Pass it as themerchantoption to the checkout SDK'smount(). It cannot authenticate a REST API call. The API returnsunauthorizedif you try. The loader also will not mount unlessmerchantstarts with this exact prefix.
A new secret key has two scopes:
payment_intents:create. Every documented/api/v1route requires it.customer_deposits:manage. No documented/api/v1route uses it, but Canopy's payout-route endpoints do. A key with this scope can read your payout route, request changes to it, and mint a payout nonce.
A key alone still cannot change where your money lands. That needs a signature from the payout wallet itself. If you do not need the second scope, issue your key with payment_intents:create only.
There is no third credential to issue. To mount checkout, pass the SDK the intentId that your POST /api/v1/intents call returned.
Canopy looks up every other checkout detail from its own records, so you have nothing else to fetch or forward. The id itself is sensitive, though. See the intent id below.
The intent id
An intent id gives access to one order. Anyone who has it can open that checkout and read the order's status and deposit history. It is not a harmless reference number.
/customer-pay takes no Authorization header and no cookie. Having the id is what lets the payer in. A hosted /pay/{id} link works the same way. This is on purpose. The payer is your user, not a Canopy account holder, and they should not have to sign up to pay.
Anyone with an intent id can:
- open the checkout for that one intent;
- read that intent's status and deposit history, including the amounts and chains it was funded from;
- pay it.
They cannot:
- redirect the funds. Canopy reads the destination from its own records for the intent, never from the request;
- change the platform fee or the treasury. Both come from on-chain route storage and are never arguments to a settlement;
- reach any other intent, or any other customer of yours. A checkout session covers only that intent's own deposit customer;
- authenticate an
/api/v1/*call. That still needs your secret key.
Your publishable key limits where an intent id works. It does not make the id safe to share. /customer-pay returns an error unless the merchant key on the URL belongs to the account that owns the intent. So another merchant cannot mount your checkout on their own registered embed origin.
Your publishable key is public on purpose, because it sits in your page's source. It ties an intent id to your account. It does not keep the id secret.
Handle an intent id the way you would a password reset link:
- do not send it to third-party logging, analytics or error-reporting tools;
- do not put it in a URL you share, bookmark, or paste into a support ticket;
- do not use it as the readable order reference in tools other people can see. Use your own order id for that, and keep the mapping on your server.
If an intent id leaks, archive the intent and create a fresh one with POST /api/v1/intents. An archived intent returns the same error as an id that never existed.
Which one goes where
| Credential | Lives in | Never appears in |
|---|---|---|
| Secret key | Your server's environment/secrets store | A browser, a client bundle, a log line, a URL |
| Publishable key | Your page's source, the mount() call | An Authorization header |
| Intent id | Your mount() call, as intentId | An Authorization header, a URL you share, a third-party log |
Spotting a swapped key
Both keys are plain strings. TypeScript will not stop you passing a secret key as merchant, or a publishable key as a bearer token. A prefix check catches both at runtime. The resulting error can look like an unrelated bug, so it helps to know what each mistake produces.
- A secret key passed as
merchanttomount(): the loader stops with aninvalid_merchant_keyerror before using it. This is also the worst way to leak a secret key, because anything passed tomount()is visible in your page's source. - A publishable key passed as the bearer token on a REST call: you get the unauthorized error. The API accepts only a secret key.
If either error appears with no obvious cause, first check which key the failing call used.
See also
- Quickstart: the full walkthrough this guide belongs to.
- API reference: every operation, with the scope it requires.