Elements Card Storage
Use card storage when an Elements integration needs to add a card to a card holder account, set a new default card, replace an existing default card, or prepare a card for future subscription or recurring payments.
Card storage is controlled by the Payment Intent. The browser does not decide which card holder account should be updated or how the stored card should be applied. Your backend creates the Payment Intent with the account and storage settings, then returns those values in the PaymentIntentSession used by Elements.
The card-storage and recurring setup fields are:
account_id:string- identifies the card holder account to update.cardHolderAccount:{ storeCard: boolean; consentCaptureMode: 'MERCHANT_MANAGED' | 'CITYPAY_OPT_IN' | 'NONE'; cardOnFileBehaviour: 'APPEND' | 'SET_DEFAULT' | 'REPLACE_DEFAULT' }- controls whether the attached card should be stored and how it should be applied to the account.recurringIntent:{ count: number; endDate: string; fixedAmount: boolean; fixedSchedule: boolean; intervalDays: number; nextDate: string; recurringAmount: number }- describes the recurring agreement for 3DS setup metadata.
recurringIntent.endDate and recurringIntent.nextDate are date-only ISO values, for example 2026-04-01. recurringIntent.recurringAmount is an amount in minor units.
cardHolderAccount fields:
storeCard: requests that the attached card is stored when consent and account settings allow it.consentCaptureMode: defines whether consent is captured by your UI, by CityPay Flow, or not captured.cardOnFileBehaviour: controls whether the stored card is appended, set as default, or replaces the default card.
recurringIntent fields:
count: number of expected recurring payments.endDate: date-only ISO value for when the recurring agreement is expected to end.fixedAmount: indicates whether recurring payments are expected to use a fixed amount.fixedSchedule: indicates whether recurring payments are expected to follow a fixed schedule.intervalDays: number of days between scheduled recurring payments.nextDate: date-only ISO value for the first expected recurring payment after setup.recurringAmount: expected recurring amount in minor units.
The TypeScript SDK uses camel-cased fields such as cardHolderAccount and recurringIntent. Raw API payloads use snake-cased fields such as card_holder_account and recurring_intent.
Create the Payment Intent on your backend. Include account_id when the payment method should be associated with a card holder account, and include cardHolderAccount when the intent is allowed to store or replace a card.
const paymentSession = await citypay.paymentIntents.create({
merchantid,
account_id: 'customer-123',
amount: 1995,
currency: 'GBP',
identifier: 'order-123',
billTo: {
firstName: 'Alex',
lastName: 'Customer',
address1: '1 Test Street',
city: 'London',
postcode: 'EC1A 1AA',
country: 'GB',
email: 'alex.customer@example.com',
},
cardHolderAccount: {
storeCard: true,
consentCaptureMode: 'CITYPAY_OPT_IN',
cardOnFileBehaviour: 'APPEND',
},
});
Common cardOnFileBehaviour values:
| Value | Behaviour |
|---|---|
APPEND | Adds the card to the account without changing the current default. Existing cards remain unchanged, and the new card is appended to the account's stored payment methods. |
SET_DEFAULT | Adds the card and sets it as the new default payment method. Any previously default card remains stored but is no longer marked as the default. |
REPLACE_DEFAULT | Adds the card and replaces the existing default payment method. The new card becomes the default, and any previously default card may be removed or deactivated depending on implementation. |
REPLACE_DEFAULT is a destructive operation and may remove fallback payment methods. Use it with care when payment resilience is important, for example on subscriptions where a fallback card may help avoid failed renewals.
Common consent modes:
| Value | Meaning |
|---|---|
MERCHANT_MANAGED | Consent is captured by your own UI. You are responsible for presenting suitable wording, checkboxes, terms, or subscription agreements and for meeting card scheme and regulatory requirements. |
CITYPAY_OPT_IN | Consent is captured by a CityPay-provided opt-in control. This is typically used with hosted or simplified Flow integrations where CityPay presents the save-card choice before storage. |
NONE | No consent capture is performed. Use this only when card details are not being stored, for example when storeCard=false. Do not use this mode with card storage unless you have confirmed it is valid for your compliance model. |
createServerIntent() returns the PaymentIntentSession used by Elements in the browser. If your backend returns the full response from paymentIntents.create(), no extra mapping is needed.
If your backend builds a smaller session object, include the cardHolderAccount values from the Payment Intent so Elements can apply the same card storage behaviour in the browser.
return {
paymentIntentId: paymentSession.paymentIntentId,
sessionToken: paymentSession.sessionToken,
opaqueKey: paymentSession.opaqueKey,
cardHolderAccount: paymentSession.cardHolderAccount,
};
On the frontend:
const elements = await citypay.elements({
pubKey: 'XXZZYY...',
createServerIntent: async () => {
const res = await fetch('/api/payments/intent-session', { method: 'POST' });
const data = await res.json();
return data.paymentSession;
},
});
Use cardElement() when you want to own the form, submit button, consent text, and result UI.
CardElement does not render a CityPay-hosted save-card checkbox. If the customer can choose whether to save the card, render your own checkbox or consent control and pass the captured value to attach({ storeCard }).
const saveCard = saveCardCheckbox.checked;
const tokeniseResponse = await card.tokenise();
const token = tokeniseResponse.data.cp_card_token;
const attachResult = await card.attach({
intentId,
token,
storeCard: saveCard,
});
If you omit storeCard, CardElement can derive the value only when it is safe:
| Session configuration | CardElement attach value |
|---|---|
No cardHolderAccount | store_card=false |
storeCard=true, consentCaptureMode=MERCHANT_MANAGED | store_card=true |
storeCard=true, consentCaptureMode=CITYPAY_OPT_IN | store_card=false unless your UI passes storeCard: true |
Use MERCHANT_MANAGED with CardElement when your own UI captures card-storage consent. Use CITYPAY_OPT_IN with Flows when you want CityPay to host the save-card checkbox.
Use paymentFlow() or verifyFlow() when you want the prebuilt Elements experience to handle the action button, processing state, result screens, retry UI, and CityPay-hosted card-storage consent where configured by the Payment Intent.
Flows read cardHolderAccount from the PaymentIntentSession.
Payment Intent cardHolderAccount | Flow UI | Attach value |
|---|---|---|
| Not supplied | No save-card checkbox | store_card=false |
storeCard=false | No save-card checkbox | store_card=false |
storeCard=true, consentCaptureMode=MERCHANT_MANAGED | No save-card checkbox because consent is captured by your own UI | store_card=true |
storeCard=true, consentCaptureMode=CITYPAY_OPT_IN | Shows "Save this card for future payments" | Customer checkbox value |
For MERCHANT_MANAGED, Flow does not show a save-card checkbox because your integration is responsible for consent capture before the Flow is started. In that case, storeCard=true means Flow attaches the card with store_card=true.
Example:
const flow = elements.paymentFlow({
element: '#payment-flow',
});
await flow.init();
await flow.awaitReady();
verifyFlow() follows the same storage rules, but verifies a card without taking payment. This is useful when updating the default card for an existing subscription or account.
For subscription-style initial payments, include recurringIntent on the customer-present Payment Intent. These values describe the recurring agreement for the initial authentication, so 3DS setup metadata can reflect the expected schedule, amount behaviour, and next payment date.
recurringIntent does not by itself charge future payments or create a merchant-side subscription schedule. Your system remains responsible for creating and running the future recurring payment schedule.
Dates should be date-only ISO values, for example 2026-04-01.
const paymentSession = await citypay.paymentIntents.create({
merchantid,
account_id: 'subscription-customer-123',
amount: 999,
currency: 'GBP',
identifier: 'subscription-123-initial',
billTo,
cardHolderAccount: {
storeCard: true,
consentCaptureMode: 'MERCHANT_MANAGED',
cardOnFileBehaviour: 'SET_DEFAULT',
},
recurringIntent: {
count: 12,
endDate: '2027-03-31',
fixedAmount: true,
fixedSchedule: true,
intervalDays: 30,
nextDate: '2026-04-01',
recurringAmount: 999,
},
});
When retrying with the same Payment Intent, reuse the same Payment Intent id and session where appropriate. If your backend rebuilds the session response, keep the same cardHolderAccount values in that response.
Elements uses the PaymentIntentSession returned by createServerIntent(). It does not fetch the Payment Intent again to discover card storage settings.