Stored Payment Methods

A stored payment method is a card or bank account kept on file against a contact, so you can charge it again later without the customer re-entering their details — for subscriptions, deposits and balances, repeat orders, or a "pay with my saved card" flow.

Storing a method always runs through checkout: the customer enters their details in the Checkout component, and ClientLoop exchanges them for a credential held at the payment provider. Card numbers and account numbers never reach your servers.

What you can and cannot store

Stored?
Card Yes
Bank account Yes
Consumer financing, such as FlexPay No — an offer applies to a single purchase and cannot be kept on file

Not every payment option can be kept on file. Any option that cannot be is withdrawn from a payment session that stores a payment method, even when your checkout configuration enables it — so the customer is never shown a way to pay that you could not charge again. FlexPay is one such option today; treat the set as one that will grow rather than as a single exception.

Step 1: Create a payment session that stores the method (Server)

Set storePaymentMethod on createPaymentSession. It takes three values:

Value What the customer sees Payment options
Disabled Nothing. The default. All of them
AskForConsent A choice: save my details, or not All of them — the customer may decline and pay with one that cannot be stored
Enabled A notice that their details are being kept Only those that can be stored

A stored method belongs to a contact, so contactId becomes required for any value but Disabled — a contactRef (merchant-supplied contact details that are not a platform contact) is not enough. Create the contact first with createContact if you do not have one.

You also declare recurringProcessingModel here: how the method will be charged later. See Choosing a recurring processing model below.

curl -X POST https://GRAPH_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "query": "mutation CreatePaymentSession($input: CreatePaymentSessionInput!) { createPaymentSession(input: $input) { id status amount currency link contactId storePaymentMethod recurringProcessingModel storePaymentMethodResult paymentMethodId } }",
    "variables": {
      "input": {
        "orgId": "35dHMM4pFzIykUsys1CDyZ9Xtkz",
        "contactId": "38crhYn0Rs9ssu1qYToJtpxiNga",
        "amount": "4350.00",
        "currency": "USD",
        "storePaymentMethod": "Enabled",
        "recurringProcessingModel": "UnscheduledCardOnFile"
      }
    }
  }'

The customer pays the 4350.00 as normal, and the method they used is kept on file. storePaymentMethod may only be set when the session is created; it cannot be changed with updatePaymentSession.

Step 2: Load checkout (Client)

Nothing changes. Render the session with the checkout component exactly as described in Checkout:

<checkout-session
  payment-session-id="PAYMENT_SESSION_ID"
></checkout-session>

Under Enabled, checkout tells the customer their details are being saved and shows only the payment options that can be stored. Under AskForConsent it offers the choice and leaves every option in place, so a completed session may store nothing. Read storePaymentMethodResult on the session to find out what happened:

Result Meaning
Stored The method was stored; paymentMethodId identifies it
Declined The customer chose not to save their details
Refused The provider rejected the details; nothing was stored
NotAttempted Storing was Disabled, or the session has not completed

Storing a method without taking a payment

Set amount to "0.00" with storePaymentMethod: "Enabled". The customer's details are validated with the provider and stored, and no payment is taken. Use this to collect a card up front for a subscription that bills later, or to put a backup method on file.

A zero amount requires Enabled rather than AskForConsent: a customer who declined would leave the session with nothing at all to do.

curl -X POST https://GRAPH_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "query": "mutation CreatePaymentSession($input: CreatePaymentSessionInput!) { createPaymentSession(input: $input) { id link paymentMethodId } }",
    "variables": {
      "input": {
        "orgId": "35dHMM4pFzIykUsys1CDyZ9Xtkz",
        "contactId": "38crhYn0Rs9ssu1qYToJtpxiNga",
        "amount": "0.00",
        "currency": "USD",
        "storePaymentMethod": "Enabled",
        "recurringProcessingModel": "Subscription"
      }
    }
  }'

The completed session has a paymentMethodId but no paymentId, because no payment was made. Note that a zero-amount session is still a real authorization: it can be refused, in which case storePaymentMethodResult is Refused and nothing was stored, and where Strong Customer Authentication applies the customer will be asked to authenticate even though nothing is being charged.

Step 3: Read the contact's stored methods (Server)

Contact.paymentMethods is a Relay connection: page forward with first and after, backward with last and before, and treat cursors as opaque. Deleted methods are omitted unless you ask for them with includeDeleted.

curl -X POST https://GRAPH_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "query": "query ContactPaymentMethods($id: ID!) { contact(id: $id) { id name paymentMethods(input: { first: 20 }) { edges { cursor node { id type status provider supportedRecurringProcessingModels card { brand last4 expirationMonth expirationYear funding } bankAccount { institutionName last4 accountType } createdAt } } pageInfo { hasNextPage endCursor } } } }",
    "variables": { "id": "38crhYn0Rs9ssu1qYToJtpxiNga" }
  }'

type tells you which detail block is populated — Card or BankAccount. status tells you whether the method is still chargeable:

Status Meaning
Active Usable for new payments.
Expired Past its expiration date. Ask the customer to store a new method.
Invalid The provider no longer accepts the credential (lost card, closed account).

Fetch a single method directly with the paymentMethod(id:) query when you already hold its id, or with the Relay node(id:) query — PaymentMethod implements Node, so it refetches like any other node.

Payment method ids carry a pmd prefix (pmd2Nc8xVQ1rL9mKZ4tYbWpEjHq) so they are unique across the graph. Treat the id as opaque. The prefix is there so the server can route the id to the right type; do not parse it, strip it, or depend on its shape.

Step 4: Charge a stored method (Server)

chargePaymentMethod takes a payment against a method on file. There is no customer interaction and no checkout component — this is a server-to-server call. It returns a result rather than a payment, because a refusal is a normal outcome here, not an error: expired cards, insufficient funds and issuer declines all come back as success: false with the provider's reason in message.

curl -X POST https://GRAPH_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "query": "mutation ChargePaymentMethod($input: ChargePaymentMethodInput!) { chargePaymentMethod(input: $input) { success message payment { id status amount currency date paymentMethodId recurringProcessingModel } } }",
    "variables": {
      "input": {
        "orgId": "35dHMM4pFzIykUsys1CDyZ9Xtkz",
        "paymentMethodId": "pmd38crhYn0Rs9ssu1qYToJtpxiNga",
        "amount": "129.00",
        "currency": "USD",
        "recurringProcessingModel": "Subscription",
        "transactionRef": "SUB-2026-09-001",
        "idempotencyKey": "SUB-2026-09-001"
      }
    }
  }'

Always send an idempotencyKey. A repeated call with the same key and organization returns the payment created by the first call rather than charging again, so a network timeout and retry cannot double-charge the customer. A natural key is whatever you already use to identify the billing period or order, as above.

Check success before reading payment. The method must be Active; charging an Expired, Invalid or deleted method fails. The currency must match the currency the method was stored under, and the amount must be greater than zero.

Each payment records the recurringProcessingModel it was declared under, so a disputed charge can be answered from the payment itself.

If you set callbackUrl, you receive the same PaymentCreated and PaymentStatusChanged webhooks described in Checkout.

Choosing a recurring processing model

Every stored method carries a recurringProcessingModel, declared when it is stored and repeated on each charge. It tells the card networks what kind of arrangement you have with the customer.

Model Who starts the payment Schedule
CardOnFile The customer, from your checkout n/a
Subscription You Fixed interval
UnscheduledCardOnFile You No fixed interval

The dividing line between the two you initiate is the schedule, not the amount. A monthly invoice whose total changes every month is still a Subscription, because the interval is what is fixed. Reach for UnscheduledCardOnFile when there is no regular interval at all — a top-up triggered by a balance falling below a threshold, or an invoice raised whenever a job is finished.

The choice also decides how a later payment is treated for Strong Customer Authentication: Subscription and UnscheduledCardOnFile charges are ones you initiate and fall outside it, while a CardOnFile charge does not. In exchange, storing a method under Subscription or UnscheduledCardOnFile requires the customer to authenticate during the session that stores it.

A method reports what it can be used for in supportedRecurringProcessingModels. Read it before offering a customer a subscription against a method that may not carry one. A later charge does not have to use the model the method was stored under.

Step 5: Delete a stored method (Server)

deletePaymentMethod releases the credential at the provider and marks the method deleted.

curl -X POST https://GRAPH_URL \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "query": "mutation DeletePaymentMethod($input: DeletePaymentMethodInput!) { deletePaymentMethod(input: $input) { id deletedAt } }",
    "variables": {
      "input": {
        "orgId": "35dHMM4pFzIykUsys1CDyZ9Xtkz",
        "id": "pmd38crhYn0Rs9ssu1qYToJtpxiNga"
      }
    }
  }'

The record is kept in a deleted state so payments already made from it stay attributable, but it drops out of Contact.paymentMethods and can no longer be charged. Deleting a method that is already deleted succeeds and returns it unchanged, so a retry is safe.

Give your customers a way to remove a stored method — it is a common requirement of card-network and consumer-protection rules, and of the agreements you make with them when you keep their details on file.