Migrate to Transfers V2

Move one checkout when you are ready. A page that still calls frame.confirmCardPayment with a charge-intent secret (ci_…) keeps charging. The page you move uses a checkout session secret (chk_sess_…) with createTransfer and confirmTransfer. Both confirms can run in the same app.

If you already use Frame.js

Keep the script tag (https://js.framepayments.com/v1/frame.js) and Frame.init with your publishable key. createTransfer and confirmTransfer are methods on that same frame instance. A checkout you have not moved still calls confirmCardPayment.

This is the page you have today. The card mounts, your server returns a charge intent, and the browser confirms it:

const frame = await Frame.init('pk_sandbox_your_publishable_key');
const card = await frame.createElement('card', {
  accountId,
  billing: true,
});
card.mount('#card-details');

// ci_… from POST /v1/charge_intents
const result = await frame.confirmCardPayment(chargeIntentClientSecret);

This is the same page after you port it. Frame.init and card.mount stay. The card takes the checkout session secret. Confirm is two calls, and the secret is chk_sess_…:

const frame = await Frame.init('pk_sandbox_your_publishable_key');
const card = await frame.createElement('card', {
  accountId,
  clientSecret, // chk_sess_… from POST /v1/checkout_sessions
  clientSecretExpiresAt: expiresAt,
  billing: true,
});
card.mount('#card-details');

const transfer = await frame.createTransfer(
  { source: { payment_method_id: paymentMethodId } },
  clientSecret,
);
const result = await frame.confirmTransfer(transfer.id, clientSecret);

confirmCardPayment accepts a ci_… secret. Pass chk_sess_… to createTransfer and confirmTransfer.

The card's complete handler still posts the encrypted payload to your server. Your server still creates the account and the payment method with the secret key. The JSON you return to the page changes: send client_secret (chk_sess_…) and the payment method id, then run the two calls above.

A wallet button still creates the payment method with the publishable key. In the paymentmethod handler, call event.complete('success'), then createTransfer and confirmTransfer with the session your server just minted. Subscription pages, including Apple Pay recurring, keep confirmCardPayment. An onboarding page that already calls onboarding.open({ clientSecret }) with onb_sess_… stays as it is.

What moves, and what stays

TodayAfter you port
POST /v1/charge_intents, then confirmCardPayment(ci_…)POST /v1/checkout_sessions, then createTransfer and confirmTransfer
Card element with accountId onlySame element, plus clientSecret from the checkout session
Wallet button creates a payment method with the publishable keyUnchanged. Your server attaches it, mints the checkout session, and the page confirms the transfer
POST /v1/subscriptions and Apple Pay recurringStay on confirmCardPayment
Hosted or embedded onboardingPOST /v1/onboarding_sessions. The browser opens onb_sess_…

Capture, void, refund, update, and list stay on the secret key. The browser never receives that key.

1. Mint a checkout session

Your server creates the account and the payment method, then mints the session with the locked amount. The amount is in minor units. A session without an amount cannot create a transfer.

curl --request POST \
  --url https://api.framepayments.com/v1/checkout_sessions \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "account_id": "<account_id>",
    "amount": { "value": 2000, "currency": "usd" },
    "authorization_mode": "automatic"
  }'

Return client_secret (chk_sess_…) and expires_at to the browser. The token lasts 30 minutes and can read only this account. frame-js cannot mint a replacement, so pass refreshClientSecret and have your server mint again when the token expires.

Mount the card element with the account and the session secret. clientSecret is what lists saved cards and prefills the name and email. A card element that only has accountId still lists saved cards with the publishable key. That list goes away when the publishable-key read is removed. Name and email already require the checkout session.

const card = await frame.createElement('card', {
  accountId,
  clientSecret,
  clientSecretExpiresAt: expiresAt,
  refreshClientSecret: async () => {
    const session = await fetch('/api/checkout-session', { method: 'POST' }).then((r) => r.json());
    return { clientSecret: session.client_secret, expiresAt: session.expires_at };
  },
  billing: true,
});
card.mount('#card-details');

2. Create and confirm the transfer in the browser

createTransfer sends confirm: false and an Idempotency-Key. The amount, when you send one, has to match the amount locked on the session. confirmTransfer sends no body.

const transfer = await frame.createTransfer(
  { source: { payment_method_id: paymentMethodId } },
  clientSecret,
);
const result = await frame.confirmTransfer(transfer.id, clientSecret);

3D Secure runs inside confirmTransfer. When the issuer challenges the card, the transfer comes back requires_3d_secure with next_action.use_frame_sdk.source. The SDK opens the challenge, then polls GET /v2/transfers/:id for the final status. Do not call confirm again after the challenge.

Check result.hasError and result.transfer.payment.status. succeeded means the charge cleared. A decline arrives as an error on the result.

3. Wallets

The payment request button still creates the payment method with the publishable key. It does not charge the card. Your server attaches that payment method to the account, mints the checkout session from step 1, and returns client_secret. The page then calls createTransfer and confirmTransfer.

Call event.complete('success') before confirmTransfer. The wallet sheet has to close before a 3DS challenge can use the page. The full handler is in Use the payment request button.

4. Onboarding

Onboarding already uses its own session. Your server calls POST/v1/onboarding_sessions with the secret key, the account id, and a return_url. The response client_secret is onb_sess_….

await onboarding.open({ clientSecret });

Bootstrap, the account read and update, phone verification, and payment-method create inside that flow send onb_sess_…. Read the account profile with that session. The publishable key does not return the profile. The hosted redirect is the same session: send the account holder to the response url. See Run onboarding.

Subscriptions

Subscription charges, including Apple Pay recurring, stay on confirmCardPayment with a ci_… secret. There is no Transfers V2 confirm for that flow yet.

Frame Assistant

Ask anything about Frame's APIs and products