---
title: "fluveo.js"
description: "Every call, option, event and result."
url: https://docs.fluveo.com/payments/fluveo-js/
---

> Index of every page: https://docs.fluveo.com/llms.txt. Integrating Fluveo? Read https://docs.fluveo.com/agents/integrate.md first.

> Too long? This page with code in one language only: https://docs.fluveo.com/payments/fluveo-js.curl.md (cURL), https://docs.fluveo.com/payments/fluveo-js.node.md (Node.js), https://docs.fluveo.com/payments/fluveo-js.python.md (Python), https://docs.fluveo.com/payments/fluveo-js.php.md (PHP).

# fluveo.js

Every call, option, event and result.

`fluveo.js` is the one script Fluveo gives the browser. It puts the payment form on your own page, confirms the payment and reads how it ended. The form is a frame of Fluveo's, so your page never holds a card number. [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.md) builds a whole checkout with it, step by step. [Payment Element](https://docs.fluveo.com/payments/payment-element.md) explains the form itself: its payment methods, its layouts, its look and its options.

> **For AI agents:**
>
> - Use only what this page lists. Anything the script lacks throws an error that names what to use: see https://docs.fluveo.com/payments/fluveo-js-errors.md.
> - Load the script from https://elements.fluveo.com/v1/ only. A page with no build step uses a script tag. With a bundler or React, install @fluveo/fluveo-js and @fluveo/react-fluveo-js, which only load that same script and hold no card code. Never install another payments package, and a copy of the script in a bundle throws script_not_from_fluveo.
> - One payment element per elements() group, and one PaymentIntent per group made with a client secret: make a new client secret on the server for a new order.
> - The server side stays plain HTTP on /v1 with the secret key. fluveo.js never takes a secret key.

## Load the script

*In your page*

```html
<script src="https://elements.fluveo.com/v1/"></script>
```

The script defines the global `Fluveo`. Load it from this address on every page that uses it, never from a copy: it makes no network call of its own, sets no cookie, and holds no card code. A test key works on any page, `http://localhost` included. A live key needs an `https` page.

A page that sends a Content-Security-Policy needs two entries for the form, and nothing else:

*Content-Security-Policy*

```text
Content-Security-Policy: script-src https://elements.fluveo.com; frame-src https://elements.fluveo.com
```

## npm packages

A page with no build step needs only the script tag of [Load the script](https://docs.fluveo.com/payments/fluveo-js.md#load). With a bundler or React, install two packages. They only load that same script, so they hold no card code, and a checkout needs no other payments package.

*Install*

```bash
npm install @fluveo/fluveo-js @fluveo/react-fluveo-js
```

`@fluveo/fluveo-js` has `loadFluveo()` and the TypeScript types of the script. `@fluveo/react-fluveo-js` has the components `Elements` and `PaymentElement` and the hooks `useFluveo()` and `useElements()`, for React 16.8 or newer.

`loadFluveo(publishableKey)` adds the script tag to the page once and resolves with the object that `Fluveo(publishableKey)` returns. It uses the script the page already loaded with a script tag, when there is one. On a server, where there is no `window`, it resolves with `null`. It rejects when the script cannot load, and with the error of a key that `Fluveo()` refuses.

```javascript
import { loadFluveo } from "@fluveo/fluveo-js";

const fluveo = await loadFluveo("pk_test_...");
const elements = fluveo.elements({ clientSecret });
```

In React, call `loadFluveo()` once per page and give what it returns, the promise itself, to `<Elements>`.

| Part | What it does |
| --- | --- |
| `<Elements fluveo={...} options={{ clientSecret }}>` | Makes the elements group for the components inside it. While `clientSecret` is empty the form waits. |
| `<PaymentElement />` | Mounts the payment element. It takes `options`, `id`, `className`, `onReady`, `onChange`, `onFocus`, `onBlur` and `onLoadError`. |
| `useFluveo()`, `useElements()` | Return the `Fluveo` object and the elements group that `confirmPayment()` needs. Each is `null` while the script loads, and the group also while `clientSecret` is empty. |

A prop that a component does not take throws [wrong_prop](https://docs.fluveo.com/payments/fluveo-js-errors.md#wrong_prop), and a part used outside `<Elements>` throws [elements_provider_missing](https://docs.fluveo.com/payments/fluveo-js-errors.md#elements_provider_missing). The React tab of [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.md#build) is a whole checkout written with both packages.

## Fluveo(publishableKey)

Returns the object the other calls hang on. `publishableKey` is your `pk_test_...` or `pk_live_...` key. It throws on a secret key, a restricted key or a webhook secret, on anything that is not a publishable key, and when the script was not loaded from its own address.

```javascript
const fluveo = Fluveo("pk_test_...");
```

## fluveo.elements(options)

Starts the form for one PaymentIntent and returns an elements group.

**Options**

- `clientSecret` (string, required): The `client_secret` of a PaymentIntent your server created with the secret key of the same account and mode.
- `mode` (string, optional): `"payment"`, its only value: the form mounts before a PaymentIntent exists. It takes the place of `clientSecret` and needs `amount` and `currency`.
- `amount` (integer, optional): With `mode`: what the buyer will pay, in the smallest unit of the currency, 1 or more.
- `currency` (string, optional): With `mode`: the three-letter currency code.

It also takes `appearance`, `locale` and `loader`: see [Form options](https://docs.fluveo.com/payments/fluveo-js.md#form-options).

```javascript
const elements = fluveo.elements({ clientSecret });
```

| Call on the group | What it does |
| --- | --- |
| `elements.create("payment")` | Returns the payment element. A group has one: a second call returns the same element. |
| `elements.getElement("payment")` | Returns the payment element, or `null` before `create()` and after `destroy()`. |

A group made with `mode` has no PaymentIntent: see [Mount the form before its PaymentIntent](https://docs.fluveo.com/payments/fluveo-js.md#mode).

## 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.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.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.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.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.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.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.md#option_not_supported): the PaymentIntent holds the amount. |

`locale` and `fonts` throw [not_available_yet](https://docs.fluveo.com/payments/fluveo-js-errors.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.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" } });
```

## fluveo.confirmPayment(options)

Confirms the PaymentIntent with what the buyer typed. Call it when the buyer presses your Pay button. A second call while one is running returns the first call's promise, so a double press pays once.

**Options**

- `elements` (object, required): The group that `fluveo.elements()` returned, with its payment element mounted.
- `clientSecret` (string, optional): Only with `mode`, which requires it: the `client_secret` of the PaymentIntent to confirm.
- `confirmParams` (object, required)
  - `return_url` (string, required): An absolute address of your site. The buyer lands there when the payment is done, and after 3D Secure.
  - `payment_method_data` (object, optional): Only `billing_details`: `name`, `email`, `phone` and `address` (`line1`, `line2`, `city`, `state`, `postal_code`, `country`), each a string.
- `redirect` (string, optional): `"always"`, the default, or `"if_required"`.

The promise never rejects for a payment that fails. What happens next depends on how the payment ended:

| The payment | With `redirect: "always"` | With `redirect: "if_required"` |
| --- | --- | --- |
| Succeeded, or is processing | The browser goes to `return_url`. | The promise resolves with `{ paymentIntent }` and the page stays. |
| Needs 3D Secure | The browser goes to the authentication page, then to `return_url`. | The same. |
| Failed, such as a declined card or an incomplete field | The promise resolves with `{ error }` and the page stays. | The same. |

After an error the form stays usable: the buyer corrects the card or types another, and the next `confirmPayment()` confirms the same PaymentIntent. The form itself shows the message of a `card_error` or a `validation_error`, so showing `error.message` yourself is your choice.

```javascript
const { error } = await fluveo.confirmPayment({
  elements,
  confirmParams: { return_url: location.origin + "/complete" },
});
if (error) document.getElementById("error-message").textContent = error.message;
```

### The error object

| `error.type` | What happened | `error.code` |
| --- | --- | --- |
| `validation_error` | A field is incomplete or not valid. Nothing was sent. | `incomplete_number`, `incomplete_expiry`, `incomplete_cvc`, `incomplete_zip` or `incomplete_card` |
| `card_error` | The card was refused. | The API's code, such as `card_declined`, with `decline_code` |
| `invalid_request_error` | A mistake in your code that the form learns only at the press, such as a PaymentIntent in another currency than the group's: no card was sent. Or the API refused the confirm, such as that of a PaymentIntent already paid. | `currency_mismatch` or `client_secret_wrong_account`, each a section of [fluveo.js errors](https://docs.fluveo.com/payments/fluveo-js-errors.md); else the API's code |
| `api_connection_error`, `api_error` | No answer came back, or one the form cannot read. The payment may have gone through: read the PaymentIntent before the buyer pays again. | None |

An error of the API also carries `message`, `param`, `doc_url` and `payment_intent` when the API sent them.

### The return address

Fluveo adds three parameters to `return_url`, in place of any parameter of the same name:

| Parameter | Value |
| --- | --- |
| `payment_intent` | The PaymentIntent's id, `pi_...`. |
| `payment_intent_client_secret` | Its client secret, for `retrievePaymentIntent()`. |
| `redirect_status` | `succeeded`, `processing` or `failed`. |

`redirect_status` is a hint for the page. Read the PaymentIntent to show the buyer how the payment ended, and mark the order paid only on the `payment_intent.succeeded` [webhook](https://docs.fluveo.com/webhooks.md#fulfil).

## 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.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.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.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.md#currency_mismatch). After a decline the next press runs the whole handler again. Disable Pay while it runs.

## fluveo.retrievePaymentIntent(clientSecret)

Reads a PaymentIntent by its client secret, as the return page does. It resolves with `{ paymentIntent }` or `{ error }`.

```javascript
const clientSecret = new URLSearchParams(location.search).get("payment_intent_client_secret");
const { paymentIntent, error } = await fluveo.retrievePaymentIntent(clientSecret);
```

## The PaymentIntent in the browser

The PaymentIntent that `fluveo.js` returns, from `retrievePaymentIntent()` and from `confirmPayment()`, holds only what a buyer may see:

**Fields**

- `id` (string): The PaymentIntent's id.
- `object` (string): `payment_intent`.
- `amount` (integer): The amount, in the smallest unit of the currency.
- `currency` (string): The three-letter currency code, in lowercase.
- `status` (string): `succeeded`, `processing`, `requires_action`, `requires_payment_method` and the other [PaymentIntent states](https://docs.fluveo.com/payments/accept-a-payment.md).
- `livemode` (boolean): `false` for a test key.
- `payment_method_types` (array of strings): The payment methods the PaymentIntent accepts.
- `next_action` (object, nullable): What the buyer must do next, when `status` is `requires_action`.
- `last_payment_error` (object, nullable): The last failure: `type`, `code`, `decline_code`, `message`, `param` and `doc_url`.
- `created` (integer): When the PaymentIntent was created, in Unix seconds.

It never holds `metadata`, `customer` or `latest_charge`. Read those on your server with the secret key.

## 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.md) has the cause and the fix of each code.

## Test mode

With a test key, the form shows a **Test mode** line with the card `4242 4242 4242 4242`, and the first mount prints one line to the browser console with the PaymentIntent's id, or `deferred_intent` for a form made with `mode`, its payment methods and your page's origin. [Testing](https://docs.fluveo.com/start/testing.md#typed-cards) lists the card numbers to type for a decline and for 3D Secure.

For a browser test, the form is the frame with the title `Fluveo payment form`, the card fields are frames in its slots `#card-number`, `#card-expiry` and `#card-cvc`, and the postal code is its input `#postal`. [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.md#playwright) has a function that types into them. `retrievePaymentIntent()` adds one hidden frame with the title `Fluveo payment status`.

## Next

- [Custom checkout](https://docs.fluveo.com/payments/custom-checkout.md): A whole checkout by copy and paste, with a list to verify it.
- [fluveo.js errors](https://docs.fluveo.com/payments/fluveo-js-errors.md): Each error code, with its cause and its fix.
- [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.md): The PaymentIntent your server creates, and its states.
