Skip to content

@movmo_app/payments

@movmo_app/payments is a React SDK that lets partners embed Movmo card capture and saved-payment management directly in their own UI surface, instead of redirecting users to the Movmo accounts UI. It wraps Spreedly Hosted Fields tokenization, the createPaymentMethodFromToken exchange, and the read / update / delete operations on saved methods into drop-in components.

The full PAN never reaches Movmo — tokenization happens client-side in Spreedly iframes against the PCI vault, exactly as it does in the Movmo accounts UI. Partners stay in PCI DSS SAQ A scope.

Current version0.5.0
Peer dependenciesReact 18, React DOM 18, Tailwind CSS 3.4+
LicenseMIT
npm@movmo_app/payments
Terminal window
npm install @movmo_app/payments

Import the package stylesheet once at app boot. The CSS file ships the Tailwind utilities the components depend on plus Spreedly iframe sizing:

import "@movmo_app/payments/dist/style.css";

Configure the SDK once before any component mounts. baseUrl is the Movmo API root (https://e2e.api.movmo.io in pre-prod, https://api.movmo.io in prod):

import { setPaymentsConfig } from "@movmo_app/payments";
setPaymentsConfig({
baseUrl: process.env.REACT_APP_MOVMO_API_URL,
});

PaymentsConfig also accepts an optional iconCdnBaseUrl (override for the CDN that serves card-brand SVGs) and fetch (described below). Defaults to the Movmo e2e icon CDN; override at boot if you ship a prod-tier CDN.

Movmo’s payment endpoints require session-cookie auth plus a CSRF token, and respond with 401 if the session is stale. createMovmoAuthFetch returns a fetch wrapper that handles both — passing it into setPaymentsConfig({ fetch }) is the recommended path for any partner that needs CSRF + session-refresh logic:

import { setPaymentsConfig, createMovmoAuthFetch } from "@movmo_app/payments";
const authFetch = createMovmoAuthFetch({
refreshSession: () =>
fetch("/auth/refresh", { credentials: "include" }).then((r) => {
if (!r.ok) throw new Error("session refresh failed");
}),
onRefreshFailed: () => {
// Sign out and send the user back to your login page.
window.location.href = "/login";
},
extraHeaders: () => ({
"x-api-key": process.env.REACT_APP_MOVMO_API_KEY,
}),
});
setPaymentsConfig({
baseUrl: process.env.REACT_APP_MOVMO_API_URL,
fetch: authFetch,
});

The package never holds your client_secret or refresh token — refreshSession is your hook to call your own backend, which performs the OAuth refresh server-side.

CreateMovmoAuthFetchOptions also exposes advanced overrides for less common environments:

  • csrfCookieName — override the cookie name carrying the CSRF token (defaults to movmo_csrf_token).
  • authPathFragment — substring used to detect requests that must skip the refresh-on-401 retry loop (defaults to /v1/auth/). Override if your auth proxy lives at a different path and you want to avoid infinite refresh recursion.
  • baseFetch — underlying fetch implementation; override only for tests or non-browser runtimes.
  • readCookie — cookie reader; defaults to document.cookie, override only in non-browser test environments.

<MovmoCardForm> renders a complete card-capture form: card number, expiry, CVV, ZIP, cardholder name, and a Save button. Card number and CVV are Spreedly iframes; everything else is rendered in your DOM and styled with the package CSS.

import { MovmoCardForm } from "@movmo_app/payments";
<MovmoCardForm
userId={user.id}
isDefault={true}
defaultCardholderName={`${user.firstName} ${user.lastName}`}
onSuccess={(method) => {
// method: PaymentMethodSummary — { id, last4, brand, expMonth, expYear, isDefault }
console.log("Saved card", method.id);
}}
onError={(message) => {
console.error("Card save failed:", message);
}}
/>;

On submit, the form tokenizes the card against Spreedly client-side and exchanges the resulting token for a Movmo payment method UUID via createPaymentMethodFromToken. onSuccess fires with the saved method; onError fires with a human-readable message.

PropTypeRequiredDescription
userIdstringyesMovmo user UUID — the card is saved to this user.
onSuccess(method: PaymentMethodSummary) => voidyesCalled once the card is saved.
onError(message: string) => voidyesCalled on tokenization or save failure.
isDefaultbooleannoSave as the user’s default card. Defaults to false.
defaultCardholderNamestringnoPre-fill the cardholder name field.
autoSavebooleannoAuto-submit when the form is valid (1200ms debounce).
hideInternalSaveButtonbooleannoOmit the built-in Save button — pair with formId and your own submit control.
onCanSubmitChange(canSubmit: boolean) => voidnoFires when “ready to submit” toggles — useful for driving an external submit button.
onSavingChange(isSaving: boolean) => voidnoFires when the saving state changes.
onErrorDetail(error: PaymentManagerError) => voidnoStructured-error sink fired alongside onError, carrying the originating source (save-from-token, card-tokenize, tokenization-session, spreedly-script). See Error handling.
formIdstringnoHTML id for the <form> element. Defaults to movmo-card-form.
autoFocus"number" | "cvv"noFocus the card-number or CVV iframe on mount.
classNamestringnoWrapper className appended to the form container.

<PaymentMethodsManager> is a higher-level component that renders the user’s saved cards, the add-card form when there are none (or when the user clicks “Add card”), and per-row actions for set-default, edit, and delete. (Edit captures the cardholder name and billing ZIP and is offered only on Spreedly-vaulted cards — the only vault where those fields are mutable.) It is what the Movmo accounts UI uses internally, and the same component drops into a checkout flow with selection-mode props.

Account-management variant:

import { PaymentMethodsManager } from "@movmo_app/payments";
<PaymentMethodsManager
userId={user.id}
defaultCardholderName={`${user.firstName} ${user.lastName}`}
/>;

Checkout selection variant — the user picks which saved card to charge, and selecting a card promotes it to default:

<PaymentMethodsManager
userId={user.id}
selectedId={selected?.id}
onSelect={(method) => setSelected(method)}
selectionSetsDefault={true}
collapsible={true}
/>
PropTypeRequiredDescription
userIdstringyesMovmo user UUID.
selectedIdstringnoControlled selected payment method id.
onSelect(method: PaymentMethodSummary) => voidnoFired when the user selects a row.
onChange(items: PaymentMethodSummary[]) => voidnoFired when the list changes (add / delete / set-default).
onError(error: PaymentManagerError) => voidnoFired on any user-facing failure, with a discriminated source. Additive — errors still render in-SDK. See Error handling.
selectionSetsDefaultbooleannoSelecting a row also sets it as default. Defaults to false.
showDefaultBadgebooleannoRender a “Default” badge on the default card. Defaults to true.
collapsiblebooleannoCollapse the list to a single preview row with a toggle. Defaults to false.
paymentTypeSelectorbooleannoShow a Credit / PayPal / Google Pay radio above the form. Defaults to true. Only Credit is implemented today.
autoSaveFirstCardbooleannoAuto-submit the add-card form for users with zero saved cards. Defaults to true.
cardLabelFormat"masked" | "branded"no•••• 4242 vs Visa 4242. Defaults to "branded".
defaultCardholderNamestringnoPre-fill the embedded <MovmoCardForm> cardholder name.
classNamestringnoWrapper className.

Delete and set-default are applied optimistically and roll back on API failure.

Every failure the SDK surfaces to the user is also rendered in-component (a red banner, or a Retry control for the list). For observability — logging to Sentry, muting a Book button, or any other side effect — pass onError to <PaymentMethodsManager>. It fires with a PaymentManagerError whose source discriminates which operation failed:

<PaymentMethodsManager
userId={user.id}
onError={(error) => {
// error: { source, message, cause? }
Sentry.captureException(error.cause ?? new Error(error.message), {
tags: { paymentSource: error.source },
});
if (error.source === "save-from-token" || error.source === "tokenization-session") {
disableCheckout(); // can't take a new card right now
}
}}
/>
interface PaymentManagerError {
source: PaymentErrorSource;
message: string; // the same copy shown in the in-SDK banner
cause?: unknown; // original Error / rejection, where available
}
type PaymentErrorSource =
| "tokenization-session" // GET /v1/payments/tokenization-session failed
| "spreedly-script" // the Spreedly Hosted Fields iframe script failed to load or init
| "card-tokenize" // Spreedly rejected the card while tokenizing
| "save-from-token" // POST .../payment-methods/from-token failed
| "list" // GET .../payment-methods failed
| "set-default" // the set-default PUT failed
| "update" // the edit-card (cardholder / ZIP) PUT failed
| "delete"; // the delete request failed

onError is additive and never swallows the in-SDK banner. The four add-card sources also surface on <MovmoCardForm> directly via its onErrorDetail prop, for partners composing their own management view around the bare card form.

<PaymentMethodPreview> is the smallest building block — a one-line “brand icon + label” preview of a single saved card. Useful for a collapsed checkout drawer, an order summary row, or any spot where you want to show the user’s selected card without rendering the full <PaymentMethodsManager>.

import {
PaymentMethodPreview,
useUserPaymentMethods,
} from "@movmo_app/payments";
const { items } = useUserPaymentMethods(user.id);
const defaultMethod = items.find((m) => m.isDefault);
{
defaultMethod && (
<PaymentMethodPreview
method={defaultMethod}
cardLabelFormat="masked"
onClick={() => openDrawer()}
/>
);
}
PropTypeRequiredDescription
methodPaymentMethodSummaryyesThe card to render — usually pulled from useUserPaymentMethods.
trailingReact.ReactNodenoSlot rendered after the label. Typical use: an expand-chevron button.
onClick() => voidnoRow click handler. Adds role="button" and Enter/Space keyboard support when present.
cardLabelFormat"masked" | "branded"no•••• 4242 vs Visa 4242. Defaults to "branded".
classNamestringnoWrapper className.

For partners building their own UI on top of the SDK’s data layer.

Fetches the user’s saved cards. Refetches when userId changes; cancels in-flight requests on unmount.

const { items, status, error, refetch } = useUserPaymentMethods(user.id);

Returns { items: PaymentMethodSummary[]; status: "idle" | "loading" | "ready" | "error"; error: string | null; refetch: () => void }.

Calls listPaymentMethods.

Returns { deletePaymentMethod, status, error } with status: "idle" | "pending" | "error". deletePaymentMethod(methodId) returns a promise — await it for rollback-style UX:

const { deletePaymentMethod } = useDeletePaymentMethod(user.id);
await deletePaymentMethod(methodId);

Calls deletePaymentMethod.

Same shape as useDeletePaymentMethod (status: "idle" | "pending" | "error"):

const { setDefault } = useSetDefaultPaymentMethod(user.id);
await setDefault(methodId);

Calls updatePaymentMethod with { isDefault: true }. The server clears the previous default atomically.

Low-level hook for partners who want to compose their own card-form layout. Manages the Spreedly Hosted Fields lifecycle, exposes validity / brand / focus state, and returns a tokenize function plus a programmatic focus helper:

const {
status, // 'idle' | 'loading' | 'ready' | 'error'
error, // string | null
errorSource, // 'tokenization-session' | 'spreedly-script' | null (boot failure only)
errorCause, // unknown — original Error/rejection behind a boot failure (for Sentry); null otherwise
validity, // { number: boolean; cvv: boolean }
brand, // string | null — 'visa' | 'master' | 'american_express' | …
focused, // { number: boolean; cvv: boolean } — live iframe focus
tokenize, // (data: CardholderTokenizeData) => void
focusField, // (field: 'number' | 'cvv') => void
} = useMovmoCardFields({
onCardTokenized: (token) => saveToYourBackend(token),
onFieldErrors: (errors) => setFieldErrors(errors),
onValidityChange: (v) => setCanSubmit(v.number && v.cvv),
onBrandChange: (b) => setDetectedBrand(b),
});
<div id="movmo-card-number" />
<div id="movmo-card-cvv" />
<button
disabled={!validity.number || !validity.cvv}
onClick={() =>
tokenize({ firstName, lastName, month, year, zip })
}
>
Save card
</button>

focused exists because the browser’s :focus-within CSS doesn’t propagate across iframe boundaries — read it to render an “active” border on the iframe shell. focusField('cvv') programmatically moves keyboard focus into a hosted iframe, useful for auto-advancing from an outer expiry input.

OptionTypeDescription
numberElstringDOM id where the card-number field mounts. Defaults to movmo-card-number.
cvvElstringDOM id where the CVV field mounts. Defaults to movmo-card-cvv.
onCardTokenized(token: string) => voidFires with the opaque vault token after a successful tokenize() call.
onFieldErrors(errors: CardFieldError[]) => voidFires with validation or vault-side errors.
onValidityChange(validity: CardFieldValidity) => voidFires when per-field validity flips. Same value also available on the returned validity.
onBrandChange(brand: string | null) => voidFires when the detected card brand changes. Same value also available on the returned brand.

Use this when <MovmoCardForm> does not fit your layout. It does not call the Movmo API on your behalf — you receive the vault token in onCardTokenized and decide what to do with it.

All public types are exported and can be imported directly:

import type {
PaymentMethodSummary,
CardFieldError,
CardFormStatus,
PaymentErrorSource,
PaymentManagerError,
MovmoCardFormProps,
PaymentMethodsManagerProps,
PaymentMethodPreviewProps,
PaymentsConfig,
CreateMovmoAuthFetchOptions,
// useMovmoCardFields
UseMovmoCardFieldsOptions,
UseMovmoCardFieldsResult,
CardholderTokenizeData,
// Hook results / status enums
UseUserPaymentMethodsResult,
UseUserPaymentMethodsStatus,
UseDeletePaymentMethodResult,
UseDeletePaymentMethodStatus,
UseSetDefaultPaymentMethodResult,
UseSetDefaultPaymentMethodStatus,
} from "@movmo_app/payments";

PaymentMethodSummary is a UI-focused subset of the partner-visible shape returned by listPaymentMethods:

interface PaymentMethodSummary {
id: string;
last4: string;
brand: string;
expMonth: number;
expYear: number;
isDefault: boolean;
}

The REST response also carries userId, methodType, createdAt, and updatedAt; the SDK strips these because nothing in the included components needs them. Read them from the REST response directly via listPaymentMethods if you need them in your own UI.

The SDK is a thin wrapper over these operations in the OpenAPI spec:

OperationPath
listPaymentMethodsGET /v1/users/{userid}/payment-methods
getPaymentMethodGET /v1/users/{userid}/payment-methods/{methodid}
createPaymentMethodFromTokenPOST /v1/users/{userid}/payment-methods/from-token
updatePaymentMethodPUT /v1/users/{userid}/payment-methods/{methodid}
deletePaymentMethodDELETE /v1/users/{userid}/payment-methods/{methodid}

The SDK also calls an internal GET /v1/payments/tokenization-session endpoint to obtain the signed Spreedly iframe parameters. This endpoint is implementation detail — partners never call it directly, and it is not part of the partner integration surface.