Pay
Updating Payment Methods
Payment Method Form

Updating Payment Methods via Form

Learn how to build custom forms for updating payment method details with secure input fields


The Payment Method Form SDK allows you to update non-sensitive payment method details through secure, PCI-compliant forms. While card numbers and account numbers cannot be changed, you can update billing addresses, revalidate security codes (CVV/ZIP), and modify account holder information. This enables customers to keep their payment methods current without re-entering complete payment details.

Prerequisites

Before building forms to update payment methods, it's helpful to learn about the following topics.


When to Use Update Forms


Use update forms when you need to modify non-sensitive payment method details through a customer-facing interface. Update forms are ideal for self-service account management where customers can maintain their own payment information without contacting support.

Common use cases

  • Billing Address Updates: Customer moves or needs to correct their billing address
  • CVV Revalidation: Verify card ownership for high-value transactions or subscription renewals
  • ZIP Code Verification: Revalidate postal code for Address Verification System (AVS) checks
  • Account Holder Corrections: Fix typos or update names after marriage or legal name changes
  • Expired Card Updates: Update expiration dates without changing the card number

Creating an Update Intent

Before displaying any update form, create an intent on your server that links to the existing payment method.

import payload
from flask import Flask, jsonify, request
 
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
 
app = Flask(__name__)
 
 
@app.route('/update-intent', methods=['POST'])
def update_intent():
    payment_method_id = request.json["payment_method_id"]
 
    # Create an intent for updating the payment method
    intent = pl.Intent.create(
        type='payment_method_form',
        payment_method_form={
            'payment_method_template': {
                'id': payment_method_id,
            }
        }
    )
 
    # Return client token
    return jsonify({'client_token': intent.token})
 
 
 
if __name__ == "__main__":
    app.run(port=3000)

The intent:

  • Links to the existing payment method via the id field
  • Returns a token used to initialize the form
  • Allows updating non-sensitive fields (billing address, account holder, etc.)
  • Supports optional CVV/ZIP revalidation

Pass the payment_method_id from your database or session to create the intent, then use the returned token to initialize the update form on the frontend.


Updating Billing Address

Allow customers to update their billing address for AVS verification and accurate billing.

<!DOCTYPE html>
<html>
<head>
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <h2>Update Billing Address</h2>
 
  <form id="update-address-form" pl-form="payment_method">
    <div>
      <label>Street Address</label>
      <input
        type="text"
        pl-input="billing_address[street_address]"
        placeholder="123 Main St"
        required
      />
    </div>
 
    <div>
      <label>Apartment, suite, etc. (optional)</label>
      <input
        type="text"
        pl-input="billing_address[unit_number]"
        placeholder="Apt 4B"
      />
    </div>
 
    <div>
      <label>City</label>
      <input
        type="text"
        pl-input="billing_address[city]"
        placeholder="San Francisco"
        required
      />
    </div>
 
    <div style="display: flex; gap: 1rem;">
      <div style="flex: 1;">
        <label>State</label>
        <input
          type="text"
          pl-input="billing_address[state_province]"
          placeholder="CA"
          required
        />
      </div>
 
      <div style="flex: 1;">
        <label>ZIP Code</label>
        <input
          type="text"
          pl-input="billing_address[postal_code]"
          placeholder="94102"
          required
        />
      </div>
    </div>
 
    <button type="submit">Update Address</button>
  </form>
 
  <script src="update-address-form.js"></script>
</body>
</html>
const paymentMethodId = 'pm_1234567890' // From URL or state
 
// Fetch client token from your server
fetch('/update-payment-method-intent', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ payment_method_id: paymentMethodId })
})
  .then(res => res.json())
  .then(data => {
    // Initialize Payload with the client token
    Payload(data.token)
 
    // Initialize the update form
    const form = new Payload.Form(
      document.getElementById('update-address-form')
    )
 
    // Listen for successful update
    form.on('updated', (evt) => {
      console.log('Address updated:', evt.payment_method_id)
      alert('Billing address updated successfully!')
      window.location.href = '/account/payment-methods'
    })
 
    // Listen for errors
    form.on('error', (evt) => {
      console.error('Error:', evt.message)
      alert(`Update failed: ${evt.message}`)
    })
  })

This example:

  1. Display: Shows form with existing address values that customer can modify
  2. Submission: Updates billing address on the existing payment method
  3. Validation: Verifies address format and updates the stored data

The form pre-fills with the current billing address, allowing customers to modify only the fields that changed.


Revalidating CVV for Security

Require CVV revalidation for high-value transactions or subscription renewals without storing the CVV.

<!DOCTYPE html>
<html>
<head>
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <h2>Verify Card Security Code</h2>
  <p>Please enter your CVV to confirm card ownership</p>
 
  <form id="cvv-validation-form" pl-form="payment_method">
    <!-- Display card info (last 4, brand) -->
    <div>
      <p>Card ending in <strong id="card-last4">****</strong></p>
    </div>
 
    <!-- Secure CVV input field -->
    <div>
      <label>CVV Code</label>
      <div pl-input="card_code" placeholder="123"></div>
      <small>3-digit code on the back of your card</small>
    </div>
 
    <button type="submit">Verify CVV</button>
  </form>
 
  <script src="cvv-validation-form.js"></script>
</body>
</html>
const paymentMethodId = 'pm_1234567890' // From URL or state
 
// Fetch payment method details
fetch(`/payment-methods/${paymentMethodId}`)
  .then(res => res.json())
  .then(paymentMethod => {
    const last4El = document.getElementById('card-last4')
    if (last4El) last4El.textContent = paymentMethod.card?.last4 || '****'
  })
 
// Fetch client token for CVV validation
fetch('/update-payment-method-intent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ payment_method_id: paymentMethodId })
})
  .then(res => res.json())
  .then(data => {
    Payload(data.token)
 
    const form = new Payload.Form(
      document.getElementById('cvv-validation-form')
    )
 
    // Listen for successful CVV validation
    form.on('updated', (evt) => {
      console.log('CVV validated for:', evt.payment_method_id)
      alert('Card verified successfully!')
      // Proceed with high-value transaction
      window.location.href = '/checkout/complete'
    })
 
    // Listen for errors
    form.on('error', (evt) => {
      console.error('CVV validation failed:', evt.message)
      alert(`Verification failed: ${evt.message}`)
    })
  })

This example:

  1. Display: Shows secure CVV input field
  2. Validation: Verifies CVV matches the card on file
  3. Security: CVV is validated but never stored permanently

Use CVV revalidation to:

  • Verify card ownership before high-value purchases
  • Confirm payment method is still valid
  • Prevent fraud on stored payment methods
  • Meet compliance requirements for recurring billing

Revalidating ZIP Code

Verify billing ZIP code for Address Verification System (AVS) checks.

<!DOCTYPE html>
<html>
<head>
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <h2>Verify Billing ZIP Code</h2>
  <p>Please confirm your billing ZIP code for AVS verification</p>
 
  <form id="zip-validation-form" pl-form="payment_method">
    <!-- Display card info (last 4, brand) -->
    <div>
      <p>Card ending in <strong id="card-last4">****</strong></p>
    </div>
 
    <!-- Secure ZIP input field -->
    <div>
      <label>ZIP / Postal Code</label>
      <input type="text" pl-input="billing_address[postal_code]" placeholder="94102">
      <small>Enter the billing ZIP code for your card</small>
    </div>
 
    <button type="submit">Verify ZIP Code</button>
  </form>
 
  <script src="zip-validation-form.js"></script>
</body>
</html>
const paymentMethodId = 'pm_1234567890' // From URL or state
 
// Fetch payment method details
fetch(`/payment-methods/${paymentMethodId}`)
  .then(res => res.json())
  .then(paymentMethod => {
    const last4El = document.getElementById('card-last4')
    if (last4El) last4El.textContent = paymentMethod.card?.last4 || '****'
  })
 
// Fetch client token for ZIP validation
fetch('/update-payment-method-intent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ payment_method_id: paymentMethodId })
})
  .then(res => res.json())
  .then(data => {
    Payload(data.token)
 
    const form = new Payload.Form(
      document.getElementById('zip-validation-form')
    )
 
    // Listen for successful ZIP validation
    form.on('updated', (evt) => {
      console.log('ZIP code verified for:', evt.payment_method_id)
      alert('ZIP code verified successfully!')
      // Proceed with payment
      window.location.href = '/checkout/complete'
    })
 
    // Listen for validation failure
    form.on('error', (evt) => {
      console.error('ZIP validation failed:', evt.message)
      alert('ZIP code verification failed. Please check your ZIP code and try again.')
    })
  })

This example:

  1. Display: Shows ZIP code input field
  2. Validation: Verifies ZIP matches billing address on file
  3. Update: Updates stored postal code if validation succeeds

ZIP code revalidation:

  • Reduces fraud by confirming cardholder identity
  • Improves AVS match rates for better authorization
  • Ensures accurate billing address information
  • Required for some high-risk transaction types

Updating Account Holder Name

Allow customers to update the name associated with their payment method.

<!DOCTYPE html>
<html>
<head>
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <h2>Update Account Holder Name</h2>
  <p>Update the name associated with this payment method</p>
 
  <form id="update-holder-form" pl-form="payment_method">
    <!-- Display card info -->
    <div>
      <p>Card ending in <strong id="card-last4">****</strong></p>
    </div>
 
    <!-- Account holder name input -->
    <div>
      <label>Account Holder Name</label>
      <input
        type="text"
        pl-input="account_holder"
        placeholder="John Doe"
        required
      />
      <small>Full name as it appears on the card</small>
    </div>
 
    <button type="submit">Update Name</button>
  </form>
 
  <script src="update-holder-form.js"></script>
</body>
</html>
const paymentMethodId = 'pm_1234567890' // From URL or state
 
// Fetch payment method details
fetch(`/payment-methods/${paymentMethodId}`)
  .then(res => res.json())
  .then(paymentMethod => {
    const last4El = document.getElementById('card-last4')
    if (last4El) last4El.textContent = paymentMethod.card?.last4 || '****'
 
    // Pre-fill the account holder name
    const nameInput = document.querySelector('[pl-input="account_holder"]')
    if (nameInput && paymentMethod.account_holder) {
      nameInput.value = paymentMethod.account_holder
    }
  })
 
// Fetch client token for updating holder name
fetch('/update-payment-method-intent', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ payment_method_id: paymentMethodId })
})
  .then(res => res.json())
  .then(data => {
    Payload(data.token)
 
    const form = new Payload.Form(
      document.getElementById('update-holder-form')
    )
 
    // Listen for successful update
    form.on('updated', (evt) => {
      console.log('Account holder updated:', evt.payment_method_id)
      alert('Account holder name updated successfully!')
      window.location.href = '/account/payment-methods'
    })
 
    // Listen for errors
    form.on('error', (evt) => {
      console.error('Error:', evt.message)
      alert(`Update failed: ${evt.message}`)
    })
  })

This example:

  1. Display: Shows account holder input with current name
  2. Submission: Updates account holder name on payment method

Use cases:

  • Correct typos in customer names
  • Change business name for business cards

Combined Update Form

Create a comprehensive form that allows updating multiple fields simultaneously.

<!DOCTYPE html>
<html>
<head>
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <h2>Update Payment Method Details</h2>
  <p>Update billing information and verify security details</p>
 
  <form id="combined-update-form" pl-form="payment_method">
    <!-- Display card info -->
    <div>
      <p>Card ending in <strong id="card-last4">****</strong></p>
    </div>
 
    <!-- Account holder name -->
    <div>
      <label>Account Holder Name</label>
      <input
        type="text"
        pl-input="account_holder"
        id="holder-name"
        placeholder="John Doe"
        required
      />
    </div>
 
    <!-- Billing address fields -->
    <div>
      <label>Street Address</label>
      <input
        type="text"
        pl-input="billing_address[street_address]"
        id="address-line-1"
        placeholder="123 Main St"
        required
      />
    </div>
 
    <div>
      <label>Apartment, suite, etc. (optional)</label>
      <input
        type="text"
        pl-input="billing_address[unit_number]"
        id="address-line-2"
        placeholder="Apt 4B"
      />
    </div>
 
    <div>
      <label>City</label>
      <input
        type="text"
        pl-input="billing_address[city]"
        id="city"
        placeholder="San Francisco"
        required
      />
    </div>
 
    <div style="display: flex; gap: 1rem;">
      <div style="flex: 1;">
        <label>State</label>
        <input
          type="text"
          pl-input="billing_address[state_province]"
          id="state"
          placeholder="CA"
          required
        />
      </div>
 
      <div style="flex: 1;">
        <label>ZIP Code</label>
        <input
          type="text"
          pl-input="billing_address[postal_code]"
          id="postal-code"
          placeholder="94102"
          required
        />
      </div>
    </div>
 
    <!-- Optional CVV validation for high-value updates -->
    <div>
      <label>CVV (for verification)</label>
      <div pl-input="card_code" placeholder="123"></div>
      <small>Enter CVV to verify card ownership</small>
    </div>
 
    <button type="submit">Update Payment Method</button>
  </form>
 
  <script src="combined-form.js"></script>
</body>
</html>
const paymentMethodId = 'pm_1234567890' // From URL or state
 
// Fetch payment method details
fetch(`/payment-methods/${paymentMethodId}`)
  .then(res => res.json())
  .then(paymentMethod => {
    const last4El = document.getElementById('card-last4')
    if (last4El) last4El.textContent = paymentMethod.card?.last4 || '****'
 
    // Pre-fill form fields
    if (paymentMethod.account_holder) {
      document.getElementById('holder-name').value = paymentMethod.account_holder
    }
 
    const ba = paymentMethod.billing_address
    if (ba) {
      if (ba.address_line_1) document.getElementById('address-line-1').value = ba.address_line_1
      if (ba.address_line_2) document.getElementById('address-line-2').value = ba.address_line_2
      if (ba.city) document.getElementById('city').value = ba.city
      if (ba.state_province) document.getElementById('state').value = ba.state_province
      if (ba.postal_code) document.getElementById('postal-code').value = ba.postal_code
    }
  })
 
// Fetch client token for combined update
fetch('/update-payment-method-intent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ payment_method_id: paymentMethodId })
})
  .then(res => res.json())
  .then(data => {
    Payload(data.token)
 
    const form = new Payload.Form(
      document.getElementById('combined-update-form')
    )
 
    // Listen for successful update
    form.on('updated', (evt) => {
      console.log('CVV validated for:', evt.payment_method_id)
      console.log('Payment method updated:', evt.payment_method_id)
      alert('Payment method updated successfully!')
      window.location.href = '/account/payment-methods'
    })
 
    // Listen for errors
    form.on('error', (evt) => {
      console.error('Error:', evt.message)
      alert(`Update failed: ${evt.message}`)
    })
  })

This example combines:

  • Billing address updates
  • Account holder name changes
  • Optional CVV revalidation
  • ZIP code verification

Combined forms provide better user experience by allowing customers to update all necessary information in one place.


Handling Update Events

Listen for form events to handle successful updates, validation errors, and user interactions.

Success Events

form.on('success', (evt) => {
  console.log('Payment method updated:', evt.payment_method_id)
  alert('Payment method updated/revalidated successfully!')
  window.location.href = '/account/payment-methods'
})

Validation Events

form.on('error', (evt) => {
  console.error('Update failed:', evt.message, evt.error_description)
  alert('Update failed: ' + evt.message)
})
 
form.on('invalid', (evt) => {
  console.log('Invalid:', evt.message)
})

Styling Secure Input Fields


You can style secure input fields to match your form's design. While secure inputs are rendered in isolated iframes for PCI compliance, the SDK provides ways to apply custom styling through CSS classes.

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="styled-update-form.css" />
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <form id="update-form" pl-form="payment_method">
    <div>
      <label>CVV (for revalidation)</label>
      <div pl-input="card_code" placeholder="123"></div>
    </div>
 
    <div>
      <label>Account Holder Name</label>
      <input type="text" pl-input="account_holder" placeholder="John Doe" class="pl-input" />
    </div>
 
    <div>
      <label>Address Line 1</label>
      <input type="text" pl-input="billing_address[street_address]" placeholder="123 Main St" class="pl-input" />
    </div>
 
    <div>
      <label>City</label>
      <input type="text" pl-input="billing_address[city]" placeholder="San Francisco" class="pl-input" />
    </div>
 
    <div style="display: flex; gap: 1rem;">
      <div style="flex: 1;">
        <label>State</label>
        <input type="text" pl-input="billing_address[state_province]" placeholder="CA" class="pl-input" />
      </div>
 
      <div style="flex: 1;">
        <label>ZIP Code</label>
        <input type="text" pl-input="billing_address[postal_code]" placeholder="94102" class="pl-input" />
      </div>
    </div>
 
    <button type="submit">Update Payment Method</button>
  </form>
 
  <script src="styled-update-form.js"></script>
</body>
</html>
// Fetch client token for updating payment method
fetch('/update-payment-method-intent', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ payment_method_id: 'pm_1234567890' })
})
  .then(res => res.json())
  .then(data => {
    // Initialize Payload with the client token
    Payload(data.token)
 
    // Initialize the update form
    const form = new Payload.Form(document.getElementById('update-form'))
 
    // Listen for successful update
    form.on('success', (evt) => {
      console.log('Payment method updated:', evt.payment_method_id)
      alert('Payment method updated successfully!')
      window.location.href = '/account/payment-methods'
    })
 
    // Listen for errors
    form.on('error', (evt) => {
      console.error('Error:', evt.message)
      alert(`Update failed: ${evt.message}`)
    })
  })
/* Extend Payload's default classes */
 
/* Base input styling */
.pl-input {
  font-family: 'Inter', -apple-system, sans-serif;
  font-size: 16px;
  padding: 12px;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  transition: border-color 0.2s;
}
 
/* Secure field specific styling */
.pl-input-sec {
  background-color: #fafafa;
}
 
/* Focused input state */
.pl-input.pl-focus {
  border-color: #3b82f6;
  outline: 2px solid rgba(59, 130, 246, 0.1);
  outline-offset: 0;
}
 
/* Invalid input state */
.pl-input.pl-invalid {
  border-color: #ef4444;
  background-color: #fef2f2;
}
 
/* Form layout */
form[pl-form="payment_method"] {
  max-width: 500px;
  margin: 0 auto;
}
 
form[pl-form="payment_method"] > * {
  margin-bottom: 16px;
}
 
form[pl-form="payment_method"] label {
  display: block;
  margin-bottom: 6px;
  font-weight: 500;
  color: #374151;
}
 
form[pl-form="payment_method"] button[type="submit"] {
  width: 100%;
  padding: 12px;
  background-color: #3b82f6;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s;
}
 
form[pl-form="payment_method"] button[type="submit"]:hover {
  background-color: #2563eb;
}

Payload provides default classes that you can style:

  • pl-input - Base input styling
  • pl-input-sec - Secure field styling
  • pl-focus - Focused input state
  • pl-invalid - Invalid input state

Schema Reference


Configuration options for payment method update intents:

Update Intent Configuration

payment_method_form
object
Configuration for the payment method form intent. Use this type to create a native form for collecting and saving payment method details (cards, bank accounts) without immediately charging. The payment method can then be used for future transactions. Returns a token for rendering with the SDK. Required when type is payment_method_form.
payment_method_template
object
Pre-filled payment method configuration including transfer type, billing address, account holder, currency settings, and account defaults. These values populate the payment method form and determine how the saved payment method can be used. Allows you to set sensible defaults for the payment method being collected.
account_defaults
object
Default transaction types that this payment method will be used for. Controls whether the payment method is the default for paying or funding transactions, and which specific types of transactions it applies to.
funding
enum[string]
Controls which funding transaction types this payment method can be used for. Possible values: "all" (use for all funding transactions), "deposits" (use only for receiving deposits), or "withdraws" (use only for withdrawals). Determines the default usage for receiving funds.
Values: all, deposits, withdraws
paying
enum[string]
Controls which paying transaction type this payment method will be the default for. Possible values: "all" (use for all paying transactions), "payments" (use only for sending payments), or "payouts" (use only for receiving payouts). Determines the default usage for sending funds. If the account default is set for payments, that payment method will be used for any automatic invoice payments.
Values: all, payments, payouts
account_holder
string
Pre-filled name of the account holder for the payment method. For bank accounts, this is the name on the account. For cards, this is the cardholder name. When provided, this value will be used to populate the form.
account_id
string
The ID of the account that will own this payment method. Links the payment method to a specific customer or processing account. Used to associate the payment method with the correct account when it is created.
bank_account
object
Configuration specific to bank account payment methods. Contains bank account-specific settings such as the currency.
currency
enum[string]
The currency for bank account transactions. Must be either "USD" (US Dollars) or "CAD" (Canadian Dollars). Determines the currency for transactions using this bank account.
Values: USD, CAD
billing_address
object
Pre-filled billing address for the payment method. When provided, these values will be used to populate the address fields in the checkout form. Customers may be able to edit these values depending on checkout configuration.
address_line_1
string
Street address of the address
address_line_2
string
Unit number of the address
city
string
City of the company
country_code
string
Country code of the address
postal_code
string
Postal code of the address
state_province
string
State of the address
id
string
The unique identifier of an existing payment method to update. When provided, the form will be used to update the payment method, such as revalidating the CVV, updating the expiration date, or changing the billing address.
transfer_type
enum[string]
The transfer capabilities for this payment method. Possible values: "send_only" (can only send payments/fund payouts), "receive_only" (can only receive payouts/deposits), or "two_way" (can both send and receive). Controls how the payment method can be used for transactions.
Values: send_only, receive_only, two_way

For complete field documentation, see:


Next Steps

Enhance payment processing and payment method management


Process Transactions

Use Payment API to process payments with updated payment methods, set up Recurring Payments for subscriptions with updated methods, and send Payouts to updated bank accounts.

Manage Payment Methods

Update methods programmatically with Payment Method API, implement Payment Method Verification to verify updated payment methods, and explore Payment Methods for overview of management options.

Build Payment Interfaces

Create Payment Method Form for collecting new payment methods, build Payment Form for processing immediate payments, and integrate Checkout Plugin for complete checkout experiences.


Related articles