@vonpay/checkout-node — Node.js SDK
Typed TypeScript/JavaScript client for the Von Payments Checkout API. Zero runtime dependencies, ESM-only, Node 20+.
Install
npm install @vonpay/checkout-node
Pinning to an exact version is recommended during the pre-1.0 window — minor bumps may add options or change defaults.
Initialize
import { VonPayCheckout, VonPayError } from "@vonpay/checkout-node";
// Simple — pass API key as a string
const vonpay = new VonPayCheckout("vp_sk_live_xxx");
// With options — pass a config object
const vonpay = new VonPayCheckout({
apiKey: "vp_sk_live_xxx",
apiVersion: "2026-04-14",
baseUrl: "https://checkout.vonpay.com", // default
maxRetries: 2, // default
timeout: 30_000, // ms, default
errorReporter: (err, ctx) => { // optional — see Error reporting below
Sentry.captureException(err, { extra: ctx });
},
});
The constructor validates the key prefix. A key must start with one of vp_sk_test_, vp_sk_live_, vp_pk_test_, or vp_pk_live_; passing a key that matches none of these throws immediately. (Most server-side calls require a secret vp_sk_* key — see sessions.get below.)
vonpay.sessions.create(params, options?)
Create a checkout session and get a checkout URL.
const session = await vonpay.sessions.create({
amount: 1499, // in cents — required and >= 1 for mode "payment"
currency: "USD", // required, 3-letter code (uppercased)
successUrl: "https://mystore.com/order/123/confirm", // optional (HTTPS)
cancelUrl: "https://mystore.com/cart", // optional
country: "US", // optional, ISO 3166-1 alpha-2
mode: "payment", // optional, default "payment"
description: "Order #123", // optional
locale: "en", // optional
expiresIn: 1800, // optional, seconds (300–604800, max 7 days)
buyerId: "cust_abc", // optional — your STABLE per-user ID (not per-visit)
buyerName: "Jane Doe", // optional
buyerEmail: "jane@example.com", // optional
lineItems: [ // optional
{ name: "Widget", quantity: 1, unitAmount: 1499 },
],
metadata: { orderId: "order_123" }, // optional
}, {
idempotencyKey: "order_123_attempt_1", // optional request option
});
// session.id => "vp_cs_live_k7x9m2n4p3"
// session.checkoutUrl => "https://acme.vonpay.com/checkout?session=..."
// session.expiresAt => "2026-03-31T15:30:00.000Z"
amount is in minor units (cents) — 1499 is $14.99. It is required and must be >= 1 for the default mode: "payment". currency is required and normalised to a 3-letter uppercase code. successUrl is optional; when present it must be a valid HTTPS URL. idempotencyKey is passed as the second-argument request option and sent as the Idempotency-Key header.
The live checkout URL is hosted on your merchant subdomain (for example https://acme.vonpay.com/checkout?session=...). expiresAt is an ISO-8601 timestamp string.
See Create a Session for the full parameter reference.
Embedded charge-and-save
If the buyer completes payment through the embedded checkout (Embedded Fields) rather than the hosted page, the buyer fields above are what wire the session to vaulting:
- A buyer reference on the session — a
buyerIdor abuyerEmail(withbuyerNameto enrich the record) — is required to vault a reusablevp_pmt_*token via the embed. A guest session with no buyer reference at all charges once and saves nothing. - Under charge-and-save the embed charges on submit — so the server must not also call
paymentIntents.createfor that same session, or the buyer is charged twice. - The embed's client-side result is a UX signal only. Confirm settlement from the
session.succeededwebhook before fulfilling.
See Charge and save for the full embedded flow and the { token } | { charged: true } | { error } result shape.
vonpay.sessions.get(sessionId)
Retrieve the full status of a session. Requires a secret key (vp_sk_*); a publishable key is rejected with HTTP 403 (auth_key_type_forbidden).
const status = await vonpay.sessions.get("vp_cs_live_k7x9m2n4p3");
// status.id => "vp_cs_live_k7x9m2n4p3"
// status.status => "succeeded"
// status.transactionId => "txn_abc123"
// status.amount => 1499
// status.currency => "USD"
Returns a full SessionStatus object including payment details and metadata. status.status is typed SessionState in the SDK, whose members are pending, succeeded, failed, and expired. The server can also send processing over the wire (a transient state during authorization); the SDK SessionState type doesn't include it, so handle it as a non-terminal/default case rather than in an exhaustive switch over SessionState.
vonpay.sessions.validate(params)
Dry-run validation of session parameters without creating a session (maps to POST /v1/sessions?dry_run=true). Returns validation results.
const result = await vonpay.sessions.validate({
amount: 1499,
currency: "USD",
successUrl: "https://mystore.com/confirm",
});
// result.valid => true
// result.warnings => ["cancelUrl is recommended for production"]
vonpay.webhooks.verifySignature(payload, signature, secret)
Verify an incoming webhook's HMAC-SHA256 signature. Uses crypto.timingSafeEqual to prevent timing attacks. Returns boolean; never throws. Prefer constructEvent for typed event parsing.
const isValid = vonpay.webhooks.verifySignature(
req.body, // raw request body (Buffer or string)
req.headers["x-vonpay-signature"] as string, // signature header (t=…,v1=…)
process.env.VON_PAY_WEBHOOK_SECRET, // whsec_* — per-endpoint secret
);
vonpay.webhooks.constructEvent(payload, signature, secret)
Verify the signature, enforce the asymmetric replay window (5 min past / 30 sec future), and parse the webhook payload into a typed event. Takes 3 arguments. On a malformed header, a stale timestamp, or an HMAC mismatch it throws a VonPayError with code webhook_invalid_signature and HTTP status 401.
import express from "express";
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 (t=…,v1=…)
endpointSecret, // whsec_* — per-endpoint secret
);
switch (event.type) {
case "charge.succeeded":
console.log(`Paid: ${event.data.transaction_id}`);
break;
case "charge.failed":
console.log(`Failed: ${event.data.failure_reason}`);
break;
case "charge.refunded":
console.log(`Refund: ${event.data.amount}`);
break;
default:
// Unknown event types: log and fall through. constructEvent still
// returns a well-formed envelope for an event type added server-side
// after this SDK version, so new types may arrive without an SDK bump.
console.log(`Unhandled event: ${event.type}`);
}
// Your handler returns the HTTP 200 ack — constructEvent only parses the
// event; it does not send an HTTP response.
res.status(200).json({ received: true });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
The webhook secret is per-endpoint (whsec_*), minted when you register the endpoint at /dashboard/developers/webhooks. The signature header is named x-vonpay-signature with the format t=<unix-seconds>,v1=<hex-hmac>. See Webhook Signing Secrets for the create / rotate / revoke lifecycle.
VonPayCheckout.verifyReturnSignature(params, secret, options?)
Static method. Verify the HMAC signature on a return URL redirect after the buyer completes checkout. Auto-detects v1 (legacy) and v2 (current) signature formats.
import { VonPayCheckout } from "@vonpay/checkout-node";
const url = new URL(req.url, `https://${req.headers.host}`);
const isValid = VonPayCheckout.verifyReturnSignature(
{
session: url.searchParams.get("session"),
status: url.searchParams.get("status"),
amount: url.searchParams.get("amount"),
currency: url.searchParams.get("currency"),
transaction_id: url.searchParams.get("transaction_id"),
sig: url.searchParams.get("sig"),
},
process.env.VON_PAY_SESSION_SECRET, // your session signing secret, NOT the API key
{
expectedSuccessUrl: "https://mystore.com/order/123/confirm",
expectedKeyMode: "live", // "live" or "test"
maxAgeSeconds: 600, // optional, default 600
},
);
The secret is your session signing secret, not the API key. In the dashboard this secret is provisioned with an ss_test_* / ss_live_* prefix — copy it verbatim from /dashboard/developers/api-keys. Verification uses crypto.timingSafeEqual to prevent timing attacks.
Options bag (v2 signatures)
expectedSuccessUrl and expectedKeyMode are required when the incoming sig starts with v2.. Passing them for v1 signatures is harmless — they're ignored.
| Option | Required for v2? | Default | Purpose |
|---|---|---|---|
expectedSuccessUrl | Yes | — | The successUrl you passed to sessions.create. Normalised (trailing slash stripped, query sorted, fragment dropped). |
expectedKeyMode | Yes | — | "test" or "live". Prevents test-mode sigs from being accepted as live. |
maxAgeSeconds | No | 600 | Maximum age of the signature in seconds. |
rejectV1 | No | — | Refuse legacy v1 signatures outright. |
See Handle the Return for a full walkthrough of the v2 format and the rationale.
vonpay.health()
Check API health and latency (maps to GET /api/health).
const health = await vonpay.health();
// health.status => "ok" // "ok" | "degraded" | "down"
// health.latencyMs => 42
Error reporting
The SDK accepts an optional errorReporter callback in the constructor so integrators can pipe SDK failures into their own observability stack (Sentry, Datadog, custom logger). Your reporter is invoked synchronously, fire-and-forget; if it throws, the SDK swallows the throw with a console.warn and continues.
A separate, opt-in SDK-telemetry channel can POST anonymised error events back to Von Payments to improve the SDK. It is enabled by default for test keys (vp_sk_test_*) and off for live keys unless you explicitly enable it; it never carries request bodies or PII. The errorReporter callback above is independent of this and runs whether or not telemetry is enabled.
When it fires
- API request failures: a non-retryable response (any status not in the retry set — 4xx including
401/403/404/422, plus non-retryable 5xx like501); retry-exhaustion on a retryable status (429,500,502,503,504); and network/timeout errors after retry exhaustion webhooks.constructEventverification failures (signature mismatch, stale timestamp, malformed header)
It does not fire on:
verifySignature/verifyReturnSignature— these return boolean, never throw- The constructor's invalid-key-prefix throw — that's a dev-time error before the reporter is wired
Callback shape
import type { ErrorReporter, ErrorReporterContext } from "@vonpay/checkout-node";
const reporter: ErrorReporter = (err, ctx) => {
// err is VonPayError | Error
// ctx is ErrorReporterContext:
// method: string // e.g. "sessions.create", "sessions.get",
// // "sessions.validate", "webhooks.constructEvent",
// // or a "GET /api/health"-style fallback
// sdkVersion: string
// url?: string // origin + path, no query string (no PII via params)
// status?: number // HTTP status if from API response
// requestId?: string // X-Request-Id for correlation
// code?: string // server error code (auth_invalid_key, etc.)
// attempt?: number // 0-indexed retry attempt
};
ErrorReporterContext.method is typed as a plain string — the SDK passes a method label such as "sessions.create" or "webhooks.constructEvent" where one is available, and otherwise falls back to a "<HTTP-METHOD> <path>" string (for example "GET /api/health").
Sentry example
import * as Sentry from "@sentry/node";
import { VonPayCheckout } from "@vonpay/checkout-node";
const vonpay = new VonPayCheckout({
apiKey: process.env.VON_PAY_SECRET_KEY!,
errorReporter: (err, ctx) => {
Sentry.captureException(err, {
tags: { sdk: "vonpay-node", method: ctx.method, code: ctx.code },
contexts: { vonpay: ctx },
});
},
});
Datadog example
import { VonPayCheckout } from "@vonpay/checkout-node";
const vonpay = new VonPayCheckout({
apiKey: process.env.VON_PAY_SECRET_KEY!,
errorReporter: (err, ctx) => {
logger.error("vonpay sdk error", { err, ...ctx });
},
});
errorReporter is opt-in and additive — if you don't configure it, errors still propagate via throw and nothing else changes.
Auto-Retry
The SDK automatically retries on 429 (rate-limited) and 5xx (server error) responses with exponential backoff. It reads the Retry-After header when present, capped at 60 seconds. Configure with maxRetries in the constructor (default: 2).
Error Handling
All methods throw VonPayError on non-2xx responses. Errors include structured fields for programmatic handling.
import { VonPayCheckout, VonPayError } from "@vonpay/checkout-node";
try {
await vonpay.sessions.create({ ... });
} catch (err) {
if (err instanceof VonPayError) {
console.error(err.message); // "Invalid API key"
console.error(err.status); // 401
console.error(err.code); // "auth_invalid_key"
console.error(err.fix); // "Check that your API key is correctly formatted and active"
console.error(err.docs); // "https://docs.vonpay.com/reference/security#key-types"
console.error(err.requestId); // "req_abc123"
console.error(err.rateLimit); // { limit: 100, remaining: 0, reset: 1710000000, retryAfter: 30 }
}
}
rateLimit is { limit, remaining, reset, retryAfter? } — retryAfter (seconds) is populated from the Retry-After header when the server sends one.
ErrorCode union type
The code field is a string-literal union, enabling exhaustive switch statements:
import type { ErrorCode } from "@vonpay/checkout-node";
function handleError(code: ErrorCode) {
switch (code) {
case "auth_missing_bearer":
case "auth_invalid_key":
case "auth_key_expired":
case "auth_key_type_forbidden":
case "auth_merchant_inactive":
case "auth_service_unavailable":
// authentication errors
break;
case "validation_error":
case "validation_missing_field":
case "validation_invalid_amount":
// validation errors
break;
case "rate_limit_exceeded":
case "rate_limit_exceeded_per_key":
// back off
break;
case "session_not_found":
case "session_expired":
case "session_wrong_state":
case "session_integrity_error":
// session errors
break;
// ... exhaustive handling
}
}
auth_invalid_key maps to HTTP 401, auth_key_type_forbidden to 403, and both rate_limit_exceeded / rate_limit_exceeded_per_key to 429.
Webhook Event Types
WebhookEvent is a discriminated union on the type field. The envelope carries id, type, created, livemode, merchant_id, and a typed data payload that varies by event type. See Webhook Event Reference for the full per-event data shapes.
| Event | Typed data fields |
|---|---|
charge.succeeded | session_id, payment_intent_id, transaction_id, amount, currency, card |
charge.failed | session_id, payment_intent_id, transaction_id, amount, currency, failure_reason, failure_code, network_decline_code, card |
charge.refunded | session_id, payment_intent_id, transaction_id, refund_id, amount, currency, reason, is_partial, original_charge_amount, card |
payment_intent.succeeded | session_id, payment_intent_id, transaction_id, amount, currency |
payment_intent.failed | session_id, payment_intent_id, transaction_id, amount, currency, failure_reason, failure_code, network_decline_code |
payment_intent.cancelled | session_id, payment_intent_id, transaction_id, amount, currency, cancellation_reason |
Every top-level data key is nullable — the SDK types each as T | null, so always null-check before use. card is { brand, last4 } (its inner fields are non-null once card itself is present) and is populated only when card enrichment is active for the merchant; null otherwise. failure_code is the normalized decline code (e.g. card_declined); network_decline_code is the raw ISO-8583 code (e.g. 05).
import type { WebhookEvent } from "@vonpay/checkout-node";
function handle(event: WebhookEvent) {
switch (event.type) {
case "charge.succeeded":
// event.data.transaction_id is typed here
break;
case "charge.failed":
// event.data.failure_reason is typed here
break;
case "charge.refunded":
// event.data.amount is typed here
break;
}
}
TypeScript
All types are exported:
import type {
VonPayCheckoutConfig,
CreateSessionParams,
CheckoutSession,
SessionStatus,
LineItem,
HealthStatus,
VonPayError,
ErrorCode,
WebhookEvent,
} from "@vonpay/checkout-node";
Sample apps
Clone-and-run reference integrations using this SDK live in Von-Payments/vonpay-samples:
checkout-nextjs— Next.js 15 / React 19 hosted checkout with signed return verification + webhook handlercheckout-express— Node + Express 5 server-only hosted checkoutcheckout-paybylink-nextjs— Pay-by-link operator + customer flowplatform-integrator-nextjs— Multi-tenant platform pattern: per-tenant credentials, multi-tenant webhook routing, idempotency keys (CRM, subscription-billing, and ISV connector shape — see also Integrate VORA as a Payment Gateway)
Each ships with .env.example, a per-sample README, and pinned to a known-working SDK version.