Security
Authentication
API requests use Bearer token authentication:
Key Types
Authorization: Bearer vp_sk_live_xxx
- Test keys (
vp_sk_test_xxx) — test mode, no real money moves - Live keys (
vp_sk_live_xxx) — live mode; presents your provider's production credentials, real payments - Publishable keys (
vp_pk_test_xxx/vp_pk_live_xxx) — the browser-side counterpart, safe to ship in a bundle
Mode is decided by the key prefix on every request — one account holds both and flips per request. What a test key actually reaches depends on how your merchant is set up: a sandbox account routes to a Von-owned emulator that contacts no processor at all, while a merchant boarded onto a real processor's sandbox presents that processor's sandbox credentials, selected by mode with no fallback. See Sandbox & Test Mode for the difference — it changes which test cards work.
Keep your secret key secret. If compromised, contact Von Payments to rotate it.
_test_ / _live_ selects how the charge is routed, on every request. It does not select which host you call — the same host serves both, and your browser's host comes from apiBaseUrl, which defaults to production and is never inferred from your key. If your browser and server name different hosts, your server calls succeed while every browser call returns 401 auth_invalid_key_publishable. See Which environment am I talking to? and the troubleshooting entry.
Key Rotation
Secret keys can be rotated without downtime. When a key is rotated, the previous key enters a 24-hour grace window during which both keys authenticate. After the grace window 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.
- Plain deactivation (
is_active=falsewith no rotation metadata) returnsauth_invalid_key - Force-deactivation mid-rotation (flipping
is_active=falsewhile grace/expiry metadata is set) returnsauth_key_expired— the deactivation is treated as an accelerated rotation, not a plain revocation
Rotate keys via /dashboard/developers/api-keys. The UI shows the new plaintext exactly once at creation — store it immediately.
Confirming the return redirect
When a buyer completes payment they are redirected to your successUrl carrying the session ID:
?session=vp_cs_live_xxx
Treat everything on that URL as untrusted. It arrives through the buyer's browser, so it is visible and modifiable, and reaching the page at all says nothing about whether the payment succeeded — the buyer lands there after the attempt whatever the outcome.
Confirm the payment by looking the session up from your server with your secret key, and take the result from the API response:
const { status } = await client.sessions.get(sessionId);
Then fulfil the order from the charge.succeeded webhook, which reaches you directly rather than through the browser. See Handle the return.
Signature checking is a real control, but its place is on webhooks — messages Von Payments sends straight to your server, each endpoint carrying its own whsec_* signing secret. See Webhook Signature Verification below.
A signature on a browser redirect can only establish that the message was not altered. It cannot establish that the payment succeeded, because a declined payment is signed exactly as validly as an approved one — so it must never be what gates an order.
PCI Compliance
Von Payments is PCI SAQ-A compliant:
- Card data is entered in a secure iframe hosted by the payment processor
- In the live hosted and embedded flows, card numbers, CVVs, and expiry dates never touch Von Payments servers or your servers
- The checkout page's Content Security Policy prevents any script from reading the payment iframe
You do not need PCI certification to use Von Payments. Both front-end integration paths (Hosted Checkout and Embedded Fields — in either embed or elements mode) using the server-side Payment Intents engine keep real card data inside the iframe boundary; your PCI scope stays at SAQ-A (the simplest level). The only path that would pull you out of SAQ-A is sending raw card data through your own server — and our API does not accept real card data for processing.
Sandbox note: In test mode you can submit a published, synthetic test card number to the sandbox emulator to exercise the flow without real card data. This path is sandbox-only, never reaches a real processor, and does not collect CVV or expiry. Live keys cannot use it.
Data Encryption
| Data | Protection |
|---|---|
| Card data | Never stored — entered in processor's iframe |
| Buyer name | AES-256-GCM encrypted at rest |
| Buyer email | AES-256-GCM encrypted at rest |
| API keys | SHA-256 hashed in database |
| Checkout session IDs | Cryptographically random (vp_cs_<mode>_ + nanoid) |
Transport Security
- Merchant-supplied URLs (such as
successUrlandcancelUrl) must use HTTPS;localhostis exempt at the validation layer so you can test locally - Live-mode keys reject
localhost/loopback redirect URLs — local redirects are permitted only with test-mode keys - HSTS header with 2-year max-age
- TLS 1.2+ only (per the platform's information security policy)
Security Headers
The checkout page serves these headers:
| Header | Value |
|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains; preload |
X-Frame-Options | DENY |
X-Content-Type-Options | nosniff |
Referrer-Policy | strict-origin-when-cross-origin |
Content-Security-Policy | Restrictive nonce-based policy allowing only required domains |
Rate Limiting
API endpoints are rate-limited. Per-IP is the default axis: when a request carries no credential, the limiter keys on the client IP. For many endpoints the primary axis is instead keyed on the API key, the merchant, or a signed-URL token (with IP used only as a fallback when no credential is present). The session-create endpoint layers a per-API-key axis on top of its per-IP axis. A few internal routes (such as the shallow health check) are unmetered. See Rate Limits for buckets and handling.
API Versioning
The API uses date-based versioning via the Von-Pay-Version header:
Von-Pay-Version: 2026-04-14
- If the header is omitted (or carries an unrecognized value), the current/latest platform-wide API version is used
- Pin this header to a specific date to prevent breaking changes when the API evolves
- New versions are announced in the changelog before becoming the default
Webhook Signature Verification
Webhooks are signed with HMAC-SHA256, keyed with a per-endpoint whsec_* signing secret. The signature ships in the x-vonpay-signature request header with both the timestamp and the HMAC inline:
x-vonpay-signature: t=1714406400,v1=abc123def456...
Verification rules:
- Capture the raw request body as bytes before any JSON parsing
- Form
signed_payload = t + "." + raw_body - Compute
v1_expected = lowercase_hex(HMAC_SHA256(key: whsec_secret_as_utf8_bytes, message: signed_payload)) - Constant-time compare against each
v1=value in the header (the header may carry twov1=entries during a rotation window; accept on any match, reject if there are more than two) - Reject if
now - t > 300(older than 5 min) ort - now > 30(more than 30 sec in the future)
Reference verifiers in Node, Python, Go, Ruby, and PHP are at Webhook Signature Verification. Use the published verifiers as-is rather than hand-rolling — the canonical implementation handles multi-secret rotation, length-safe constant-time compares, and asymmetric replay-window enforcement.
Important: Each webhook endpoint has its own
whsec_*signing secret. It is NOT your API key (vp_sk_*), and it is not thess_*session secret (which your integration does not use). The webhook signing secret is used only for webhook signatures, and your API key is used only for outbound API auth. The session signing secret backs return URL signatures (and other server-side session integrity checks) — never use it as your webhook or API credential. See Webhook Signing Secrets for the create / rotate / revoke lifecycle.
Reporting Vulnerabilities
If you discover a security vulnerability, contact security@vonpay.com.