Skip to main content

Confirm Card Setup With Elements

@peakgateway/web-sdk exposes elements.confirmCardSetup(clientSecret, cardElement) for the current SetupIntent browser flow:

  1. tokenize card details through the hosted Elements iframe
  2. call the public SetupIntent confirm endpoint with the one-time token plus clientSecret
  3. return the SetupIntent response when no redirect challenge is required

Runtime Contract

This helper depends on the public SetupIntent confirm route being available at:

POST /api/v1/public/setup-intents/{id}/confirm
GET /api/v1/public/setup-intents/{id}?clientSecret=...

If the API returns a non-2xx response, the helper fails with SetupIntentApiError and exposes the upstream status and response body.

A confirmable SetupIntent must have been created with an authorized customerId: a saved card is customer-owned, so an intent created with only an external reference is rejected at confirm rather than saving an unusable credential.

Setup intents created by an authenticated merchant through the dashboard, Virtual Terminal, or merchant API are server-classified as MERCHANT_MANAGED, including merchant-issued hosted setup links. The customer does not need an account or consent to save a card. The caller cannot override this classification with ownerType. Customer self-service setup through the customer portal remains CUSTOMER_MANAGED and requires a verified customer account. Both paths enforce the authorized organization; a customer from another organization is not found. An otherwise identical same-key retry of an older customer-managed intent returns that existing intent unchanged, retaining its stored ownership and verified-identity requirements.

When the location's AVS policy applies to a raw card on the intent's DIRECT route, the client-secret read reports billingAddressRequired: true. Send billing.addressLine1 and billing.zip with the confirm. Without them the confirm fails with HTTP 400 BILLING_ADDRESS_REQUIRED before the client secret is consumed or an attempt is counted, so the same clientSecret can retry with the address.

On SUCCEEDED, setupIntent.paymentMethodId is the canonical saved-payment-method handle for the stored card — the same id the saved-payment-method API lists. The response field is paymentMethodId; send its value back unchanged as savedPaymentMethodId on a later charge by that authenticated customer. It is an opaque server-issued handle, never a provider token or an internal vault id: echo it back exactly as returned and do not parse or re-derive it. It is absent when the intent did not establish a card the caller may charge.

When tokenization returns the same binding for the same customer in the same organization, a new consented setup reuses its existing ACTIVE card handle instead of creating a second saved method. The original method's consent and provenance stay unchanged; the new intent retains its own consent and verification evidence. A different customer cannot adopt the handle, and a disabled method is not restored by this path. Saved-method/default changes and intent completion commit together. The replacement endpoint returns that resolved handle, including when the card matches the original saved method; replacing the same binding keeps that method ACTIVE and selects it as the default.

Example

import {
  PeakGateway,
  SetupIntentActionRequiredError,
  SetupIntentApiError,
} from '@peakgateway/web-sdk'

const peak = new PeakGateway({
  sessionId,
  sessionToken,
})

const elements = peak.elements()
const card = elements.create('card')

card.mount('#card-element')

try {
  const setupIntent = await elements.confirmCardSetup(clientSecret, card)

  switch (setupIntent.status) {
    case 'SUCCEEDED':
      console.log('Card saved', setupIntent.paymentMethodId)
      break
    case 'REQUIRES_ACTION':
      // Current helper behavior for non-redirect next_action shapes.
      console.log('Additional action required', setupIntent.nextAction)
      break
    default:
      console.log('SetupIntent returned', setupIntent.status)
  }
} catch (err) {
  if (err instanceof SetupIntentActionRequiredError) {
    // The helper already started the redirect for redirect_to_url actions.
    // On the return page, re-fetch the latest state:
    // const latest = await elements.retrieveSetupIntent(clientSecret)
    return
  }
  if (err instanceof SetupIntentApiError) {
    console.error("Setup failed", err.status)
    return
  }
  throw err
}

Current Browser Boundary

The helper intentionally stops at the browser/runtime contract:

  • redirect_to_url starts a browser redirect and throws SetupIntentActionRequiredError
  • the return page can call elements.retrieveSetupIntent(clientSecret) to inspect the latest state
  • non-redirect nextAction payloads are returned to the caller unchanged
  • processor-driven 3DS callback completion remains outside this browser helper

That deeper callback flow remains outside the browser runtime boundary.