paymentMethodId. Card data never reaches your servers.
Saving the card gives no spending permission. The agent pays only within an order intent that the user approves, and one order intent can cover several payments. See Approve an Order Intent.
Prerequisites
- Crossmint API key — a client-side key with the
payment-methods.create,payment-methods.read, andpayment-methods.updatescopes. Theupdatescope lets the user re-enter the card security code (CVC) later, when a card credential needs it. In staging, the pre-generated development keys on the API Keys page have full access. A key that you create needs these scopes. - User JWT — sign in the user with your auth provider, registered under 3P Auth providers or Custom tokens in the Crossmint Console. See Bring Your Own Auth.
- SDK version —
@crossmint/client-sdk-react-ui5.1.0 or later. To upgrade from an earlier version, see Upgrade from Earlier SDK 5 Versions or Upgrade from SDK 4.
Saved card limit. In production, a project can save up to 50 cards without contacting Crossmint. To save more, contact our sales team. The first 1,000 saved cards are free, and after that each saved card costs 0.05 USD. The limit counts the saved cards of all the project’s users; deleted cards do not count. Staging has no limit. The limit applies to cards only: bank accounts have no limit, but in production saving them must be enabled for your project.At the limit, saving another card returns
400 with the code payments:payment-method.card-limit-reached, and the project’s limit in parameters.limit. Cards that are already saved keep working, so agents can still spend with them. To save a new card, remove a card that a user no longer needs. To raise the limit, contact the Crossmint team.Save the Card
1
Install the SDK
2
Wrap your app with the Crossmint provider
Set up
CrossmintProvider at the root of your app. This makes the Crossmint SDK available to all child components.3
Add the card form and your submit button
Place this component on a page where the user manages their payment methods, or in an ephemeral UI dedicated to saving cards.The
CrossmintPaymentMethodManagement component handles all PCI compliance concerns. Card data is collected directly by Crossmint and never passes through your servers — this is why the UI must be rendered inside your app rather than called from your backend.The component renders no submit button. Render your own button and call ref.current.submit() from it:- Keep the button disabled until
onReadyfires andonChangereportscomplete: true. - Show the
messageof aninvalid,failed, orunavailableresult. The card form shows field errors, not this message. - Clear your message on the next
onChange, when the user edits the form.
jwt comes from your auth provider. submit() never rejects, so it needs no try/catch, and a second click during a submit returns the same result, so the card saves once.The form also asks for a billing address. The address fields appear once the card fields are complete, and complete turns true when the card and the address are valid. The cardholder name is optional: complete turns true without it. To save cards without an address, see Billing Address.submit() resolves with a status:savedorselected:onPaymentMethodSelectedreceived the payment method.invalid,failed, orunavailable: showmessage. Each also carries acode, such asincomplete_cardorsave_failed. To show your own text, branch oncode, and treat a code that you do not handle as a generic error.superseded: the component changed during the submit, for example after a newjwt. Ignore the result.
In staging, use the test card number 4242 4242 4242 4242 with any future expiration date and any three-digit CVC. A user can save the same card once: saving it again in a new form resolves
failed with the code save_failed. For other test cards, see Test Agent Cards.4
Store the payment method ID
When the user saves a card, the
onPaymentMethodSelected callback returns a paymentMethodId. The saved result of ref.current.submit() carries the same payment method. Persist this ID in your backend associated with the user, apply strict access controls, and avoid exposing it in client-side storage or logs.The ID cannot reveal the original card number, but it can be used to register the card and create order intents.Let Users Pick a Saved Card
Add"existing" to allowedModes to list the cards that the user saved before. The user picks one, or adds a new card.
In the SaveCard component above, import useMemo from React, add these two values, and replace the component and the button:
- While a saved card is selected,
onChangereportsmode: "existing"andcomplete: true, andsubmit()resolvesselectedwithout saving anything. - After a new card saves, the component lists it and selects it.
- If the user has no saved cards, the component shows only the card form.
Billing Address
The form collects a billing address by default, because issuers decline more cards that have none. The address fields appear once the card fields are complete, or after a submit, andcomplete stays false until the address is valid. If the card is complete but the address is not, submit() resolves with status: "invalid" and code: "invalid_billing_address". Your users can search for the address or type it in.
fields.billingDetails.address controls the address fields:
true(the default): the form asks for the address, and address line 2 is optional.- An object: the form asks for the address, and
line2sets address line 2 to"optional"(the default),"required", or"hidden". false: the form does not ask for an address.
fields to SaveCard and pass it to the component:
{ billingDetails: { address: false } } instead.
The saved card returns the address in card.billing.address:
country is a 2-letter ISO code. From this component, line2 is an empty string when it is left empty, and so are postalCode and stateOrRegion in countries that do not use them. A card saved through the API can also lack line2 and stateOrRegion.
If
allowedModes includes "existing", your users can select a card saved without an address: a card saved with address: false, with SDK 5.0 or earlier, or through the API without one. Check that card.billing.address exists before you use it.Prefill the Billing Address
PassdefaultValues.billingDetails.address to start the form with an address that you already have. The user can edit every value. In SaveCard, add defaultValues and pass it to the component:
country is an ISO 3166-1 alpha-2 code, and stateOrRegion is the ISO 3166-2 subdivision code without the country prefix ("FL", not "US-FL" or "Florida"). null or undefined at any level means no default, and so does an empty string in an address field, so you can pass values from your database as they are. Otherwise, defaultValues, billingDetails, and address must be objects, not arrays, and each field must be a string, or the SDK throws. The form ignores an invalid field value with a console warning and keeps the other values.
With address: false, the form has no address fields, so it ignores defaultValues.billingDetails.address and logs a console warning. With line2: "hidden", it ignores the line2 default.
Troubleshooting
The save fails because Crossmint cannot verify the JWT
The save fails because Crossmint cannot verify the JWT
submit() resolves failed with save_failed and a message such as “Couldn’t find a JWT signing key in the JWKS for kid …”. With allowedModes set to ["new"], the card form loads and onReady fires before Crossmint checks the JWT, so the error appears on the first save. In the Console, check the JWT authentication settings of the project that owns your API key: the JWKS endpoint must be reachable from the internet and list the key ID (kid) that signs your JWTs, and the issuer must match the iss claim.The card form reloads while the user types
The card form reloads while the user types
The value of a prop that travels in the card form URL changed:
jwt, allowedModes, allowedPaymentMethodTypes, fields, defaultValues, or appearance. A new object or array with the same values does not reload the form. Check that these values do not change while the user types, for example a JWT that your app refreshes on each render.Saving a test card again fails
Saving a test card again fails
A user can save the same card once. Saving it again in a new form resolves
failed with save_failed and the message “Payment method already exists for this user with the same details”. Submitting the same form again after a save returns the saved card. Use another test card, sign in as another user, or remove the card.Upgrade from Earlier SDK 5 Versions
From SDK 5.1, the form asks for a billing address by default. After the upgrade, your users enter an address with each new card, andcomplete stays false until the address is valid. Your code needs no change.
To keep the SDK 5.0 behavior, pass fields={{ billingDetails: { address: false } }}. Issuers decline more cards that have no address.
In SDK 5.0, defaultValues worked only together with fields.billingDetails.address. From SDK 5.1, defaultValues works on its own, so you can remove a fields prop that you added only for the prefill.
Upgrade from SDK 4
In SDK 4, the card form rendered its own Save button and showed the submit errors. In SDK 5, the card form has no button: your app renders the button, callsref.current.submit(), and shows the error message.
If you used the card form’s button, the form has no way to save after the upgrade. Add a ref, your button, and the error message. submit and error are the ones in the Save the Card sample:
Before
After
hideSubmitButton with your own button:
- Remove
hideSubmitButton. TypeScript reports it as an unknown prop. - Show the
messageof aninvalid,failed, orunavailableresult. In SDK 5, the card form does not show it.
onReady fires and onChange reports complete: true. SDK 4 had no signal for this, so a click before the form loads resolves unavailable with not_ready. The form also asks for a billing address now, as described in Upgrade from Earlier SDK 5 Versions.
Next Steps
Register a Card
Find the rails that the saved card supports
Approve an Order Intent
Let the user approve a limit on the card
SDK Reference
All props, results, and codes of the component

