# Von Payments Checkout API > Von Payments is a hosted checkout page. Merchants create a session via API, redirect the buyer to the checkout URL, the buyer pays, and is redirected back with a signed confirmation. ## Quick Start 1. Call `POST /v1/sessions` with your API key to create a checkout session 2. Redirect the buyer to the returned `checkoutUrl` 3. Buyer pays on the Von Payments hosted page (cards, Apple Pay, Google Pay, Klarna, 130+ methods) 4. Buyer is redirected to your `successUrl` with signed query params 5. Verify the HMAC signature server-side ## Authentication All merchant API calls require a Bearer token: ``` Authorization: Bearer vp_sk_live_xxx ``` ### Key Types - **Publishable keys** (`vp_pk_*`) — safe for browser code, can only create sessions - **Secret keys** (`vp_sk_*`) — server-only, full API access - **Legacy keys** (`vp_key_*`) — treated as secret, deprecated ## API Versioning Include `Von-Pay-Version: 2026-04-14` header to pin a version. Response always includes `Von-Pay-Version` and `Von-Pay-Latest-Version` headers. ## Self-Healing Errors All error responses include structured fields for programmatic handling: ```json { "error": "Human-readable message", "code": "auth_missing_bearer", "fix": "Include an Authorization: Bearer header", "docs": "https://docs.vonpay.com/reference/security#authentication" } ``` ## Key Rotation Secret keys can be rotated without downtime. When a key is rotated, the previous key enters a grace window (default 1h) during which both keys authenticate. After grace closes, the previous key returns `401` with `code: auth_key_expired` — distinct from `auth_invalid_key` so SDKs can detect rotation and refresh instead of failing the payment. If a publisher force-deactivates a key mid-rotation (`is_active=false` while grace/expiry metadata is set), that also returns `auth_key_expired` — the deactivation is treated as an accelerated rotation endpoint, not a plain revocation. Plain deactivation (`is_active=false` with no rotation metadata) returns `auth_invalid_key`. See https://docs.vonpay.com/reference/security#key-rotation for the full rotation flow. ## Dry-Run Validation Validate a session request without creating it: ``` POST /v1/sessions?dry_run=true → 200 { "valid": true, "warnings": [] } → 400 { "valid": false, "errors": [...] } ``` ## Machine Discovery ``` GET /.well-known/vonpay.json ``` Returns API version, endpoints, docs URLs, SDK package names. ## Create a Checkout Session ``` POST /v1/sessions Authorization: Bearer vp_sk_live_xxx Content-Type: application/json Idempotency-Key: optional-dedup-key { "amount": 1499, "currency": "USD", "country": "US", "successUrl": "https://mystore.com/order/123/confirm", "cancelUrl": "https://mystore.com/cart", "description": "Order #123", "locale": "en", "mode": "payment", "buyerId": "cust_123", "buyerName": "Jane Doe", "buyerEmail": "jane@example.com", "lineItems": [ { "name": "Premium Widget", "quantity": 1, "unitAmount": 1499 } ], "metadata": { "orderId": "order_123" }, "expiresIn": 1800 } ``` Response (201): ```json { "id": "vp_cs_live_k7x9m2n4p3abcdef", "checkoutUrl": "https://checkout.vonpay.com/checkout?session=vp_cs_live_k7x9m2n4p3abcdef", "expiresAt": "2026-03-31T15:30:00.000Z" } ``` ### Required fields - `amount` (integer) — amount in minor units (cents). 1499 = $14.99 - `currency` (string) — ISO 4217, 3 chars (USD, EUR, GBP) ### Optional fields - `country` (string) — ISO 3166-1 alpha-2, 2 chars. Auto-detected if omitted. - `successUrl` (string) — HTTPS redirect URL after payment success - `cancelUrl` (string) — HTTPS redirect URL on cancel - `description` (string) — payment description for bank statements - `locale` (string) — checkout page language (en, fr, de, etc.) - `mode` (string) — "payment" (default). Future: "setup" for card-on-file. - `buyerId` (string) — your external customer ID (enables saved payment methods) - `buyerName` (string) — pre-fills billing form, encrypted at rest - `buyerEmail` (string) — encrypted at rest - `lineItems` (array) — items displayed on checkout page. Each: { name, quantity, unitAmount, imageUrl? } - `metadata` (object) — key-value string pairs, passed through to webhooks - `expiresIn` (integer) — session TTL in seconds (300-3600, default 1800) ### Notes - `merchantId` is derived from your API key — you don't pass it - `amount` is the source of truth for charging. `lineItems` are display-only. - Sessions expire after 30 minutes by default - Idempotency: pass `Idempotency-Key` header to prevent duplicate sessions on retries - **Sandbox vs live is decided at session creation** from the authenticating key's merchant config (internal `is_sandbox` flag). It is never read from the request body, and it is frozen for the lifetime of the session — subsequent reads and webhook reconciliation honor the same mode. There is no `sandbox` or `mode=test|live` field on the request body. ## Get Session Status Requires a secret key (`vp_sk_*`). Publishable keys receive 403. ``` GET /v1/sessions/vp_cs_live_k7x9m2n4p3abcdef Authorization: Bearer vp_sk_live_xxx ``` Response: ```json { "id": "vp_cs_live_k7x9m2n4p3abcdef", "status": "succeeded", "mode": "payment", "merchantId": "default", "amount": 1499, "currency": "USD", "country": "US", "description": "Order #123", "transactionId": "txn_abc123", "metadata": { "orderId": "order_123" }, "createdAt": "2026-03-31T15:00:00.000Z", "updatedAt": "2026-03-31T15:05:00.000Z", "expiresAt": "2026-03-31T15:30:00.000Z" } ``` Session statuses: `pending` → `processing` → `succeeded` | `failed` | `expired` ## Verify Return URL Signature After payment, the buyer is redirected to your `successUrl` with signed params: ``` https://mystore.com/confirm?session=vp_cs_live_xxx&status=succeeded&amount=1499¤cy=USD&transaction_id=txn_abc&sig=hexstring ``` Verify the HMAC-SHA256 signature server-side: ``` sig = HMAC-SHA256( key: VON_PAY_SESSION_SECRET, data: "{session}.{status}.{amount}.{currency}.{transaction_id}" ) ``` Example data string: `vp_cs_live_k7x9m2n4p3abcdef.succeeded.1499.USD.txn_abc123` ### Node.js verification ```javascript import crypto from "crypto"; function verifyReturnSignature(params, secret) { const data = `${params.session}.${params.status}.${params.amount}.${params.currency}.${params.transaction_id || ""}`; const expected = crypto.createHmac("sha256", secret).update(data).digest("hex"); return crypto.timingSafeEqual(Buffer.from(params.sig, "hex"), Buffer.from(expected, "hex")); } ``` ### Python verification ```python import hmac, hashlib def verify(session, status, amount, currency, transaction_id, sig, secret): data = f"{session}.{status}.{amount}.{currency}.{transaction_id or ''}" expected = hmac.new(secret.encode(), data.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(sig, expected) ``` ## Health Check ``` GET /api/health ``` No auth required. Returns `200` when healthy, `503` when degraded. ## Internal Routes (not for merchant integration) These exist only so the hosted checkout page can call back into the checkout service. They are not part of the merchant API — merchants should not call them directly. - `POST /api/checkout/init` — origin-validated; initializes payment embed for a session - `POST /api/checkout/complete` — origin-validated; finalizes session after provider success - `POST /api/checkout/client-error` — origin-validated; receives client-side error reports from the hosted page. Body capped at 4 KB — larger bodies return `413 { ok: false, error: "Payload too large" }`. - `POST /api/csp-report` — unauthenticated; receives browser-emitted Content-Security-Policy violation reports. Browsers send `application/csp-report`; no auth envelope exists for these reports so the route is origin-checked and rate-limited via the shared `clientError` bucket. Oversized or malformed bodies are rejected. ## Rate Limits | Endpoint | Limit | |----------|-------| | POST /v1/sessions | 10/min per IP | | GET /v1/sessions/:id | 30/min per IP | Rate-limited responses return `429` with `Retry-After: 60` header. ## Error Format All errors return: ```json { "error": "Human-readable message" } ``` Every response includes `X-Request-Id` header for debugging. | Code | Meaning | |------|---------| | 400 | Invalid request body | | 401 | Authentication failed | | 403 | Forbidden — secret key required | | 404 | Session not found | | 409 | Session in wrong state | | 410 | Session expired | | 429 | Rate limited | | 500 | Server error | | 503 | Auth service unavailable (`code: auth_service_unavailable`) — the replicated auth schema is mid-migration and the key-state classifier cannot safely enforce grace/expiry predicates. Retry with exponential backoff; clears automatically once replication catches up. | ## SDKs ### Node.js (@vonpay/node) ```bash npm install @vonpay/node ``` ```typescript import { VonPay } from "@vonpay/node"; const vonpay = new VonPay("vp_sk_live_xxx"); // Create session const session = await vonpay.sessions.create({ amount: 1499, currency: "USD", successUrl: "https://mystore.com/confirm", lineItems: [{ name: "Widget", quantity: 1, unitAmount: 1499 }], }); // Redirect buyer to session.checkoutUrl // Check status const status = await vonpay.sessions.get("vp_cs_live_xxx"); // Verify return signature const isValid = VonPay.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); ``` ### Browser (vonpay.js) ```html ``` ## Security - PCI SAQ-A: card data never touches your servers or ours (secure iframe) - PII (buyer name, email) encrypted with AES-256-GCM at rest - HMAC-SHA256 signed return URLs (includes amount + currency) - All URLs must be HTTPS (localhost exempt in sandbox) - Session tokens are 16-char cryptographically random strings ## Links - Docs: https://docs.vonpay.com - OpenAPI spec: https://checkout.vonpay.com/openapi.yaml - Status: https://checkout.vonpay.com/api/health