Accept a payment

Build a payment integration by charging a payment method against an Account using the transfers API. This guide covers both client-side and server-side code to create a checkout that accepts card payments through Frame.

Prerequisites

Before creating a transfer the following must be in place. See transfers for the full conceptual model.

RequirementDetails
Active billing agreementThe merchant must have an active billing agreement for the charge category. New merchants get one provisioned by default at signup.
Processing fee planA transfer fee plan tied to that billing agreement. New merchants get a "Standard Platform Fee" plan provisioned by default (no platform fee items configured — charges are billed at $0 platform fee until you customize).
Account capabilityThe account must have an active card_send (cards) or bank_account_send (ACH) capability.

The fee plan and billing agreement are provisioned automatically when your merchant is created; typically nothing to set up. To customize fees or scope agreements per-account, see the dashboard's Transfer billing agreements view.

1. Set up Frame

In the Frame Dashboard, create a publishable key and a secret API key. Use the secret key on your server to authenticate API requests, and use the publishable key with Frame.js on the client to initialize your payment form.

2. Create an account

Every Frame payment is anchored to an Account. The account represents the end user (or merchant) you're transacting with — it's where Frame stores identity information, payment methods, verification state, and the Sonar signals collected by Frame.js.

Create an individual account with the card_send capability so it can be charged for a card payment. For ACH, request bank_account_send instead.

Parameters
typestring

Use individual for end users and sole proprietors, or business for companies.

capabilitiesarray

Include card_send to accept card payments from the account holder, or bank_account_send to accept ACH payments. The _send suffix refers to the account holder sending payment instructions; the funds flow inbound to the merchant.

profile.individualobject

Identity information for the account holder. At least one identity object (profile.individual or profile.business, matching type) is required at account creation. Provide whatever you already have; Frame collects the rest during hosted onboarding.

external_idstringoptional

Optional identifier from your own system. Useful for linking Frame accounts to records in your platform.

POST/v1/accounts
curl --request POST \
  --url https://api.framepayments.com/v1/accounts \
  --header 'Authorization: Bearer API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "type": "individual",
  "capabilities": ["card_send"],
  "profile": {
    "individual": {
      "email": "marcia@example.com",
      "name": { "first_name": "Marcia", "last_name": "Longo" }
    }
  },
  "external_id": "user_12345"
}'
RESPONSE
{
  "id": "c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d",
  "object": "account",
  "type": "individual",
  "status": "active",
  "external_id": "user_12345",
  "livemode": false,
  "created": 1773398080
}

3. Collect payment details

Use Frame.js to collect payment details on the client. Frame.js uses an iframe to securely send card data to Frame over HTTPS, and — separately — collects the device and behavioral signals that power Sonar fraud protection.

Set up Frame.js

Include the Frame.js script in the <head> of your checkout page. Always load Frame.js directly from js.framepayments.com to remain PCI compliant.

SET UP FRAME.JS
<head>
  <title>Checkout</title>
  <script src="https://js.framepayments.com/v1/index.js"></script>
</head>

Initialize Frame with your publishable key:

INITIALIZE FRAME
const frame = await Frame.init("<PUBLISHABLE_KEY>");

When Frame.js initializes, a Sonar session is created in the browser and stored in local storage. Once the account holder has interacted with your site, Frame associates these Sonar signals with their account, and the most recent session is attached to every transfer you create — no extra parameters required.

Add the Payment Element to your checkout page

Create a container for the Payment Element and mount it:

PAYMENT FORM MARKUP
<form id="payment-form">
  <div id="payment-card-element">
    <!-- Frame.js will create form elements here -->
  </div>
  <button id="pay" type="submit">Pay now</button>
  <div id="messages"></div>
</form>
MOUNT PAYMENT ELEMENT
const theme = frame.themes("clean");
const card = await frame.createElement("card", { theme: theme });
card.mount("#payment-card-element");

Capture the card payload

The card element emits a change event whenever the customer edits a field. When payload.isComplete is true, the payload's encrypted card fields are ready to send to your server. Hold onto the most recent complete payload and forward it on form submission.

CAPTURE CARD PAYLOAD
let cardPayload = null;
card.on("change", (payload) => {
  cardPayload = payload.isComplete ? payload : null;
});

document.getElementById("payment-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  if (!cardPayload) return;

  await fetch("/api/charge", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ cardPayload, amount: 5000 }),
  });
});

The cardPayload contains encrypted card.number and card.cvc strings, plain card.expiry.month and card.expiry.year, and an optional billingAddress if your element collects one. The card number and CVC never leave Frame.js in plaintext, so it's safe to forward cardPayload to your own server.

4. Create a payment method

On your server, exchange the card payload for a PaymentMethod bound to the account. Pass the encrypted card fields through unchanged and include account so the resulting payment method is attached to the Frame account you created in step 2.

Parameters
typestring

Use card for card payments or ach for bank charges.

accountstring

The Frame account ID to attach the payment method to. Required for the charge flow — without it, the transfer in step 5 fails with Customer or account is required to use existing payment method.

card_numberstring

The encrypted card number from cardPayload.card.number. Forward as-is.

cvcstring

The encrypted CVC from cardPayload.card.cvc. Forward as-is.

exp_monthstring

Two-digit expiration month from cardPayload.card.expiry.month.

exp_yearstring

Two-digit expiration year from cardPayload.card.expiry.year.

billingobjectoptional

Billing address. Map from cardPayload.billingAddress if present.

POST/v1/payment_methods
curl --request POST \
  --url https://api.framepayments.com/v1/payment_methods \
  --header 'Authorization: Bearer API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "type": "card",
  "account": "c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d",
  "card_number": "<cardPayload.card.number>",
  "cvc": "<cardPayload.card.cvc>",
  "exp_month": "<cardPayload.card.expiry.month>",
  "exp_year": "<cardPayload.card.expiry.year>"
}'
RESPONSE
{
  "id": "d0a736d3-0dba-4eda-99f6-dfe27849f282",
  "object": "payment_method",
  "type": "card",
  "status": "active",
  "account_id": "c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d",
  "card": {
    "brand": "visa",
    "exp_month": "12",
    "exp_year": "29",
    "last_four": "4242"
  },
  "livemode": false,
  "created": 1773410000
}

5. Create a transfer

The Transfer object represents the movement of funds. In the charge flow you provide a source_payment_method_id (the card or bank account being charged) and the account_id the transfer is associated with.

destination_payment_method_idFunds destination
OmittedFunds go to the merchant (platform)
ProvidedFunds are routed to the account that owns that payment method
Parameters
amountinteger

Amount in the smallest currency unit (e.g. 5000 for $50.00).

account_idstring

The Frame account associated with the transfer.

source_payment_method_idstring

The payment method to charge. Required for the charge flow.

currencystringoptional

ISO currency code. Defaults to USD.

destination_payment_method_idstringoptional

Optional. When provided, funds are routed to the account that owns the payment method.

descriptionstringoptional

Arbitrary description for your records.

POST/v1/transfers
curl --request POST \
  --url https://api.framepayments.com/v1/transfers \
  --header 'Authorization: Bearer API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "amount": 5000,
  "account_id": "c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d",
  "source_payment_method_id": "d0a736d3-0dba-4eda-99f6-dfe27849f282",
  "currency": "USD",
  "description": "Order #12345"
}'
RESPONSE
{
  "id": "f47a2dd3-e0b1-55ec-b633-23c1bbc101ec",
  "object": "transfer",
  "status": "pending",
  "amount": 5000,
  "currency": "USD",
  "net_amount": 4789,
  "frame_fee": 211,
  "description": "Order #12345",
  "source_payment_method": {
    "id": "d0a736d3-0dba-4eda-99f6-dfe27849f282",
    "object": "payment_method",
    "type": "card",
    "status": "active"
  },
  "destination_payment_method": null,
  "billing_agreement": "b37da62b-c233-4358-967a-7910656fb94f",
  "livemode": false,
  "created": 1773415510
}

6. Fraud protection

Sonar evaluation is automatic for transfers. When you create a transfer, Frame:

  1. Looks up the most recent Sonar session for that account (collected by Frame.js on the client) and attaches it to the intent.
  2. Runs fraud and geo-compliance checks before the charge is authorized.

7. Handle post-transfer events

Frame emits webhooks as the transfer progresses. Subscribe to these events to update your order state, notify the customer, or trigger downstream processes.

EventTriggered when
transfer.createdThe transfer is successfully created
transfer.succeededThe transfer reaches a terminal success state
transfer.failedThe transfer fails. The failure_reason field explains why

See the webhooks integration page for setup details.

Reference

For the full API surface used in this guide, see POST/v1/transfers, POST/v1/payment_methods, and POST/v1/accounts.