Architecture
Every Frame integration involves three actors. Understanding what each one is allowed to do — and which credential it holds — is the load-bearing model for everything else in this SDK.
The three actors
Your frontend runs Frame.js in the browser. It holds your publishable key (pk_*) — safe to expose — and, for any session-scoped flow, a short-lived client_secret minted by your backend. It never sees your secret key. The frontend's job is to render UI: card input, wallet buttons, onboarding modal, Terms of Service. It collects sensitive values, encrypts them in the browser, and hands them to the Frame API directly so they never transit your server.
Your backend holds your secret key (sk_*) and owns everything that requires it. Creating accounts. Provisioning capabilities. Minting onboarding sessions and checkout sessions. Confirming charge intents. Receiving webhooks. It can use a Frame server SDK or call the Frame API directly. It hands only the client_secret to your frontend — nothing more.
The Frame API is the source of truth. Your backend authenticates with the secret key. Your frontend authenticates with the publishable key plus a client_secret. The two authentication paths are intentionally separate so the secret key has no route to the browser.
How the credentials split
| Credential | Lives in | Used for |
|---|---|---|
Secret key sk_* | Backend only — environment variable | Account creation, session minting, confirming intents, webhook signature verification |
Publishable key pk_* | Frontend, embedded in the page | Identifies your application to Frame; safe to expose |
client_secret | Frontend, passed in per session | One operation. The prefix says which. |
The prefix is the whole scope:
onb_sess_opens one onboarding flow.ci_confirms one charge intent.chk_sess_reads one account's name, email, and saved payment methods for the card element.
A publishable key does not load that account or those payment methods. An onboarding session does not either. Mint chk_sess_ with POST /v1/checkout_sessions.
The client_secret is the seam between the two halves. Your backend mints it after authorizing the operation; your frontend uses it to perform exactly that operation, nothing more. If a client_secret leaks, the blast radius is one session.
A worked example: embedded onboarding
- Frontend asks your backend to start an onboarding flow.
- Backend authenticates with
sk_*, callsPOST /v1/accountsandPOST /v1/onboarding_sessions, and returns the session'sclient_secretto the frontend. - Frontend calls
onboarding.open({ clientSecret }). The Frame.js component uses theclient_secretto authenticate directly with the Frame API and runs the modal flow end-to-end. - Backend receives webhooks as the user completes each step. Webhooks are the authoritative signal — frontend callbacks are advisory.
The same shape repeats for card charges (mint a charge intent server-side, confirm it from the browser with client_secret) and for Terms of Service capture (the signed token from the browser gets verified server-side).
A worked example: checkout identity
The card element can show the account's name and email and its saved payment methods. Those reads use a checkout client secret, not the publishable key and not the onboarding session.
- Frontend asks your backend for a checkout token for an account id.
- Backend authenticates with
sk_*, callsPOST /v1/checkout_sessionswith{ "account_id": "<ACCOUNT_ID>" }, and returnsclient_secret(chk_sess_…) andexpires_at. The token lasts 30 minutes. - Frontend creates the card element with
accountIdandclientSecret. The element loads the redacted account and the redacted payment-method list. - When
expires_athas passed, or a read returns 401, the element callsrefreshClientSecret. That function asks your backend to mint again. Frame.js has no secret key and cannot mint the token itself.
The account payload is the id, first name, last name, and email. Each payment method is the id, brand, last four, and expiry, or the ACH account type and last four. See Card configurations.
Element lifecycle
Frame elements are mounted asynchronously. They construct, fetch any needed config, and emit a ready event when they're available for interaction. A few rules apply across every element:
Register event listeners before calling mount(). The ready event fires once and won't replay — listeners attached after mount can miss it.
- Construct, then mount.
Frame.init()or a lightweight factory returns the element;element.mount(selector)attaches it to the DOM. - Listen before mount. Always wire
on("ready"),on("change"),on("error")before themount()call so the first event lands in your handler. - Trust webhooks for state changes. Browser events tell you what the user did in the moment; webhooks confirm what Frame accepted. Reconcile state from the webhook, not the event.