Skip to main content

Charge at submit — CVV, AVS & 3-D Secure at the first charge

Charge at submit is a variant of the charge-and-save flow for elements mode. On the buyer's first charge, one collection.submit() does four things in a single call:

  • charges the card for the session amount,
  • verifies the CVV the buyer just typed (returns a cvvResultCode),
  • runs Address Verification against the billing address (returns an avsResultCode), and
  • completes 3-D Secure in-page when it's required — the buyer never leaves your page —
  • then vaults the card as a reusable vp_pmt_* (when a buyer is on the session).

Why a dedicated flow: the default vault-then-charge path stores the card first and charges the stored token later. A stored token has no CVV — the security code can never be stored — so vault-then-charge can't return a CVV result. Charging at submit keeps the buyer-present CVV in the same request that authorises the card, so you get the CVV and AVS signals on the transaction that actually moves the money.

Availability

Charge at submit is enabled per session and, for CVV recovery, per account. 3-D Secure and AVS activate at the account level with your gateway. If a session wasn't created for charge-at-submit, submit() uses the standard vault-then-charge path and returns no cvvResultCode / chargeStatus. Ask your Vonpay contact to confirm it's enabled for your account before you rely on it in production.

Enable it on the session

Charge at submit is a session-level opt-in. Create the session server-side (with your secret key) using integrationMode: "elements" and chargeAtSubmit: true:

curl -X POST https://checkout.vonpay.com/v1/sessions \
-H "Authorization: Bearer $VON_PAY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 4999,
"currency": "USD",
"buyerId": "buyer_123",
"integrationMode": "elements",
"chargeAtSubmit": true
}'

Return the session id and publishableKey to the browser, then mount the discrete card fields and submit exactly as in custom layout (elements mode). Nothing changes about how you mount the fields — the session flag alone switches submit() to the charge path.

A session created with mode: "setup" (vault-only) or without chargeAtSubmit: true will not charge at submit; submit() vaults the card and returns a token instead.

Read the result

Charge at submit adds four fields to SubmitResult:

interface SubmitResult {
// ── standard fields ──
token?: string; // vp_pmt_* — a reusable method was vaulted (buyer on session)
charged?: true; // the card was charged
last4?: string; // display-only
brand?: string; // "visa" | "mastercard" | … (display-only)
error?: VoraMirrorError; // present iff submit failed

// ── charge-at-submit fields ──
chargeStatus?: "succeeded" | "pending"; // was the money captured, or still settling?
avsResultCode?: AvsResultCode; // address verification outcome (nullable)
cvvResultCode?: CvvResultCode; // security-code outcome (nullable)
paymentIntentId?: string; // reconcile against the charge.* webhook
}

Discriminate in the same order as every other submit — errortokencharged — then read chargeStatus:

const result = await collection.submit();

if (result.error) {
// Nothing was charged. Surface result.error.message (see "Errors" below).
} else {
// The card was charged. A buyer on the session also gets result.token (vp_pmt_*);
// a guest gets result.charged with no token.
if (result.chargeStatus === "succeeded") {
// Money captured. result.avsResultCode / result.cvvResultCode carry the verification.
} else if (result.chargeStatus === "pending") {
// Authorised but not yet settled — do NOT fulfil yet. Wait for the charge webhook,
// keyed on result.paymentIntentId.
}
}
Submit already charged — never charge the session twice

When charge at submit is on, submit() has already charged the card. Do not also call POST /v1/payment_intents for the same session — that double-charges the buyer. The client result is a UX signal; confirm settlement server-side via the charge webhook before fulfilling, and treat chargeStatus: "pending" as not yet settled.

Billing address for AVS

AVS only runs when the charge carries a billing address. You can supply it two ways:

Mount an address element — the address element collects the billing address in-form and forwards it automatically:

const address = collection.create("address", { mode: "billing" });
address.mount("#billing-address");
// …mount card fields…
const result = await collection.submit(); // the address rides along

Or pass it from your own form — supply a billingAddress to submit() and skip the element entirely:

const result = await collection.submit({
billingAddress: {
line1: "1600 Pennsylvania Ave NW",
city: "Washington",
state: "DC",
postalCode: "20500",
country: "US", // 2-letter ISO 3166-1 code
},
});

line1, postalCode and country are required and validated before any network call (a missing or malformed field returns frame_field_validation_failed, and nothing is charged). An explicit submit({ billingAddress }) takes precedence over a mounted address element. Unlike the card security code, the billing address is ordinary data — it may come from your own form.

The result codes are gateway-independent:

avsResultCodeMeaning
matchAddress and postal code both matched
partial_postalPostal code matched, street did not
partial_addressStreet matched, postal code did not
no_matchNeither matched
unavailable / not_supportedThe issuer/gateway didn't return a result
cvvResultCodeMeaning
matchSecurity code matched
no_matchSecurity code did not match
not_provided / unavailableNo result returned

Both are nullable. You own the decision — a no_match is a signal, not an automatic decline; decide whether to fulfil, review, or refund based on your own risk policy.

3-D Secure

When 3-D Secure is enabled for your account, submit() handles it inline and automatically — there's nothing extra to call. If the buyer's card requires a step-up challenge, Vonpay renders it in a modal on your page (the buyer never leaves your checkout), completes authentication, and then charges the authenticated session:

  • Authenticated / attempted / issuer-declined (Y / A / N / R) → the charge proceeds and the processor settles or declines it, with the authentication attached. A declined card surfaces frame_payment_declined; nothing is captured.
  • Buyer cancels the challengeframe_3ds_challenge_failed. Not charged.
  • Challenge times outframe_3ds_challenge_timeout. Not charged.
  • The challenge can't complete (a technical or ambiguous result) → frame_3ds_challenge_failed. Not charged.

The card is never charged unless the challenge produced a real authentication outcome — an incomplete, cancelled, or ambiguous challenge always fails closed. The authentication result stays internal to Vonpay; it isn't surfaced on SubmitResult.

If 3-D Secure isn't enabled for the account, a card that requires it surfaces the interim frame_3ds_required instead of charging — treat that as "have the buyer use another card." See the 3-D Secure guide for the full model, sandbox test cards, and the server-driven confirm path.

Errors

All failures come back on result.error — the card is not charged when error is set. The codes you'll handle on this path:

CodeMeaning
frame_field_validation_failedA required field (including a supplied billing address) was missing or malformed. Fixed before any charge.
frame_payment_declinedThe card was authorised through 3-D Secure (or no 3DS was required) but the issuer/processor declined the charge.
frame_3ds_challenge_failed3-D Secure did not complete — buyer cancelled or the challenge couldn't finish. Nothing charged.
frame_3ds_challenge_timeoutThe buyer abandoned the 3-D Secure challenge. Nothing charged.
frame_3ds_requiredThe card needs 3-D Secure but it isn't enabled for the account. Ask the buyer to use another card.

See the full error reference for the complete code union and handling guidance.