Re-enter a saved card's security code
A saved card has no security code: card rules forbid storing it. When a returning buyer pays with a card you saved, you can ask them to type its security code again, and the charge then carries a code the bank can check. Use it for payments the buyer is present for, on cards you already hold as a vp_pmt_*.
It takes three steps: your server opens a session for the card, your page shows one field, and your server charges.
1. Open a session on your server
Call POST /v1/security_code_sessions with your secret key and the saved card. If the card was saved for a buyer, send that buyer's buyer_id too: a card that is not this buyer's is refused with 404 payment_method_not_found.
curl -X POST https://checkout.vonpay.com/v1/security_code_sessions \
-H "Authorization: Bearer $VON_PAY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"payment_method": { "id": "vp_pmt_test_QAqnXEJF_TCum1jg" },
"buyer_id": "buyer_123"
}'
{
"object": "security_code_session",
"id": "vp_scs_test_8Hq2mZr4Kd0yT1",
"payment_method": { "id": "vp_pmt_test_QAqnXEJF_TCum1jg" },
"card": { "brand": "visa", "last4": "4242" },
"status": "open",
"expires_at": "2026-10-08T15:15:00Z",
"created_at": "2026-10-08T15:00:00Z",
"livemode": false
}
Send the id to the page. The Node and Python SDKs (3.7.0 and later) call this with securityCodeSessions.create() / security_code_sessions.create().
Each session:
- lasts 15 minutes and pays for one charge attempt, whatever its outcome (a
422 security_code_not_collectedrefusal does not use it up); - is for one saved card, and only a card (a saved wallet is refused with
422 capability_not_supported); - counts toward a limit of 10 sessions per saved card per hour (
429 rate_limit_exceeded, withRetry-After). Asking for a saved card's code is a way to guess it, so the limit is tight on purpose.
Not every payment connection supports this: on one that does not, the call returns 422 capability_not_supported.
2. Show the field
The field has its own session, so you do not call sessions.retrieve(). Pass the id, mount the field, and enable your pay button when the code is complete:
const field = vora.securityCode({ session: securityCodeSessionId });
field.on("ready", ({ card }) => {
label.textContent = `Security code for ${card.brand} ending ${card.last4}`;
});
field.on("change", ({ complete }) => {
payButton.disabled = !complete;
});
field.on("error", (err) => showError(err.message));
field.mount("#security-code", { placeholder: "CVC" });
payButton.onclick = async () => {
try {
await field.submit(); // { status: "collected" }
} catch (err) {
// frame_tokenization_failed: the code was not accepted; let the buyer correct it
return showError(err.message);
}
const res = await fetch("/pay", { method: "POST" });
const body = await res.json();
if (body.redirect) vora.redirectToChallenge(body.redirect); // 3-D Secure
};
mount() takes the same style schema as the card security-code field, and a placeholder. The code goes from the field straight to the payment provider: it never reaches your page's JavaScript, your servers or ours, and change only tells you whether it is complete.
One field per page. The field cannot share a page with a card form. It refuses to start when a card form is already mounted, and card fields started after it disconnect it. Both report frame_card_form_conflict. Put the security-code step on its own page or step.
3. Charge on your server
After submit() resolves, charge the same saved card with POST /v1/payment_intents, sending payment_method and security_code_session together, and a return_url for 3-D Secure:
curl -X POST https://checkout.vonpay.com/v1/payment_intents \
-H "Authorization: Bearer $VON_PAY_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ord_42_create" \
-d '{
"amount": 1499,
"currency": "USD",
"payment_method": { "id": "vp_pmt_test_QAqnXEJF_TCum1jg" },
"security_code_session": "vp_scs_test_8Hq2mZr4Kd0yT1",
"buyer_id": "buyer_123",
"return_url": "https://your-store.example.com/checkout/return"
}'
The result is an ordinary payment intent. If the bank asks for 3-D Secure, it comes back requires_action with next_action.redirect_to_url: return that to the page and send the buyer with vora.redirectToChallenge(), as on any saved-card charge. The check cannot run inside the page on this flow.
security_code_session is for a buyer who is present, so it cannot be combined with mit, network_token, payment_method_type or gateway.
After the charge
A charge attempt uses up the session, whether it was paid, declined or failed. To try again, open a new session and create a new field. The exception is 422 security_code_not_collected: the charge arrived before the buyer submitted the code, so the session is still usable. Send the same request again once submit() resolves. Retrying the same request with the same Idempotency-Key still returns the original outcome.
Errors
From your server's charge (nothing is charged on any of these):
| Code | When | What to do |
|---|---|---|
security_code_not_collected | 422. The buyer has not submitted the code yet, or submitted it for a different saved card. The session is not used up. | Wait for submit() to resolve, then send the same request again. |
security_code_session_expired | 410. The session was already used, or is more than 15 minutes old. | Open a new session and ask the buyer again. |
security_code_session_not_found | 404. Unknown, another account's or mode's, or opened for a different payment_method. | Send the session opened for this card, with the same key mode. |
From the field:
| Code | When | What to do |
|---|---|---|
frame_tokenization_failed | The code was not accepted, or no answer came back in 30 seconds. | Let the buyer correct it and submit again. |
frame_session_expired | The session was used or expired, or a code was already collected. | Open a new session and create a new field. |
frame_session_not_found | The session id is unknown, or its mode does not match your publishable key. | Pass the id your server received. |
frame_card_form_conflict | A card form is on the same page. | Show the field on a page or step without a card form. |
Related
- Tokenization & saved cards: how a card becomes a
vp_pmt_* - 3-D Secure: the redirect and the return
- Payment intents: charging a saved card