# Change Fluveo's payment form > One file for one task, the look and the behaviour of Fluveo's payment form beyond a first checkout. It holds the parts of the fluveo.js reference that shape the form, which are its calls, its events and its options, and the errors those parts throw. The payment form works with a `clientSecret` and no other option. This file is for what comes after a first checkout: another theme, another layout, a hidden field, a handler for one of the form's events, a form mounted by amount. The checkout itself is in https://docs.fluveo.com/llms/custom-checkout.txt, with the server route, the page, the webhook and the list that verifies them. Build that first. The form's own page, Payment Element, is in this file whole, after the parts of the reference: what the form shows, then its layouts, its look and its options, each of the three with an example. Each theme's photograph and every variable's default are on https://docs.fluveo.com/payments/fluveo-js-appearance.md. > **For AI agents:** > > Follow this file over anything you remember from other payments libraries. The options have names you know; what each one takes is Fluveo's own. > > - The form's look is set only in code, with the `appearance` option. The form is a frame of Fluveo's, so the CSS of your page does not reach it. > - Use only the names this file lists: each call, option, value and event. The form works without any option, so leave out what you do not need. > - A name the script does not have throws at the call. Nothing is ignored in silence. The error's message lists what the call takes: keep only that, and never work around it. > - When `fluveo.js` throws, fix the call as the error's `code` says. Never show a `FluveoIntegrationError`'s message to the buyer. The parts below come from pages of the Fluveo docs, each under its title and its address. Every other call is on the reference's address, and every other error on the errors page's address. Test mode is at https://docs.fluveo.com/payments/fluveo-js.md#test-mode, and `element_not_supported` at https://docs.fluveo.com/payments/fluveo-js-errors.md#element_not_supported. Every page of the docs: [llms.txt](https://docs.fluveo.com/llms.txt). # fluveo.js Source: https://docs.fluveo.com/payments/fluveo-js.node.md Part of the page "fluveo.js"; the address above has the whole page. ## The payment element The payment element is the form: the card number, the expiration date and the security code, the country and, for the US, Puerto Rico, Canada and the UK, the postal code. Each error shows once: a field's under that field and a declined card's under the card number, or, with condensed inputs, all of them under the block of fields. It shows a placeholder of its own size while it loads, takes its container's width and sets its own height. `elements.create("payment")` also takes `layout`, `fields` and `defaultValues`: see [Form options](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options). | Call | What it does | | --- | --- | | `element.mount(target)` | Shows the form inside `target`, a CSS selector or an element of your page. Mounting again in the same container changes nothing. | | `element.unmount()` | Removes the form from the page. The element can be mounted again. | | `element.destroy()` | Removes the form and every handler. The next `elements.create("payment")` makes a new element. | | `element.on(event, handler)` | Calls `handler` on each event below, and returns the element. | | `element.update(options)` | Changes the layout of a form that is already made: see [element.update()](https://docs.fluveo.com/payments/fluveo-js.node.md#element-update). | Every handler gets one object with `elementType: "payment"`. | Event | When | What the object adds | | --- | --- | --- | | `ready` | The form is on the page and can take input. | Nothing. | | `change` | What the buyer typed became complete, incomplete or empty. | `complete` (every field is valid), `empty` (every field is empty) and `value.type`, which is `"card"`. | | `focus`, `blur` | The buyer entered or left the form. | Nothing. | | `loaderror` | The form could not load, gave no answer 20 seconds after `mount()`, or its PaymentIntent takes no card. | `error`, with `type` and `message`. | Your page learns `complete` and `empty` and nothing else of the card: no digit, no brand, no last four. Enable your Pay button on `complete` when you want to. ```javascript const element = elements.create("payment"); element.on("change", (event) => { document.getElementById("submit").disabled = !event.complete; }); element.mount("#payment-element"); ``` ## Form options Every option here is optional: the form works with `clientSecret` alone. A name or a value that an option does not take throws [option_not_supported](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#option_not_supported), and its message lists what the option takes. **Options of fluveo.elements()** - `appearance` (object, optional): The form's look. Default: the `"fluveo"` theme, with spaced inputs. The default of each variable in each theme is on the page [Appearance](https://docs.fluveo.com/payments/fluveo-js-appearance.node.md). - `theme` (string, optional): `"fluveo"`, the default. `"night"`, for a dark page. `"flat"`: filled fields with no border. - `variables` (object, optional): Each variable changes one value of the theme, and one left out keeps the theme's. A value is a text, written as CSS writes it. One that is not of its variable's kind throws [appearance_value_invalid](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#appearance_value_invalid), which says what each kind takes. - `colorPrimary` (string, optional): A color: the border and the ring of the field in focus, and the selected tab. - `colorBackground` (string, optional): A color: the background of the fields and the tabs. - `colorText` (string, optional): A color: the form's text. - `colorTextSecondary` (string, optional): A color: the secondary text, such as a tab that is not selected. - `colorTextPlaceholder` (string, optional): A color: the placeholders. - `colorDanger` (string, optional): A color: an error, and the border of its field. - `fontFamily` (string, optional): A font family: a list of font names the buyer's device has. - `fontSizeBase` (string, optional): A length: the base size of the form's text. - `spacingUnit` (string, optional): A length: the paddings and the gaps follow it. - `borderRadius` (string, optional): A length: the corners of the fields and the tabs. - `labels` (string, optional): `"above"`: over each field. `"floating"`: inside the field until the buyer types. `"auto"`, the default: floating with condensed inputs, above with spaced ones. - `inputs` (string, optional): `"spaced"`, the default: each field is a box of its own. `"condensed"`: the fields are joined in one block. - `disableAnimations` (boolean, optional): `true`: the form does not animate. Default: `false`. - `locale` (string, optional): The language of every text the buyer reads in the form. `"auto"`, the default: Brazilian Portuguese for a browser in Portuguese, English for any other. `"en"`: English. `"pt-BR"`: Brazilian Portuguese. `Fluveo(publishableKey, { locale })` takes the same values, and the one given here wins. - `loader` (string, optional): The placeholder the form shows in your container while it loads. `"auto"`, the default, and `"always"` show it. `"never"` shows none: the form shows as it loads. *appearance* ```javascript const elements = fluveo.elements({ clientSecret, appearance: { theme: "night", labels: "floating", variables: { colorPrimary: "#0f766e", borderRadius: "8px" }, }, }); ``` *locale* ```javascript const elements = fluveo.elements({ clientSecret, locale: "pt-BR" }); ``` *loader* ```javascript const elements = fluveo.elements({ clientSecret, loader: "never" }); ``` **Options of elements.create()** - `layout` (string or object, optional): How the form lists its payment methods: a type alone, or an object with a `type`. Default: `"tabs"`. - `type` (string, required): `"tabs"`: the methods above the fields. `"accordion"`: the methods as rows, and the open row holds the fields. `"auto"`: tabs. - `defaultCollapsed` (boolean, optional): `true`: no method is open until the buyer opens one. Default: `false`. - `radios` (string or boolean, optional): Whether an accordion row has a radio button. `"always"` or `"never"`. `"if_multiple"` and `"auto"`, the default: only when the form has more than one method. `true` is `"auto"`, and `false` is `"never"`. - `spacedAccordionItems` (boolean, optional): `true`: each accordion row is a box of its own. Default: `false`. - `fields` (object, optional): Which address fields the form asks the buyer for. Default: `"auto"` at each level, and the form asks for the country and, where cards have one, the postal code. A field set to `"never"` is neither shown nor sent: pass its value on every confirm, or `confirmPayment()` throws [billing_details_missing](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#billing_details_missing). - `billingDetails` (string or object, optional): `"auto"`, `"never"`, which hides both fields, or an object with `address`. - `address` (string or object, optional): `"auto"`, `"never"`, which hides both fields, or an object with the two fields. - `country` (string, optional): `"auto"` or `"never"`. - `postalCode` (string, optional): `"auto"` or `"never"`. - `defaultValues` (object, optional): The country and the postal code the form opens with. The buyer may change both. Default: none, and the form opens on the country of the buyer's browser language, or on the US when it names none. - `billingDetails` (object, optional) - `address` (object, optional) - `country` (string, optional): The two capital letters (ISO 3166-1) of a country in the form's list, such as `"CA"`. - `postal_code` (string, optional): A text. *layout* ```javascript const element = elements.create("payment", { layout: { type: "accordion", defaultCollapsed: true, spacedAccordionItems: true }, }); ``` *fields* ```javascript const element = elements.create("payment", { fields: { billingDetails: { address: { postalCode: "never" } } }, }); ``` *defaultValues* ```javascript const element = elements.create("payment", { defaultValues: { billingDetails: { address: { country: "US", postal_code: "94107" } } }, }); ``` ### elements.update(options) Changes a group that is already made, before or after its form is mounted. | Option | What an update does | | --- | --- | | `appearance` | Replaces the last appearance whole, at once: a part the new one leaves out goes back to its default. | | `loader` | Holds from the next `mount()`. | | `amount`, `currency` | Only on a group made with `mode`. On a group made with `clientSecret` they throw [option_not_supported](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#option_not_supported): the PaymentIntent holds the amount. | `locale` and `fonts` throw [not_available_yet](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#not_available_yet). Any other name, `clientSecret` and `mode` among them, throws `option_not_supported`. For another language or another PaymentIntent, call `fluveo.elements()` again. ```javascript elements.update({ appearance: { theme: "flat", inputs: "condensed" } }); ``` ### element.update(options) Changes a payment element that is already made. It takes `layout`, as `elements.create()` does: the card fields keep what the buyer typed, and a method the buyer opened stays open. `fields` and `defaultValues` throw [not_available_yet](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#not_available_yet): `elements.create()` reads them once. Any other name throws `option_not_supported`. For other fields, call `element.destroy()` and create the element again. ```javascript element.update({ layout: { type: "accordion", radios: "always" } }); ``` ## Mount the form before its PaymentIntent `fluveo.elements()` takes `{ mode: "payment", amount, currency }` in place of `clientSecret`. The form mounts with your publishable key alone, and your server creates the PaymentIntent when the buyer presses Pay. Use it when the amount can change while the form is on the page; with a settled amount, `clientSecret` is the shorter way. 1. `fluveo.elements({ mode: "payment", amount, currency })` makes the group, with `amount` in the smallest unit of the currency. Mounting creates nothing. A payment your account cannot take comes as the `loaderror` event. 2. [`elements.update({ amount, currency })`](https://docs.fluveo.com/payments/fluveo-js.node.md#elements-update) sets the cart's new amount. 3. At the press of Pay, await `elements.submit()` first. It checks the fields, shows their errors and sends nothing. It resolves with `{}`, or with `{ error }`, a `validation_error`: stop there. Before `mount()` it throws [submit_not_mounted](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#submit_not_mounted). 4. Your server creates the PaymentIntent and gives the page its `client_secret`. 5. `fluveo.confirmPayment({ elements, clientSecret, confirmParams })` confirms it and ends as [confirmPayment()](https://docs.fluveo.com/payments/fluveo-js.node.md#confirm-payment) says. Without `clientSecret` it throws `confirm_client_secret_missing`, and without the `submit()` of its press, `submit_required`. *checkout.js* ```javascript const fluveo = Fluveo("pk_test_..."); const elements = fluveo.elements({ mode: "payment", amount: 2000, currency: "usd" }); elements.create("payment").mount("#payment-element"); document.getElementById("payment-form").addEventListener("submit", async (event) => { event.preventDefault(); const { error: invalid } = await elements.submit(); if (invalid) return; const { clientSecret } = await fetch("/create-payment-intent", { method: "POST" }) .then((r) => r.json()); const { error } = await fluveo.confirmPayment({ elements, clientSecret, confirmParams: { return_url: location.origin + "/complete" }, }); if (error) document.getElementById("error-message").textContent = error.message; }); ``` The buyer pays the PaymentIntent's amount, whatever the group holds. A PaymentIntent in another currency than the group's is not paid: `confirmPayment()` resolves `{ error }` with the code [currency_mismatch](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#currency_mismatch). After a decline the next press runs the whole handler again. Disable Pay while it runs. ## Errors in your code A mistake in the code that calls `fluveo.js` throws at the call, as a `FluveoIntegrationError` with a `code` and a `doc_url`. Nothing is ignored in silence: an option or a call the script does not have throws an error that names it. [fluveo.js errors](https://docs.fluveo.com/payments/fluveo-js-errors.node.md) has the cause and the fix of each code. # fluveo.js errors Source: https://docs.fluveo.com/payments/fluveo-js-errors.node.md Part of the page "fluveo.js errors"; the address above has the whole page. ## option_not_supported A call got an option, an event name or a value that `fluveo.js` does not have. The message names the call as you wrote it and lists what the call takes. Fix: keep only what the message lists. [fluveo.js](https://docs.fluveo.com/payments/fluveo-js.node.md) shows every call with its options, and [Form options](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options) what each value of an option does. | Call | What it takes | | --- | --- | | `Fluveo(publishableKey, options)` | `locale` | | `fluveo.elements(options)` | `clientSecret`, or `mode`, `amount` and `currency`; `appearance`, `fonts`, `locale`, `loader` | | `elements.create("payment", options)` | `layout`, `fields`, `defaultValues` | | `elements.update(options)` | `appearance`, `fonts`, `locale`, `loader`, and on elements made with `mode` also `amount` and `currency` | | `element.update(options)` | `layout`, `fields`, `defaultValues` | | `element.on(event, handler)` | `"ready"`, `"change"`, `"focus"`, `"blur"`, `"loaderror"` | | `fluveo.confirmPayment(options)` | `elements`, `clientSecret` (only with `mode`), `confirmParams`, `redirect` | | `confirmParams` | `return_url`, `payment_method_data` with `billing_details` only | | `redirect` | `"always"` or `"if_required"` | | `appearance` | `theme`, `variables`, `labels`, `inputs`, `disableAnimations` | | `appearance.theme` | `"fluveo"`, `"night"`, `"flat"` | | `appearance.labels` | `"above"`, `"floating"`, `"auto"` | | `appearance.inputs` | `"spaced"`, `"condensed"` | | `appearance.disableAnimations` | `true` or `false` | | `appearance.variables` | `colorPrimary`, `colorBackground`, `colorText`, `colorTextSecondary`, `colorTextPlaceholder`, `colorDanger`, `fontFamily`, `fontSizeBase`, `spacingUnit`, `borderRadius` | | `locale` | `"auto"`, `"en"`, `"pt-BR"` | | `loader` | `"auto"`, `"always"`, `"never"` | | `layout` | `"tabs"`, `"accordion"`, `"auto"`, or an object with `type`, `defaultCollapsed`, `radios`, `spacedAccordionItems` | | `layout.radios` | `"always"`, `"never"`, `"if_multiple"`, `"auto"`, `true` or `false` | | `layout.defaultCollapsed`, `layout.spacedAccordionItems` | `true` or `false` | | `fields` | `billingDetails`, which is `"auto"`, `"never"` or an object with `address`; `address` is `"auto"`, `"never"` or an object with `country` and `postalCode`, each `"auto"` or `"never"` | | `defaultValues` | `billingDetails.address.country`, the two-letter code of a country the form offers, and `billingDetails.address.postal_code`, a text | The message lists every name the call knows. Of those names, `fonts` throws [not_available_yet](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#not_available_yet), and so do `locale` in `elements.update()` and `fields` and `defaultValues` in `element.update()`. ## not_available_yet The call or the option is part of `fluveo.js`, and this build of the script does not act on it. It throws so that nothing is ignored in silence. Today these throw it: the option `fonts` of `fluveo.elements()`, `rules` inside `appearance`, and an option an `update()` cannot change yet, which is `fonts` or `locale` on `elements.update()` and `fields` or `defaultValues` on `element.update()`. Fix: remove the option. The form works without it. Set the language with `fluveo.elements({ locale })`, and `fields` and `defaultValues` with `elements.create("payment")`: an `update()` changes none of the three. [fluveo.js](https://docs.fluveo.com/payments/fluveo-js.node.md#elements-update) lists what each `update()` takes. ## billing_details_missing `fluveo.confirmPayment()` was called for a form that does not ask the buyer for the country or the postal code, and `confirmParams` has no value for it. `elements.create("payment", { fields })` hides a field with `"never"`, and the form sends nothing for a hidden field, so your page has to. The message names the option that hid the field and the value that is missing. Nothing is sent to the API. | `fields` sets to `"never"` | `confirmParams` must hold | | --- | --- | | `fields.billingDetails.address.country` | `payment_method_data.billing_details.address.country` | | `fields.billingDetails.address.postalCode` | `payment_method_data.billing_details.address.postal_code` | `"never"` on `billingDetails` or on `address` hides both. A hidden field needs its value on every confirm, whatever the country, and a value that is empty or only spaces counts as missing. Fix: pass what your page knows of the buyer, such as `confirmParams: { return_url, payment_method_data: { billing_details: { address: { country: "US", postal_code: "94107" } } } }`, or set the field to `"auto"` so the form asks for it. ## appearance_value_invalid `fluveo.elements({ appearance })` or `elements.update({ appearance })` got a variable whose value is not of its kind. The message names the variable, the value it got, the kind it takes and an example. Each value is a text, written as CSS writes it: a number such as `4` is refused, and `"4px"` is taken. | Variable of `appearance.variables` | Kind | What it takes | | --- | --- | --- | | `colorPrimary`, `colorBackground`, `colorText`, `colorTextSecondary`, `colorTextPlaceholder`, `colorDanger` | color | A hex color, a color name, or `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `lab()`, `lch()`, `oklab()` or `oklch()` with no other function inside, such as `"#2961b2"`. Never `var()`, `url()`, `inherit`, `initial`, `unset`, `revert` or `currentColor`. | | `fontFamily` | font family | A list of font names, up to 200 characters, such as `"Inter, sans-serif"`. | | `fontSizeBase`, `spacingUnit`, `borderRadius` | length | `"0"`, or a number of up to three digits and two decimals in `px`, `rem` or `em`, such as `"4px"`. | A name that `appearance` or `appearance.variables` does not have throws [option_not_supported](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#option_not_supported), and `rules` throws [not_available_yet](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#not_available_yet). Fix: pass a value of the variable's kind, as the example in the message shows. Write a color of your stylesheet out in full: the form is a frame, and it cannot read a variable of your page. ## confirm_client_secret_missing `fluveo.confirmPayment()` got no `clientSecret`, or one that is not a PaymentIntent's client secret, for elements made with `{ mode, amount, currency }`. Fix: at the press of Pay, call `elements.submit()`, create the PaymentIntent on your server and pass its `client_secret` as `confirmPayment({ elements, clientSecret, confirmParams })`. ## submit_not_mounted `elements.submit()` ran while the payment element was not mounted in the page: before `mount()`, or after `unmount()` or `destroy()`. Fix: call it after `elements.create("payment").mount("#payment-element")`, when the buyer presses Pay. ## submit_required `fluveo.confirmPayment()` of elements made with `{ mode, amount, currency }` ran with no `elements.submit()` called before it. One call serves until a confirm ends in anything but a field's `validation_error`, so call it at each press of Pay. Fix: in the Pay handler, run `const { error } = await elements.submit()` before any other async work, and stop on `error`. Then create the PaymentIntent and confirm. Let one press end before the next starts. # Payment Element Source: https://docs.fluveo.com/payments/payment-element.node.md The payment form: its methods, layouts, look and options. The Payment Element is Fluveo's payment form. `fluveo.js` puts it on your own page, in a frame of Fluveo's, so the card number never reaches your page or your server. The form shows the payment methods of one PaymentIntent, checks what the buyer types and shows each error where it belongs. Load it with a script tag, or with the [two npm packages](https://docs.fluveo.com/payments/fluveo-js.node.md#packages) when you use a bundler or React. [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.node.md) builds a whole checkout around it. ![The payment form with a test card typed in: the Card tab, then the card number, the expiration date, the security code, the country and the ZIP code.](https://docs.fluveo.com/payments/img/payment-element/form.png "The form, with a test card typed in") > **For AI agents:** > > - The form's look is set only in code, with the `appearance` option. No Dashboard setting changes it, and the CSS of your page does not reach it. > - Use only the option names and values that the fluveo.js reference lists under Form options. > - A name the script does not take throws an error at the call, and its message says what to use. Fix the call as it says; never work around it. ## Compatible APIs The form works with the [PaymentIntents API](https://docs.fluveo.com/api/payment_intents.node.md). Your server creates one PaymentIntent per order with its secret key and gives the page only the `client_secret`. [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.node.md) explains the PaymentIntent and its states, and [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.node.md#build) has the server route to copy. | Your code | The form | | --- | --- | | Creates the PaymentIntent on your server and passes its `client_secret` to `fluveo.elements()`. | Reads the PaymentIntent and shows its payment methods. | | Mounts the form in a container of your page. | Draws its fields in a frame, checks each one and says what to fix. | | Calls `fluveo.confirmPayment()` when the buyer presses your Pay button. | Sends the card to Fluveo and confirms the PaymentIntent, through 3D Secure when the card asks for it. | | Marks the order paid on the `payment_intent.succeeded` webhook. | Nothing: the page's result is only for the buyer. | | Or mounts the form from an amount and a currency, and creates the PaymentIntent when the buyer presses Pay. | Shows before a PaymentIntent exists, and confirms the one your code passes then. | The last row is the second way to use the form: see [Mount the form before its PaymentIntent](https://docs.fluveo.com/payments/fluveo-js.node.md#mode). ## Payment methods The form shows one payment method, the card. It asks for the card number, the expiration date, the security code, the country and, where cards have one, the postal code. A PaymentIntent's `payment_method_types` says which methods it accepts. The form reads that list when it mounts and, with a test key, prints it to the browser console, as [Test mode](https://docs.fluveo.com/payments/fluveo-js.node.md#test-mode) describes. Every other method is taken outside the form: | Method | Where it is taken | | --- | --- | | [Card](https://docs.fluveo.com/payments/cards.node.md) | In this form, and on the [hosted checkout](https://docs.fluveo.com/payments/checkout.node.md) page. | | [Pix](https://docs.fluveo.com/payments/pix.node.md) | Your server creates it and your page shows its code. | | [Boleto](https://docs.fluveo.com/payments/boleto.node.md) | Your server issues it and your page shows its line and its PDF. | | [Apple Pay](https://docs.fluveo.com/payments/apple-pay.node.md) | Your page sends the buyer's approval to Fluveo as a card token. | ## Layout The form lists its payment methods as tabs above the fields, or as an accordion whose open row holds the fields. Choose with the `layout` option of `elements.create("payment")`. ![The same payment form three times, side by side: with tabs, as an accordion with a radio button, and as an accordion without one.](https://docs.fluveo.com/payments/img/payment-element/layouts.png "The three layouts") ### Tabs Each payment method is a box above the fields, and the open one is outlined. Tabs are the default, so [layout](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-layout) can be left out. *checkout.js* ```javascript const fluveo = Fluveo("pk_test_..."); const elements = fluveo.elements({ clientSecret }); const paymentElement = elements.create("payment", { layout: { type: "tabs" }, }); paymentElement.mount("#payment-element"); ``` That is the whole script, once the page has its `clientSecret`. [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.node.md#build) has the page around it. ![The payment form in the tabs layout, empty, as the buyer first sees it on a narrow page.](https://docs.fluveo.com/payments/img/payment-element/layout-tabs.png "Tabs") ### Accordion with radio buttons The methods are rows, and the open row holds the fields. `radios: "always"` draws a radio button in each row, also when the form has one method. [layout](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-layout) has four parts: `type`, `defaultCollapsed`, `radios` and `spacedAccordionItems`. *checkout.js* ```javascript const fluveo = Fluveo("pk_test_..."); const elements = fluveo.elements({ clientSecret }); const paymentElement = elements.create("payment", { layout: { type: "accordion", radios: "always" }, }); paymentElement.mount("#payment-element"); ``` ![The payment form as an accordion: one row with a radio button and the word Card, open, with the card fields inside it.](https://docs.fluveo.com/payments/img/payment-element/layout-accordion-radios.png "Accordion with radio buttons") ### Accordion without radio buttons `radios: "never"` leaves the radio button out of every row. An accordion with one method draws none either when [layout](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-layout) has no `radios`. *checkout.js* ```javascript const fluveo = Fluveo("pk_test_..."); const elements = fluveo.elements({ clientSecret }); const paymentElement = elements.create("payment", { layout: { type: "accordion", radios: "never" }, }); paymentElement.mount("#payment-element"); ``` ![The payment form as an accordion with no radio button: the row Card, open, with the card fields inside it.](https://docs.fluveo.com/payments/img/payment-element/layout-accordion.png "Accordion without radio buttons") ## Appearance The form is a frame, so the CSS of your page does not reach it: its look is set in code, with the `appearance` option of `fluveo.elements()`. A theme is a whole look with a name. A variable changes one value of the theme, such as the main colour, the font or the corner radius, and every part of the form that uses that value follows it. ![The payment form in its three themes, side by side: fluveo is light, night is dark, and flat has filled fields with no border.](https://docs.fluveo.com/payments/img/fluveo-js-appearance/themes.png "The three themes") *checkout.js* ```javascript const fluveo = Fluveo("pk_test_..."); const elements = fluveo.elements({ clientSecret, appearance: { theme: "flat", variables: { colorPrimary: "#0f766e" }, }, }); const paymentElement = elements.create("payment"); paymentElement.mount("#payment-element"); ``` ![The payment form in another theme, with a store's own main colour on the Card tab.](https://docs.fluveo.com/payments/img/payment-element/appearance.png "A theme and one variable") [Appearance](https://docs.fluveo.com/payments/fluveo-js-appearance.node.md) shows each theme and gives every variable's default in each theme. ## Options Every option is optional: the form works with `clientSecret` alone. `appearance`, `locale` and `loader` go to `fluveo.elements()`. `layout`, `fields` and `defaultValues` go to `elements.create("payment")`. *checkout.js* ```javascript const fluveo = Fluveo("pk_test_..."); const elements = fluveo.elements({ clientSecret, locale: "en", loader: "always", }); const paymentElement = elements.create("payment", { layout: "accordion", defaultValues: { billingDetails: { address: { country: "CA" } } }, }); paymentElement.mount("#payment-element"); ``` | Option | What it does | | --- | --- | | [appearance](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-appearance) | Sets the theme and the variables of the form's look. | | [locale](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-locale) | Sets the language of the form. Left out, it follows the buyer's browser. | | [loader](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-loader) | Shows or hides the placeholder while the form loads. | | [layout](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-layout) | Lists the payment methods as tabs or as an accordion. | | [fields](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-fields) | Hides the country or the postal code when your page already asked for it. | | [defaultValues](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-defaultValues) | Sets the country and the postal code the form opens with. | ## Errors The form shows the buyer's errors by itself, each one once: - a field that is incomplete or not valid, or that the API refused, under that field; - a declined card, right under the card number; - a form that could not load, in place of the fields. With `inputs: "condensed"` in [appearance](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-appearance), the fields are joined in one block, and the errors of the fields and of the card show under it: the fields' in their order, then the card's. Its messages are in English and in Brazilian Portuguese, as `locale` picks. Where the form has no sentence of its own, it shows the API's message. Your code reads the rest: `confirmPayment()` resolves with [the error object](https://docs.fluveo.com/payments/fluveo-js.node.md#error-object), and [Errors](https://docs.fluveo.com/errors.node.md) lists the API's codes. The form shows no message when the answer to a payment did not come back, so show that error's `message` on your page. A mistake in your code throws at the call; one the form learns only at the press, such as [currency_mismatch](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#currency_mismatch), resolves as `{ error }` of the type `invalid_request_error`. The options throw [option_not_supported](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#option_not_supported), [appearance_value_invalid](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#appearance_value_invalid) and [billing_details_missing](https://docs.fluveo.com/payments/fluveo-js-errors.node.md#billing_details_missing). Type a [test card](https://docs.fluveo.com/start/testing.node.md#typed-cards) into the form to see a decline or 3D Secure. ## With other elements The Payment Element is the one element of `fluveo.js`: `elements.create()` takes `"payment"` and no other name. The rest of the checkout is your own page: the e-mail field, the address, the cart and the Pay button. Pass what your page already knows about the buyer: | Your page has | Pass it as | | --- | --- | | The buyer's name, e-mail or phone | [confirmParams](https://docs.fluveo.com/payments/fluveo-js.node.md#confirm-payment), in the `billing_details` of `payment_method_data`. | | The country or the postal code, asked in your own address fields | The `address` of that `billing_details`, with that field set to `"never"` in [fields](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-fields) so the form does not ask again. | | A country or a postal code to start from | [defaultValues](https://docs.fluveo.com/payments/fluveo-js.node.md#form-options-defaultValues). The buyer may still change it. | ## Move from the hosted checkout [Hosted checkout](https://docs.fluveo.com/payments/checkout.node.md) sends the buyer to a page of Fluveo's. Move when the payment belongs inside your own page: your layout around the form, your cart beside it, and a declined card that leaves the buyer where they are. The hosted page also shows Pix and Boleto, so keep it for an order paid that way. Your server keeps its secret key, its `Idempotency-Key` and its webhook endpoint. It creates a PaymentIntent in place of a Checkout Session, gives the page the `client_secret` and marks the order paid on `payment_intent.succeeded`, which a session's payment already sends. Your page gains the form and the calls that mount it and confirm the payment: [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.node.md) has every file.