Payment Intents quickstart
Server-side payment lifecycle in 5 steps. Use this path when you need explicit control over auth, capture, void, and refund — for example delayed capture (auth on order, capture on ship), subscriptions, or platform-integrator flows.
Comparing paths first? See Choose your integration. For the deep reference (every field, error code, capability gate, MIT details), see the Payment Intents reference.
Step 0: Get your test keys
Same path as the Checkout quickstart Step 0 — start your application at app.vonpay.com/merchant/apply (a test sandbox belongs to a business; no approval needed), click Activate Evaluation Sandbox in Developer Tools, and grab vp_sk_test_* (secret) and vp_pk_test_* (publishable). The server-side calls below use the secret key.
Step 1: Tokenize a card
Payment Intents charges a payment-method token, never raw card data. The standard production path pairs Payment Intents with Embedded Fields — the buyer's card enters our iframe, you get back a vp_pmt_* token (reusability model).
The "Embedded Fields tokenizes → Payment Intents charges" pairing assumes the embed only mints a vp_pmt_* token and moves no money on submit. Under the embedded charge-and-save flow the embed already charges on submit (and, with a buyer on the session, also vaults a reusable vp_pmt_* in the same step). On that flow you must not also create a Payment Intent for the same session — doing so charges the buyer twice.
Confirm which embed behavior your account uses before pairing with Payment Intents. If your embed charges on submit, settle on the embed result alone and skip Step 2 below for that session.
For this quickstart, run the Embedded Fields quickstart with your test keys up to the submit step, and enter one of the sandbox test cards, such as 4111 1111 1110 1203. On that tokenize-only session, result.token is the vp_pmt_test_* you charge below. There is no way to make a token without a card entering the iframe, in test mode or live. If you already collect cards server-side through your provider, bind the provider's handle instead: Getting a payment-method token.
Step 2: Create a Payment Intent
Use the vp_pmt_* from Step 1 as the payment method. capture_method: "automatic" does auth + capture in one call:
Node
import { VonPayCheckout } from "@vonpay/checkout-node";
const vonpay = new VonPayCheckout(process.env.VON_PAY_SECRET_KEY);
const intent = await vonpay.paymentIntents.create(
{
amount: 1499,
currency: "USD",
captureMethod: "automatic",
paymentMethod: { id: "vp_pmt_test_QAqnXEJF_TCum1jg" },
returnUrl: "https://mystore.com/checkout/return?order=ord_42", // where the bank returns the buyer; your order ref tells the page which order
metadata: { orderId: "ord_42" },
},
{ idempotencyKey: "ord_42_create" }
);
// intent.status: "succeeded" | "requires_action" | "failed"
Python
from vonpay.checkout import PaymentMethodRef
intent = vonpay.payment_intents.create(
amount=1499,
currency="USD",
capture_method="automatic",
payment_method=PaymentMethodRef(id="vp_pmt_test_QAqnXEJF_TCum1jg"),
return_url="https://mystore.com/checkout/return?order=ord_42", # where the bank returns the buyer; your order ref tells the page which order
metadata={"order_id": "ord_42"},
idempotency_key="ord_42_create",
)
cURL
curl -X POST https://checkout.vonpay.com/v1/payment_intents \
-H "Authorization: Bearer vp_sk_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ord_42_create" \
-d '{
"amount": 1499,
"currency": "USD",
"capture_method": "automatic",
"payment_method": { "id": "vp_pmt_test_QAqnXEJF_TCum1jg" }
}'
Use capture_method: "manual" if you want to authorize now and capture later — the intent will stop at authorized and wait for an explicit /capture call.
Step 3: Handle requires_action (3DS challenge)
You must send return_url in Step 2 to get here
A challenge is issued only if the create call included return_url; without it a card that needs authentication is rejected — 422 provider_request_rejected with missing_redirect_url — and the failure reads like a card problem.
{
"amount": 1499,
"currency": "USD",
"payment_method": { "id": "vp_pmt_test_QAqnXEJF_TCum1jg" },
"return_url": "https://mystore.com/checkout/return?order=ord_42"
}
Absolute HTTPS URL (localhost is accepted for local development). It is ignored when no challenge is needed, so send it on every buyer-present charge. Full detail: Authentication challenges (3DS).
If the buyer's bank requires Strong Customer Authentication, the intent returns status: "requires_action" with a next_action block:
if (intent.status === "requires_action" && intent.nextAction?.type === "redirect_to_url") {
// Top-level navigation — NOT inside an iframe (banks block this).
res.redirect(intent.nextAction.redirectToUrl.url);
}
The buyer completes the challenge on their bank's page. You learn the final status via webhook (payment_intent.succeeded / payment_intent.failed) — don't trust the buyer's browser. See the reference for full 3DS handling.
Step 4: Capture, void, or refund
For auth-only intents, capture later (empty body captures the full authorized amount):
curl -X POST https://checkout.vonpay.com/v1/payment_intents/vpi_test_abc123/capture \
-H "Authorization: Bearer vp_sk_test_xxx" \
-H "Idempotency-Key: ord_42_capture" \
-d '{}'
Refund (omit amount for full refund):
curl -X POST https://checkout.vonpay.com/v1/refunds \
-H "Idempotency-Key: order_1042_refund_1" \
-H "Authorization: Bearer vp_sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{ "payment_intent": "vpi_test_abc123", "reason": "requested_by_customer" }'
Void (only valid before capture):
curl -X POST https://checkout.vonpay.com/v1/payment_intents/vpi_test_abc123/void \
-H "Authorization: Bearer vp_sk_test_xxx" \
-H "Idempotency-Key: ord_42_void" \
-d '{}'
Once an intent is captured, reverse it with a refund rather than a void — see reversing a captured intent so you can branch up front rather than catch the error mid-flow.
Step 5: Rebill a saved card (MIT)
Charge a card on file — subscription renewals, retries, scheduled installments — by passing an mit block. You store the token and run the schedule; Vonpay relays each charge.
Requires the token to have been minted with setup_for_future_use: "off_session" and the merchant's processor to have capabilities.supported_operations.mit === true.
curl -X POST https://checkout.vonpay.com/v1/payment_intents \
-H "Authorization: Bearer vp_sk_test_xxx" \
-H "Idempotency-Key: sub_8821_cyc_2026_05" \
-H "Content-Type: application/json" \
-d '{
"amount": 2999,
"currency": "USD",
"capture_method": "automatic",
"payment_method": { "id": "vp_pmt_test_R6mzKBh3_Ud8nGgf" },
"mit": {
"initiator": "merchant",
"reason": "recurring",
"original_transaction_id": "vpi_test_1stconsentintent01"
}
}'
original_transaction_id is the first cardholder-initiated intent in the chain (where consent was captured). See the Payment Intents reference — MIT for chain-validity rules and the full mit field table.
Step 6: Go live
Same swap as the Checkout path: vp_sk_test_* → vp_sk_live_*. Direct-charge with payment_method may need a per-processor activation gate on live keys — if your live calls return endpoint_not_implemented, contact your VORA point of contact to enable it for your merchant.
Next steps
- Payment Intents reference — full lifecycle reference, error envelopes, MIT block (recurring + retries), capabilities matrix
- Embedded Fields quickstart — the standard browser-side tokenization pairing
- Webhooks —
payment_intent.*event handling - Error codes — canonical error catalog