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.
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 — error → token → charged — 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.
}
}
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:
avsResultCode | Meaning |
|---|---|
match | Address and postal code both matched |
partial_postal | Postal code matched, street did not |
partial_address | Street matched, postal code did not |
no_match | Neither matched |
unavailable / not_supported | The issuer/gateway didn't return a result |
cvvResultCode | Meaning |
|---|---|
match | Security code matched |
no_match | Security code did not match |
not_provided / unavailable | No 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 surfacesframe_payment_declined; nothing is captured. - Buyer cancels the challenge →
frame_3ds_challenge_failed. Not charged. - Challenge times out →
frame_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:
| Code | Meaning |
|---|---|
frame_field_validation_failed | A required field (including a supplied billing address) was missing or malformed. Fixed before any charge. |
frame_payment_declined | The card was authorised through 3-D Secure (or no 3DS was required) but the issuer/processor declined the charge. |
frame_3ds_challenge_failed | 3-D Secure did not complete — buyer cancelled or the challenge couldn't finish. Nothing charged. |
frame_3ds_challenge_timeout | The buyer abandoned the 3-D Secure challenge. Nothing charged. |
frame_3ds_required | The 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.
Related
- Custom layout (elements mode) — mounting the discrete card fields
- Saved cards & tokenization — the charge-and-save vs vault-only flows
- 3-D Secure — the full 3DS model and sandbox test cards
- Errors — the complete
frame_*code union