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#
<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: script-src https://elements.fluveo.com; frame-src https://elements.fluveo.comnpm 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.
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.
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, 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.
const fluveo = Fluveo("pk_test_...");fluveo.elements(options)#
Starts the form for one PaymentIntent and returns an elements group.
The
client_secretof a PaymentIntent your server created with the secret key of the same account and mode."payment", its only value: the form mounts before a PaymentIntent exists. It takes the place ofclientSecretand needsamountandcurrency.With
mode: what the buyer will pay, in the smallest unit of the currency, 1 or more.With
mode: the three-letter currency code.
It also takes appearance, locale and loader: see Form options.
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.
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.
| 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(). |
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.
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.
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
"fluveo", the default."night", for a dark page."flat": filled fields with no border.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
A color: the border and the ring of the field in focus, and the selected tab.
A color: the background of the fields and the tabs.
A color: the form's text.
A color: the secondary text, such as a tab that is not selected.
A color: the placeholders.
A color: an error, and the border of its field.
A font family: a list of font names the buyer's device has.
A length: the base size of the form's text.
A length: the paddings and the gaps follow it.
A length: the corners of the fields and the tabs.
"above": over each field."floating": inside the field until the buyer types."auto", the default: floating with condensed inputs, above with spaced ones."spaced", the default: each field is a box of its own."condensed": the fields are joined in one block.true: the form does not animate. Default:false.
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.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.
const elements = fluveo.elements({
clientSecret,
appearance: {
theme: "night",
labels: "floating",
variables: { colorPrimary: "#0f766e", borderRadius: "8px" },
},
});const elements = fluveo.elements({ clientSecret, locale: "pt-BR" });const elements = fluveo.elements({ clientSecret, loader: "never" });How the form lists its payment methods: a type alone, or an object with a
type. Default:"tabs".4 child fields
"tabs": the methods above the fields."accordion": the methods as rows, and the open row holds the fields."auto": tabs.true: no method is open until the buyer opens one. Default:false.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.trueis"auto", andfalseis"never".true: each accordion row is a box of its own. Default:false.
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, orconfirmPayment()throws billing_details_missing.1 child field
"auto","never", which hides both fields, or an object withaddress.1 child field
"auto","never", which hides both fields, or an object with the two fields.2 child fields
"auto"or"never"."auto"or"never".
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
1 child field
2 child fields
The two capital letters (ISO 3166-1) of a country in the form's list, such as
"CA".A text.
const element = elements.create("payment", {
layout: { type: "accordion", defaultCollapsed: true, spacedAccordionItems: true },
});const element = elements.create("payment", {
fields: { billingDetails: { address: { postalCode: "never" } } },
});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: 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.
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.
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.
The group that
fluveo.elements()returned, with its payment element mounted.Only with
mode, which requires it: theclient_secretof the PaymentIntent to confirm.2 child fields
An absolute address of your site. The buyer lands there when the payment is done, and after 3D Secure.
Only
billing_details:name,email,phoneandaddress(line1,line2,city,state,postal_code,country), each a string.
"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.
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; 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.
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.
fluveo.elements({ mode: "payment", amount, currency })makes the group, withamountin the smallest unit of the currency. Mounting creates nothing. A payment your account cannot take comes as theloaderrorevent.elements.update({ amount, currency })sets the cart's new amount.- 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 }, avalidation_error: stop there. Beforemount()it throws submit_not_mounted. - Your server creates the PaymentIntent and gives the page its
client_secret. fluveo.confirmPayment({ elements, clientSecret, confirmParams })confirms it and ends as confirmPayment() says. WithoutclientSecretit throwsconfirm_client_secret_missing, and without thesubmit()of its press,submit_required.
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 }.
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:
idstringThe PaymentIntent's id.
objectstringpayment_intent.amountintegerThe amount, in the smallest unit of the currency.
currencystringThe three-letter currency code, in lowercase.
statusstringsucceeded,processing,requires_action,requires_payment_methodand the other PaymentIntent states.livemodebooleanfalsefor a test key.payment_method_typesarray of stringsThe payment methods the PaymentIntent accepts.
What the buyer must do next, when
statusisrequires_action.The last failure:
type,code,decline_code,message,paramanddoc_url.createdintegerWhen 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.