Confirm Card Setup With Elements
@peakgateway/web-sdk exposes elements.confirmCardSetup(clientSecret, cardElement) for the current SetupIntent browser flow:
- tokenize card details through the hosted Elements iframe
- call the public SetupIntent confirm endpoint with the one-time token plus
clientSecret - return the
SetupIntentresponse 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_urlstarts a browser redirect and throwsSetupIntentActionRequiredError- the return page can call
elements.retrieveSetupIntent(clientSecret)to inspect the latest state - non-redirect
nextActionpayloads 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.