Accept a payment with frameOS

Build a payment integration on frameOS 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 frameOS.

Prerequisites

Before creating a transfer the following must be in place. See Transfers for the full list.

RequirementDetails
Active billing agreementThe merchant must have an active billing agreement for the charge category.
Processing fee planA transfer fee plan configured with a billable_event_type that matches the transfer type — transfer.charge.card for card charges, transfer.charge.ach for ACH.
Account capabilityThe account must have an active card_send (cards) or bank_account_send (ACH) capability.

Sandbox setup

Run these two requests once per merchant + environment to make charge transfers possible. Skip this section if your merchant already has a fee plan and billing agreement configured.

Create a transfer fee plan scoped to card charges:

CREATE FEE PLAN
curl --request POST \
  --url https://api.framepayments.com/v1/transfer_fee_plans \
  --header 'Authorization: Bearer API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Card charge fees",
  "fee_application_mode": "deduct",
  "items": [
    {
      "fee_type": "flat_plus_percentage",
      "amount_fixed_cents": 30,
      "amount_percentage": 2.9,
      "billable_event_type": "transfer.charge.card"
    }
  ]
}'

The billable_event_type must match the transfer being billed: transfer.charge.card for card charges, transfer.charge.ach for ACH. The default transfer.created will not match a card charge and the transfer will fail.

Create a transfer billing agreement that activates the fee plan for the charge category:

CREATE BILLING AGREEMENT
curl --request POST \
  --url https://api.framepayments.com/v1/transfer_billing_agreements \
  --header 'Authorization: Bearer API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "transfer_fee_plan_id": "d4e5f6a7-b8c9-0123-def4-567890abcdef",
  "category": "charge",
  "effective_date": "2026-01-01"
}'

Omit account_id to apply the agreement platform-wide, or provide one to scope it to a single account.

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 frameOS 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, frameOS 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 Payment Method bound to the account. Pass the encrypted card fields through unchanged and include account so the resulting payment method is attached to the frameOS account you created in step 2.

Parameters
typestring

Use card for card payments or ach for bank charges.

accountstring

The frameOS account ID to attach the payment method to. Required for the frameOS 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 frameOS 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",
  "charge_intent": "5b8a1c0e-2f3a-4d8c-b1ab-9c7d5e2a6f01",
  "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 frameOS transfers. When you create a transfer, frameOS:

  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

frameOS 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.completedThe transfer reaches a terminal success state
transfer.failedThe transfer fails. The failure_reason field explains why

See the Webhooks documentation for setup details.