Docs
API 2026-05-27.dahlia Dashboard

Payments

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 builds a whole checkout with it, step by step. Payment Element explains the form itself: its payment methods, its layouts, its look and its options.

Load the script#

In your page
<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
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. 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
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>.

PartWhat 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, and a part used outside <Elements> throws elements_provider_missing. The React tab of Custom checkout 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
  • clientSecretstringRequired

    The client_secret of a PaymentIntent your server created with the secret key of the same account and mode.

  • modestringOptional

    "payment", its only value: the form mounts before a PaymentIntent exists. It takes the place of clientSecret and needs amount and currency.

  • amountintegerOptional

    With mode: what the buyer will pay, in the smallest unit of the currency, 1 or more.

  • currencystringOptional

    With mode: the three-letter currency code.

It also takes appearance, locale and loader: see Form options.

javascript
const elements = fluveo.elements({ clientSecret });
Call on the groupWhat 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.

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.

CallWhat 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().

Every handler gets one object with elementType: "payment".

EventWhenWhat the object adds
readyThe form is on the page and can take input.Nothing.
changeWhat 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, blurThe buyer entered or left the form.Nothing.
loaderrorThe 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, and its message lists what the option takes.

Options of fluveo.elements()
  • appearanceobjectOptional

    The form's look. Default: the "fluveo" theme, with spaced inputs. The default of each variable in each theme is on the page Appearance.

    5 child fields
    • themestringOptional

      "fluveo", the default. "night", for a dark page. "flat": filled fields with no border.

    • variablesobjectOptional

      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, which says what each kind takes.

      10 child fields
      • colorPrimarystringOptional

        A color: the border and the ring of the field in focus, and the selected tab.

      • colorBackgroundstringOptional

        A color: the background of the fields and the tabs.

      • colorTextstringOptional

        A color: the form's text.

      • colorTextSecondarystringOptional

        A color: the secondary text, such as a tab that is not selected.

      • colorTextPlaceholderstringOptional

        A color: the placeholders.

      • colorDangerstringOptional

        A color: an error, and the border of its field.

      • fontFamilystringOptional

        A font family: a list of font names the buyer's device has.

      • fontSizeBasestringOptional

        A length: the base size of the form's text.

      • spacingUnitstringOptional

        A length: the paddings and the gaps follow it.

      • borderRadiusstringOptional

        A length: the corners of the fields and the tabs.

    • labelsstringOptional

      "above": over each field. "floating": inside the field until the buyer types. "auto", the default: floating with condensed inputs, above with spaced ones.

    • inputsstringOptional

      "spaced", the default: each field is a box of its own. "condensed": the fields are joined in one block.

    • disableAnimationsbooleanOptional

      true: the form does not animate. Default: false.

  • localestringOptional

    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.

  • loaderstringOptional

    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
const elements = fluveo.elements({
  clientSecret,
  appearance: {
    theme: "night",
    labels: "floating",
    variables: { colorPrimary: "#0f766e", borderRadius: "8px" },
  },
});
locale
const elements = fluveo.elements({ clientSecret, locale: "pt-BR" });
loader
const elements = fluveo.elements({ clientSecret, loader: "never" });
Options of elements.create()
  • layoutstring or objectOptional

    How the form lists its payment methods: a type alone, or an object with a type. Default: "tabs".

    4 child fields
    • typestringRequired

      "tabs": the methods above the fields. "accordion": the methods as rows, and the open row holds the fields. "auto": tabs.

    • defaultCollapsedbooleanOptional

      true: no method is open until the buyer opens one. Default: false.

    • radiosstring or booleanOptional

      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".

    • spacedAccordionItemsbooleanOptional

      true: each accordion row is a box of its own. Default: false.

  • fieldsobjectOptional

    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.

    1 child field
    • billingDetailsstring or objectOptional

      "auto", "never", which hides both fields, or an object with address.

      1 child field
      • addressstring or objectOptional

        "auto", "never", which hides both fields, or an object with the two fields.

        2 child fields
  • defaultValuesobjectOptional

    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.

    1 child field
    • billingDetailsobjectOptional
      1 child field
      • addressobjectOptional
        2 child fields
        • countrystringOptional

          The two capital letters (ISO 3166-1) of a country in the form's list, such as "CA".

        • postal_codestringOptional

          A text.

layout
const element = elements.create("payment", {
  layout: { type: "accordion", defaultCollapsed: true, spacedAccordionItems: true },
});
fields
const element = elements.create("payment", {
  fields: { billingDetails: { address: { postalCode: "never" } } },
});
defaultValues
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.

OptionWhat an update does
appearanceReplaces the last appearance whole, at once: a part the new one leaves out goes back to its default.
loaderHolds from the next mount().
amount, currencyOnly on a group made with mode. On a group made with clientSecret they throw option_not_supported: the PaymentIntent holds the amount.

locale and fonts throw 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: 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
  • elementsobjectRequired

    The group that fluveo.elements() returned, with its payment element mounted.

  • clientSecretstringOptional

    Only with mode, which requires it: the client_secret of the PaymentIntent to confirm.

  • confirmParamsobjectRequired
    2 child fields
    • return_urlstringRequired

      An absolute address of your site. The buyer lands there when the payment is done, and after 3D Secure.

    • payment_method_dataobjectOptional

      Only billing_details: name, email, phone and address (line1, line2, city, state, postal_code, country), each a string.

  • redirectstringOptional

    "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 paymentWith redirect: "always"With redirect: "if_required"
Succeeded, or is processingThe browser goes to return_url.The promise resolves with { paymentIntent } and the page stays.
Needs 3D SecureThe browser goes to the authentication page, then to return_url.The same.
Failed, such as a declined card or an incomplete fieldThe 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.typeWhat happenederror.code
validation_errorA field is incomplete or not valid. Nothing was sent.incomplete_number, incomplete_expiry, incomplete_cvc, incomplete_zip or incomplete_card
card_errorThe card was refused.The API's code, such as card_declined, with decline_code
invalid_request_errorA 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; else the API's code
api_connection_error, api_errorNo 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:

ParameterValue
payment_intentThe PaymentIntent's id, pi_....
payment_intent_client_secretIts client secret, for retrievePaymentIntent().
redirect_statussucceeded, 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.

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 }) 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.
  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() says. Without clientSecret it throws confirm_client_secret_missing, and without the submit() of its press, submit_required.
checkout.js
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. 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
  • idstring

    The PaymentIntent's id.

  • objectstring

    payment_intent.

  • amountinteger

    The amount, in the smallest unit of the currency.

  • currencystring

    The three-letter currency code, in lowercase.

  • statusstring

    succeeded, processing, requires_action, requires_payment_method and the other PaymentIntent states.

  • livemodeboolean

    false for a test key.

  • payment_method_typesarray of strings

    The payment methods the PaymentIntent accepts.

  • next_actionobjectNullable

    What the buyer must do next, when status is requires_action.

  • last_payment_errorobjectNullable

    The last failure: type, code, decline_code, message, param and doc_url.

  • createdinteger

    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 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 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 has a function that types into them. retrievePaymentIntent() adds one hidden frame with the title Fluveo payment status.

Next#