This document teaches how to integrate Finix.js into a customer website to accept card and bank (ACH) payments via Finix. It covers the Finix APIs involved, the account/application setup required, and the steps to embed and use the library.
Finix.js is a client-side, hosted payment form. It injects an iframe into the merchant's page that collects sensitive PCI/NACHA data (card number, expiration, security code, bank account/routing numbers) and exchanges it directly with the Finix Payments API for a single-use token. The merchant's backend then uses that token to create a reusable Payment Instrument and charge the customer via Transfer or Authorization API calls. Because raw card/bank data never hits the merchant's servers, this flow keeps cards in SAQ-A PCI scope, and is the only supported pattern for Finix web integrations.
Before writing any code, the merchant must have:
https://finix.payments-dashboard.com/).APxxxxxxxxxxxxxxxxxxxxxx). This identifies the platform/application that owns the tokens. It is passed to Finix.PaymentForm(...). Application IDs are environment-scoped — sandbox and production IDs are different.MUxxxxxxxxxxxxxxxxxxxxxx). This represents the entity being paid. Needed server-side when creating Authorizations/Transfers, and client-side if using Finix.Auth for fraud session tracking.| Environment string | Payments API host |
|---|---|
| --- | --- |
prod / production / live | https://finix.live-payments-api.com |
sandbox / sb | https://finix.sandbox-payments-api.com |
Always build/test in sandbox first. Switching to prod is a one-line change (environment string + Application ID).
finix.js and calls Finix.PaymentForm(...), which mounts an iframe into a container .- The buyer enters card or bank data into the iframe's inputs.
- On submit, the iframe POSTs the sensitive data directly to the Finix Payments API at
/applications/:app_id/tokens and receives a single-use token (TKxxxx...). - The token (plus a
fraud_session_id from Finix.Auth and any checkout context) is POSTed to the merchant's backend. - The backend uses server-side API credentials to create a Buyer Identity, exchange the token for a Payment Instrument, and create a Transfer or Authorization (see "Server-Side: Claiming the Token" below).
Finix.js handles steps 1–3. Steps 4–5 are the merchant's responsibility.
4. Embedding Finix.js
4a. Minimal integration
<div id="finix-form"></div>
<script src="https://js.finix.com/v/2/finix.js"></script>
<script>
const form = Finix.PaymentForm(
"finix-form", // container: id string OR an HTMLElement
"sandbox", // environment
"APgPDQrLD52TYvqazjHJJchM", // Application ID
{
onSubmit: (error, response) => {
if (error) {
console.error(error);
return;
}
const token = response.data.id; // "TKxxxx..."
// POST token to your backend to finish the charge.
fetch("/api/charge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...paymentData, token }), // include other checkout data (amount, currency, buyer info, cart id, etc.)
});
},
}
);
</script>
The library attaches window.Finix on load and exposes:
Finix.PaymentForm(element, env, applicationId, options) — renders card + bank form (default is cards only; enable bank via paymentMethods: ['card', 'bank']).Finix.Auth(env, merchantId, callback) — initializes Sift fraud session tracking (see "Fraud Session" below).- Aliases:
Finix.TokenForm, Finix.CardTokenForm, Finix.BankTokenForm all delegate to PaymentForm.
element may be either an element ID (string) or an HTMLElement — the latter is required when mounting inside a Shadow DOM / web component.
4b. The form instance
PaymentForm returns an instance with:
form.submit((error, response) => {...}) — trigger tokenization from a custom button. See "Custom submit button" below.form.theme(themeName) — swap theme at runtime.
4c. Options Reference
Pass as the 4th argument to PaymentForm. All are optional. This section is the longest because options are where most integrations get stuck — read carefully before customizing.
4c.1 Quick index
Option Type Default Purpose --- --- --- --- paymentMethods`('card' \ 'bank')[]` ['card']Which payment method tabs to render. showAddressbooleanfalseShow the billing address section. showLabelsbooleantrueRender field elements above inputs. labelsRecord{}Override default label text per field. showPlaceholdersbooleantrueRender input placeholders. placeholdersRecord{}Override placeholder text per field. hideFieldsstring[][]Hide specific fields (deep dive below). requiredFieldsstring[][]Force normally-optional fields to be required (deep dive below). requireSecurityCodebooleantrueRequire CVV/CVC on cards. confirmAccountNumberbooleantrueRequire bank account number to be typed twice. hideErrorMessagesbooleanfalseSuppress inline error text (fields still validate). errorMessagesRecord{}Custom inline error copy per field. hidePotentialIssueMessagesbooleanfalseHide soft "are you sure?" warnings (e.g. unusual card length). defaultValuesRecord{}Prefill non-sensitive fields (deep dive below). enableDarkModebooleanfalseRespect prefers-color-scheme: dark. themestring'finix'Brand preset (deep dive below). stylesStylesCfg{}Per-state CSS overrides (deep dive below). fontsArray<{fontFamily,url,format}>[]Inject custom @font-face rules (deep dive below). submitLabelstring'Submit'Built-in submit button label. plaidLinkSettings{ displayName, language, countries }— Enable Plaid embedded bank linking (see "Plaid Bank Linking" below). onLoad() => void— Fires when the iframe has mounted and initialized. onUpdate(state, binInfo, hasErrors) => void— Fires on every field change. onSubmit(error, response) => void— Fires after tokenization completes.
4c.2 The complete field name list
Every option that takes a field-keyed record (labels, placeholders, hideFields, requiredFields, errorMessages, defaultValues) uses the same set of canonical field names. Use these exactly — arbitrary keys are silently ignored.
Section Field name Notes --- --- --- Card card_holder_nameCardholder name. Optional by default. Card numberCard number. Always required when card is active. Card expiration_dateMM/YY. Always required when card is active. Card security_codeCVV/CVC. Required unless requireSecurityCode: false or hideFields: ['security_code']. Bank (USA + CAN) account_holder_nameAccount holder name. Required by default when bank is active. Bank (USA + CAN) account_numberAccount number. Required when bank is active. Bank (USA + CAN) account_type`PERSONAL_CHECKING \ PERSONAL_SAVINGS \ BUSINESS_CHECKING \ BUSINESS_SAVINGS`. Required when bank is active. Bank (USA) bank_codeUS ABA routing number (9 digits). Required when country is USA. Bank (CAN) transit_number5-digit transit. Required when country is CAN; replaces bank_code. Bank (CAN) institution_number3-digit institution. Required when country is CAN; replaces bank_code. Address address_line1Street. Address address_line2Apt/suite. Address address_cityCity. Address address_regionState/province. Dropdown is auto-populated for USA/CAN. Address address_countryDropdown; dictates bank field set and postal-code format. Address address_postal_codeZIP (US: 12345 or 12345-6789) / postal code (CAN: A1A 1A1).
4c.3 paymentMethods
paymentMethods: ['card'] // card only (default)
paymentMethods: ['bank'] // ACH/bank only
paymentMethods: ['card', 'bank'] // tabs for both
When both are present, a tab bar appears and the user chooses. The active method determines which required fields are enforced. Bank fields vary by address_country — selecting CAN swaps bank_code for transit_number + institution_number.
4c.4 hideFields (deep dive)
Removes a field from the DOM and from validation. The tokenization payload simply omits it.
Fields that are safe to hide:
card_holder_name — optional for cards.account_holder_name — hideable for bank; if hidden the default-required check is skipped for it.security_code — equivalent to setting requireSecurityCode: false; produces a token without CVV, but many issuers will decline the downstream charge. Only hide if your Finix account/use-case permits card-on-file without CVV.- Any
address_* field — all address fields are optional at the tokenization layer. To hide the entire address section at once, prefer showAddress: false (or simply omit it — false is the default).
Fields you should NOT hide (will break tokenization):
number, expiration_date — required to mint a card token.account_number, account_type, and bank_code (USA) / transit_number+institution_number (CAN) — required to mint a bank token.
Common hide patterns:
// Show address but skip the optional second line
hideFields: ['address_line2'],
// CVV-less card-on-file
hideFields: ['security_code'],
// Drop name fields (name captured elsewhere in your checkout)
hideFields: ['card_holder_name', 'account_holder_name'],
If the user hides a field that was also listed in requiredFields, the hide wins — the field does not render and is not validated.
4c.5 requiredFields (deep dive)
What it does: promotes a normally-optional field to required. It does not add fields that aren't already rendering — you still need showAddress: true (or no matching hideFields entry) for address fields to appear.
Fields that are already required by default (no need to list them):
- Card:
number, expiration_date, security_code (unless requireSecurityCode: false). - Bank (USA):
account_holder_name, account_number, bank_code, account_type. - Bank (CAN):
account_holder_name, account_number, transit_number, institution_number, account_type.
Fields you can add to requiredFields:
card_holder_nameaccount_holder_name (redundant for bank — already required by default)name — legacy alias that maps to card_holder_name or account_holder_name depending on the active payment method. Prefer the explicit names in new code.address_line1, address_line2, address_city, address_region, address_country, address_postal_code
Anything else is silently dropped (only the list above is honored).
Interactions to be aware of:
requireSecurityCode: false + requiredFields: ['security_code'] — the requiredFields entry is not honored for security_code. Use requireSecurityCode as the switch.hideFields + requiredFields for the same field — hide wins.confirmAccountNumber: true (default) implicitly requires account_number_confirmation when bank is active; you don't list it manually.
Typical billing-address collection for AVS/fraud checks:
AVS itself only evaluates address_line1 + address_postal_code, but most merchants collect the full address for records and fraud scoring:
showAddress: true,
requiredFields: [
'address_line1',
'address_city',
'address_region',
'address_postal_code',
// address_country is a dropdown with a default, so it's not strictly needed here
],
For a minimal AVS-only setup:
showAddress: true,
hideFields: ['address_line2', 'address_city', 'address_region', 'address_country'],
requiredFields: ['address_line1', 'address_postal_code'],
4c.6 defaultValues (deep dive)
Prefills non-sensitive fields so the buyer doesn't retype data your checkout already has.
Allowed fields:
card_holder_name, account_holder_nameaddress_line1, address_line2, address_city, address_region, address_country, address_postal_code
Never prefill (the iframe ignores these even if passed):
number, expiration_date, security_code — would break PCI scope.account_number, bank_code, transit_number, institution_number — would break NACHA scope.
Format requirements:
address_country → ISO-3 alpha codes ('USA', 'CAN', 'GBR', …). The dropdown's list comes from countryNamesOptions and is ACH-restricted to USA/CAN when bank is the active method.address_region → two-letter state/province abbreviation for US ('CA', 'NY') and CAN ('ON', 'BC'). Other countries: free text.address_postal_code → already-formatted string ('94105', '94105-1234', 'M5V 3L9'). The formatter will re-normalize input, but passing the canonical form avoids confusing the buyer.
Example:
defaultValues: {
card_holder_name: 'Ada Lovelace',
address_line1: '50 Beale St',
address_line2: 'Suite 600',
address_city: 'San Francisco',
address_region: 'CA',
address_country: 'USA',
address_postal_code: '94105',
},
4c.7 theme vs styles
theme is a preset that changes the whole form's look (colors, borders, button). styles is a targeted override on top of whichever theme is active. Use theme for brand-alignment, then nudge with styles.
Built-in themes: finix (default), amethyst, sapphire, topaz, ruby, emerald, midnight, elevated. Switch at runtime via form.theme('ruby').
4c.8 styles (deep dive)
styles is an object with two top-level branches — default (always applied) and dark (applied only when enableDarkMode is on and the user/browser is in dark mode; merged over default).
Under each branch, each target element exposes a set of states. States are also applied additively in this order: default → success (if valid) → error (if invalid & errors shown) → focused (if focused).
Full shape:
styles: {
default: {
form: {
default: { /* container around entire form */ },
},
section: {
default: { /* each section wrapper (card/bank/address) */ },
},
sectionHeader: {
default: { /* section heading text */ },
focused: { /* when a field inside is focused */ },
},
input: {
default: { /* all inputs */ },
success: { /* input with valid value */ },
error: { /* input with validation error and showErrors=true */ },
focused: { /* input while focused */ },
},
submitButton: {
default: { /* built-in submit button */ },
disabled: { /* when the form has errors */ },
},
},
dark: {
// same tree — these get merged on top of `default` when dark is active
input: { default: { /* ... */ } },
// ...
},
},
Values are camelCased CSS strings (same as React/inline style objects), not CSS declarations. Use string values everywhere, including numeric-looking ones:
input: {
default: {
border: '1px solid #7D90A5',
borderRadius: '12px',
padding: '12px 14px',
fontSize: '16px',
fontFamily: '"Effra", sans-serif',
},
focused: {
border: '1px solid #3B82F6',
boxShadow: '0 0 0 3px rgba(59,130,246,0.2)',
},
error: {
border: '1px solid #DC2626',
color: '#DC2626',
},
success: {
border: '1px solid #16A34A',
},
},
State precedence example. An input that is both focused and in error ends up with the merge default → error → focused. So if both define border, focused wins. To force error styling to trump focus, re-declare border inside the error block of the dark/default tree, or move the style onto outline so they don't collide.
Dark mode notes. dark only applies when you set enableDarkMode: true. Everything inside dark is a patch over default — you don't have to redeclare unchanged rules.
What you can't do. You can't inject arbitrary CSS, pseudo-classes (:hover), or selectors. If you need hover styles or animation, do it by adjusting default/focused or filing a feature request.
4c.9 fonts
Injects @font-face rules into the iframe so the CSS fontFamily values in styles actually resolve. Each font object:
fonts: [
{
fontFamily: 'Effra',
url: 'https://cdn.example.com/fonts/effra.woff2',
format: 'woff2',
},
],
Constraints enforced by the iframe:
url must be served over https:// — anything else is rejected with a console warning.format must be one of woff, woff2, truetype, opentype, embedded-opentype, svg.fontFamily is sanitized (strips anything that isn't word/space/comma/hyphen) to prevent CSS injection.
After the font is loaded, reference it in styles via the sanitized family name: fontFamily: '"Effra", sans-serif'.
4c.10 Error messages
Two independent switches:
hideErrorMessages: true — hides inline error text. Fields still validate and the form still blocks submission; you just don't show the red copy.errorMessages: { field: 'custom string' } — per-field override. Still only shown when hideErrorMessages is false. The keys match the canonical field names listed in "The complete field name list" above.
hidePotentialIssueMessages: true suppresses the soft warnings like "This Visa number is valid, but Visa typically issues 13 or 16 digit cards…" without affecting hard validation.
4c.11 Callbacks
onLoad: () => { /* iframe mounted & initialized */ },
onUpdate: (state, binInfo, hasErrors) => {
// state — { [fieldName]: { value, errors, isDirty, isFocused, ... } }
// binInfo — { cardBrand, bin } once enough digits are entered
// hasErrors — true while any required/invalid field blocks submission
// Typical use: enable/disable your custom submit button.
},
onSubmit: (error, response) => { /* see "The onSubmit Response" below for shape */ },
Callbacks live on the parent and are invoked in response to iframe messages — they are stripped from the options object before it crosses the postMessage boundary.
4d. Custom submit button
Omit the built-in button by providing your own and calling form.submit:
<div id="finix-form"></div>
<button id="pay-btn" disabled>Pay</button>
<script>
const form = Finix.PaymentForm("finix-form", "sandbox", "APxxx...", {
onUpdate: (_state, _bin, hasErrors) => {
document.getElementById("pay-btn").disabled = hasErrors;
},
});
document.getElementById("pay-btn").addEventListener("click", () => {
form.submit((error, response) => {
if (error) return handleError(error);
sendTokenToBackend(response.data.id);
});
});
</script>
Note: do not wrap #finix-form in a element whose submit you also handle — the library manages its own submission via postMessage to the iframe.
5. The onSubmit Response
onSubmit: (error, response) => {
if (error) {
// error.data?.message — human-readable
// error.data?._embedded?.errors — per-field validation errors from Finix
return;
}
const tokenData = response.data;
// tokenData.id → "TKxxxxxxxxxxxxxxxxxxxxxx" (single-use, 30 days)
// tokenData.fingerprint → stable hash of the payment method
// tokenData.instrument → "PAYMENT_CARD" | "BANK_ACCOUNT"
// tokenData.expires_at → ISO timestamp
const token = tokenData.id; // what you forward to your backend
};
Tokens are single-use and expire in ~30 days. The merchant's server should exchange the token for a persistent Payment Instrument immediately after receiving it (see "Server-Side: Claiming the Token" below).
6. Plaid Bank Linking (optional)
Instead of (or in addition to) manual routing + account entry, merchants can enable Plaid Link for ACH. Add plaidLinkSettings to options and make sure paymentMethods includes 'bank':
Finix.PaymentForm("finix-form", "sandbox", "APxxx...", {
paymentMethods: ['card', 'bank'],
plaidLinkSettings: {
displayName: 'Acme Corp',
language: 'en',
countries: ['USA', 'CAN'],
},
onSubmit: (err, res) => { /* ... */ },
});
When the user successfully links an account via Plaid, the library posts a third_party_token payload instead of raw bank fields. The returned token is used exactly like a normal token on the server side.
7. Fraud Session (Finix.Auth)
For card and bank payments Finix recommends initializing a Sift fraud session tied to the Merchant ID. This produces a sessionKey (fraud_session_id) that must be forwarded to the backend and attached to the Authorization/Transfer request.
const auth = Finix.Auth("sandbox", "MUxxxxxxxxxxxxxxxxxxxxxx", (sessionKey) => {
// Stash it; attach to the payload when creating an Authorization/Transfer server-side.
window.__finixFraudSessionId = sessionKey;
});
Important: Finix.Auth takes the Merchant ID, not the Application ID. The supported environment strings are the same ones in the prerequisites table above (sandbox/sb and prod/production/live).
8. Server-Side: Claiming the Token and Charging the Customer
Everything above this section happens in the browser. Once the merchant's backend receives the token (TKxxxx...), it has 30 days to claim it. The backend does this in three steps:
- Create (or reuse) a Buyer Identity →
IDxxxx... - Create a Payment Instrument from the token + Identity →
PIxxxx... (this claims the token) - Create a Transfer (a sale/debit) — or an Authorization if you want to capture later — using the Payment Instrument as the
source and the Merchant ID as merchant.
Transfers are the most common path (one-step sale). Authorizations follow the same pattern but require a separate capture call afterwards.
All requests use HTTP Basic Auth with a server-side API key pair and must include the Finix-Version header.
8a. (Optional) Create a Buyer Identity
Skip this step if you already have an Identity for this buyer — Identities are reusable and can be updated later via PUT.
You can create a fully-populated Identity, or an empty one with just identity_roles and type — both are valid. Populating it up front avoids asking the buyer for the same info later.
curl -i -X POST \
-u USfdccsr1Z5iVbXDyYt7hjZZ:313636f3-fac2-45a7-bff7-a334b93e7bda \
https://finix.sandbox-payments-api.com/identities \
-H 'Content-Type: application/json' \
-H 'Finix-Version: 2022-02-01' \
-d '{
"entity": {
"phone": "7145677613",
"first_name": "John",
"last_name": "Smith",
"email": "finix_example@finix.com",
"personal_address": {
"line1": "741 Douglass St",
"line2": "Apartment 7",
"city": "San Mateo",
"region": "CA",
"postal_code": "94114",
"country": "USA"
}
},
"identity_roles": ["BUYER"],
"tags": { "key": "value" },
"type": "PERSONAL"
}'
Response contains "id": "IDxxxx..." — keep it for step 8b.
8b. Create a Payment Instrument (claims the token)
This is the step that claims the token. The token becomes a persistent, reusable Payment Instrument tied to the buyer's Identity. Once claimed, the original TK... is consumed; future charges use the returned PI....
curl -i -X POST \
-u USfdccsr1Z5iVbXDyYt7hjZZ:313636f3-fac2-45a7-bff7-a334b93e7bda \
https://finix.sandbox-payments-api.com/payment_instruments \
-H 'Content-Type: application/json' \
-H 'Finix-Version: 2022-02-01' \
-d '{
"token": "TKiMxe323RE5Dq3wLVtG8kSW",
"type": "TOKEN",
"identity": "IDjvxGeXBLKH1V9YnWm1CS4n"
}'
Example response (card):
{
"id": "PImmCg3Po7oNi7jaZcXhfkEu",
"application": "APgPDQrLD52TYvqazjHJJchM",
"identity": "IDgWxBhfGYLLdkhxx2ddYf9K",
"instrument_type": "PAYMENT_CARD",
"brand": "MASTERCARD",
"card_type": "DEBIT",
"last_four": "8210",
"expiration_month": 12,
"expiration_year": 2029,
"fingerprint": "FPRiCenDk2SoRng7WjQTr7RJY",
"enabled": true,
"currency": "USD",
"address": { "line1": "900 Metro Center Blv", "city": "San Francisco", "region": "CA", "postal_code": "94404", "country": "USA" }
}
The id (PImmCg3Po7oNi7jaZcXhfkEu) is the source you'll use in step 8c.
8c. Create a Transfer (most common: one-step sale)
This actually moves the money. amount is in the currency's minor units (cents for USD).
curl -i -X POST \
-u USfdccsr1Z5iVbXDyYt7hjZZ:313636f3-fac2-45a7-bff7-a334b93e7bda \
https://finix.sandbox-payments-api.com/transfers \
-H 'Content-Type: application/json' \
-H 'Finix-Version: 2022-02-01' \
-d '{
"amount": 662154,
"currency": "USD",
"merchant": "MUmfEGv5bMpSJ9k5TFRUjkmm",
"source": "PI6iQcTtJNCS8GZAVKYi5Ueb",
"tags": { "test": "Sale" }
}'
Fields:
merchant — the Merchant ID being paid (the same MU... used in Finix.Auth).source — the Payment Instrument ID from step 8b.amount — integer in minor units. 662154 = $6,621.54.fraud_session_id — (recommended) the sessionKey returned by Finix.Auth in the browser (see "Fraud Session" above).tags — arbitrary metadata for your own reporting.
8d. Authorizations (when you need to capture later)
Identical structure to Transfers, but posted to /authorizations. They place a hold on funds without capturing. Call POST /authorizations/:id/capture (or void) to finish. Use this for workflows like "authorize at checkout, capture on ship." For most use cases — e-commerce sales, one-click pay — a Transfer is what you want.
8e. What lives where
Concern Location Credential --- --- --- Collect card/bank Browser (iframe) Application ID (public) Tokenize (TK...) Browser → Finix Application ID Create Identity, Payment Instrument, Transfer/Authorization Server only API username + password Webhooks (dispute, settlement, etc.) Server Verify signature
Never put the API username/password into finix.js or any browser-delivered bundle. The US...:... credentials shown above are public Finix sandbox test keys — safe to copy for local experiments, but replace with your own for live traffic.
9. Integration Checklist
- [ ] Sandbox Application ID and Merchant ID obtained from Finix Dashboard.
- [ ]
finix.js script tag added; container present in the DOM before the script runs.- [ ]
PaymentForm called with correct environment + Application ID. - [ ]
onSubmit (or custom-button form.submit) wired to a backend endpoint. - [ ] Backend exchanges token → Payment Instrument → Authorization/Transfer using server-side API credentials.
- [ ]
Finix.Auth initialized with the Merchant ID; sessionKey forwarded to backend and included as fraud_session_id. - [ ] Tested in sandbox with Finix test cards (e.g.
4111 1111 1111 1111) and test bank routing numbers before swapping to live environment + live Application ID. - [ ] Merchant page served over HTTPS in production (Finix enforces this for the iframe).
10. References
- Finix.js tokenization guide: https://docs.finix.com/guides/online-payments/payment-tokenization/tokenization-forms
- Create an Identity (Buyer): https://docs.finix.com/api/identities/createidentity
- Create a Payment Instrument (claims the token): https://docs.finix.com/api/payment-instruments/createpaymentinstrument
- Create a Transfer (one-step sale): https://docs.finix.com/api/transfers/createtransfer
- Create an Authorization (auth + later capture): https://docs.finix.com/api/authorizations/createauthorization
共 1 个版本