Skip to main content

Quickstart

Get a working checkout integration in 5 minutes.

This is the Checkout quickstart — the simplest path. For other integration paths see Choose your integration, or jump straight to the Embedded Fields quickstart if you want buyers to stay on your domain.

Step 0: Get your test keys​

A test sandbox belongs to a business: start your application first (no approval needed), then in the developer dashboard click Activate Evaluation Sandbox. It shows your test keys once, when the sandbox is created (full steps: Sandbox & test mode):

  • vp_sk_test_* — secret API key (server-only)
  • vp_pk_test_* — publishable key

Your webhook signing secret (whsec_*) is separate again — one is issued per webhook endpoint when you create it.

Test keys never move real money; which totals produce which outcome is on Test mode. A test request refused with sandbox_account_required means the key is not on a sandbox with its processor test account: a test key on a live account, or a sandbox whose test account is not set up yet. Live keys (vp_sk_live_*) follow merchant approval — Go-Live Checklist.

Other prerequisites​

  • Node.js 20+ (or Python 3.9+ for the Python SDK)

Step 1: Install the SDK​

Node.js​

npm install @vonpay/checkout-node

Python​

pip install vonpay-checkout

CLI​

npm install -g @vonpay/checkout-cli
vonpay checkout login

Pin the major you tested against (npm, PyPI).


Step 2: Create a checkout session​

When to create it: mint the session when the buyer clicks Checkout — not on every pageview. It's a server-to-server call with a 30-minute TTL, so creating it earlier just risks it expiring unpaid. See When to create the session.

Node.js​

import { VonPayCheckout } from "@vonpay/checkout-node";

const apiKey = process.env.VON_PAY_SECRET_KEY;
if (!apiKey) throw new Error("VON_PAY_SECRET_KEY is required");

const vonpay = new VonPayCheckout(apiKey);

const session = await vonpay.sessions.create({
amount: 1499, // $14.99 in cents
currency: "USD",
successUrl: "https://mystore.com/order/123/confirm",
cancelUrl: "https://mystore.com/cart",
lineItems: [
{ name: "Premium Widget", quantity: 1, unitAmount: 1499 },
],
buyerId: "user_8f3a2b", // your STABLE per-user ID — same value every visit, never randomize
buyerName: "Jane Doe",
buyerEmail: "jane@example.com", // enables cross-session buyer dedup
});

Python​

from vonpay.checkout import VonPayCheckout, LineItem

vonpay = VonPayCheckout(os.environ["VON_PAY_SECRET_KEY"])

session = vonpay.sessions.create(
amount=1499,
currency="USD",
success_url="https://mystore.com/order/123/confirm",
cancel_url="https://mystore.com/cart",
line_items=[
LineItem(name="Premium Widget", quantity=1, unit_amount=1499),
],
buyer_name="Jane Doe",
buyer_email="jane@example.com",
)

CLI​

vonpay checkout sessions create --amount 1499 --currency USD

cURL​

curl -X POST https://checkout.vonpay.com/v1/sessions \
-H "Authorization: Bearer vp_sk_test_xxx" \
-H "Content-Type: application/json" \
-H "Von-Pay-Version: 2026-04-14" \
-H "Idempotency-Key: order_123_create" \
-d '{
"amount": 1499,
"currency": "USD",
"successUrl": "https://mystore.com/order/123/confirm",
"lineItems": [{"name": "Premium Widget", "quantity": 1, "unitAmount": 1499}]
}'

Response:

{
"id": "vp_cs_test_k7x9m2n4p3",
"checkoutUrl": "https://checkout.vonpay.com/checkout?session=vp_cs_test_k7x9m2n4p3",
"expiresAt": "2026-03-31T15:30:00.000Z"
}

Step 3: Redirect the buyer​

Send the buyer to the checkout URL returned in the session response.

Node.js (Express)​

res.redirect(session.checkoutUrl);

Python (Flask)​

return redirect(session.checkout_url)

The buyer sees the Von Payments hosted checkout page with billing address, payment methods (cards, Apple Pay, Google Pay, Klarna, etc.), and your order summary.


Step 4: Handle the webhook​

When a payment event occurs (charge succeeds, fails, or is refunded), Von Payments sends a signed POST to each registered webhook endpoint that subscribes to that event type. Register an endpoint at /dashboard/developers/webhooks → Webhooks; mint a whsec_* signing secret at create time and store it in your handler env.

Node.js (Express)​

import { VonPayCheckout } from "@vonpay/checkout-node";

const apiKey = process.env.VON_PAY_SECRET_KEY;
if (!apiKey) throw new Error("VON_PAY_SECRET_KEY is required");

const vonpay = new VonPayCheckout(apiKey);
const endpointSecret = process.env.VON_PAY_WEBHOOK_SECRET; // whsec_*

app.post("/webhooks/vonpay", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = vonpay.webhooks.constructEvent(
req.body, // raw body (Buffer)
req.headers["x-vonpay-signature"] as string, // signature header
endpointSecret, // whsec_* — per-endpoint secret
);

// A "Send test event" delivery can carry a real session's ids: acknowledge it and stop.
if (event.test_event === true) {
res.status(200).json({ received: true });
return;
}

switch (event.type) {
case "charge.succeeded":
console.log(`Payment succeeded: ${event.data.payment_intent_id}`);
// fulfill the order, keyed on payment_intent_id — transaction_id is the
// card processor's reference, not a Von id (see Webhook events)
break;
case "charge.failed":
console.log(`Payment failed: ${event.data.failure_reason}`);
break;
}

res.status(200).json({ received: true });
} catch (err) {
console.error("Webhook verification failed:", err.message);
res.status(400).json({ error: "Invalid signature" });
}
});

Python (Flask)​

from vonpay.checkout import VonPayCheckout

vonpay = VonPayCheckout(os.environ["VON_PAY_SECRET_KEY"])
endpoint_secret = os.environ["VON_PAY_WEBHOOK_SECRET"] # whsec_*

@app.route("/webhooks/vonpay", methods=["POST"])
def webhook():
try:
event = vonpay.webhooks.construct_event(
request.data,
request.headers.get("x-vonpay-signature"),
endpoint_secret,
)

# A "Send test event" delivery can carry a real session's ids: acknowledge it and stop.
if event.test_event:
return {"received": True}, 200

if event.type == "charge.succeeded":
print(f"Payment succeeded: {event.data['payment_intent_id']}") # not transaction_id: that is the processor's reference

return {"received": True}, 200
except Exception as e:
return {"error": str(e)}, 400

Key details:

  • The webhook secret is per-endpoint (whsec_*), minted when you register the endpoint. Each endpoint has its own. Not the same as your API key.
  • The x-vonpay-signature header carries t=<unix_seconds>,v1=<hex_hmac> — one v1= entry, no grace window; accept both secrets while you rotate (how).
  • Replay window is asymmetric: reject if more than 5 min in the past or 30 sec in the future.
  • Register express.raw() for this route before any app-wide express.json(). constructEvent verifies the signature against the raw request body; if express.json() parses it first, verification fails. Scope the raw parser to the webhook path only (as shown above), not globally.
  • Testing this handler: vonpay checkout trigger charge.succeeded --url http://localhost:3000/webhooks/vonpay fires a signed synthetic event at it — every charge.* and payment_intent.* event, refund.failed and session.* can be triggered (CLI reference). A test event is signed with your API key, not the endpoint's whsec_* secret.
  • See Webhooks for the full surface walkthrough and Webhook Verification for the algorithm + reference verifiers in 5 languages.

Step 5: Confirm the payment when the buyer returns​

After payment, the buyer is redirected to your successUrl with the session ID:

https://mystore.com/order/123/confirm?session=vp_cs_test_k7x9m2n4p3

That redirect means the buyer came back. It does not mean they paid. Read the outcome from the API with your secret key:

import { VonPayCheckout } from "@vonpay/checkout-node";

const client = new VonPayCheckout(process.env.VON_PAY_SECRET_KEY);

const sessionId = new URL(req.url, `https://${req.headers.host}`)
.searchParams.get("session");

// A buyer can legitimately land here with no `session` parameter — a bookmarked
// confirmation URL, or a 3-D Secure return to a `successUrl` you set without one.
// That is a broken link, NEVER a payment outcome. Do not throw: an error page
// here is exactly what makes a buyer who already paid pay a second time.
if (!sessionId) {
// Show a neutral "we're confirming your order" page and let the webhook fulfil.
return renderConfirming();
}

const { status } = await client.sessions.get(sessionId);

if (status === "succeeded") {
// Show the confirmation page
} else if (status === "pending" || status === "processing") {
// Still in flight — show "confirming your payment", NOT a failure
} else {
// failed or expired — offer to try again
}
Do not fulfil the order here

A buyer who pays and then closes their laptop never loads this page. Fulfilment belongs on the charge.succeeded webhook, which reaches you regardless of what the browser does. This page is for showing the buyer something immediately.

And treat pending / processing as "still working", never as a failure — on the 3-D Secure path buyers routinely arrive here before the payment settles, and a failure page makes them pay twice.

See Handle the return for the full pattern, including fulfilling exactly once.


Step 6: Go live​

Replace your test keys with live keys. That's it.

  1. Swap vp_sk_test_xxx for vp_sk_live_xxx
  2. Point your webhook subscription at your production endpoint and use its whsec_* secret
  3. Ensure successUrl uses HTTPS
  4. Test with a small real payment

Next steps​