API Reference
The complete Von Payments Checkout API is documented in OpenAPI 3.1 format.
OpenAPI Spec
openapi.yaml — import into Postman, Insomnia, Redocly, or any OpenAPI tool.
Two front-end integration paths
Hosted Checkout (POST /v1/sessions → redirect the buyer to checkoutUrl) and Embedded Fields (the browser SDK on your own page, in embed or elements mode) both vault a vp_pmt_* and settle through Payment Intents, the server-side engine for authorization, capture, refund and void. On an Embedded Fields charge-and-save session submit() has already charged — do not also call POST /v1/payment_intents for it. Side-by-side comparison: Choose your integration.
Endpoints Summary
Get session status
GET /v1/sessions/{id} returns the full status of a previously-created session. Requires a secret key (vp_sk_*); publishable keys are rejected with auth_key_type_forbidden. See Session Object for the response shape.
Close a session so you can charge another way
POST /v1/sessions/{sessionId}/expire closes an open, unpaid session so no payment can start on it afterwards. Call it before you take payment for the same order another way, such as when a buyer whose card was declined on the payment form switches to a card you saved earlier.
Reading the session first is not enough. GET /v1/sessions/{sessionId} tells you what has been recorded so far, but a payment the buyer's browser sent before giving up can still arrive after your read. Expiring the session closes that window: the check and the change happen in one atomic step, and payment attempts take the same step, so an expire and a payment racing for one session can never both succeed.
Closing a declined session. A failed session created with integrationMode: "elements" and chargeAtSubmit: true can also be closed (expiredFrom: "failed"): when no payment is recorded, paymentStatus is unpaid, no payment attempt on it is still in progress (such as a card waiting on the buyer's bank check), no hold from it is waiting to be released, and its hosted checkout page was never opened. A decline known to belong to the session's latest payment attempt is closed immediately. Otherwise the session must be unchanged for 10 minutes: the wait covers a payment that can still finish shortly after a decline it cannot be tied to, such as a wallet payment on the same session. If the decline allowed another card on the same session (retry.allowed: true), the session is back to pending and closes immediately.
200means the session is closed and no payment attempt that goes through our servers was in progress or can start. Expiring an already-expiredsession whoseexpiredFromispendingorfailedalso returns200, so the call is safe to retry.409 session_not_expirablemeans nothing changed. On a declined session it carries areason:recent_activitycomes withretryAfterSecondsand aRetry-Afterheader, so call again after that many seconds;payment_may_be_in_flightmay never clear. Treat an unrecognisedreasonaspayment_may_be_in_flight. Without arecent_activityreason, a payment may exist on the session that has not been reported, which is also what you get whenexpiredFromisprocessingornull. Do not take payment for the order another way. For those two states, do not rely on polling the session or on a webhook either: there is no session-expiry event, and otherwise a payment that lands on an already-expired session is not reported today. On anelements+chargeAtSubmitsession,expiredFrom: "processing"can later change tofailedonce the provider confirms the payment was never authorized after a timed-out bank check, and that attempt'spayment_intent.failed/charge.failedwebhooks announce it. Contact support with the session id.
One exception, on embed sessions. The buyer's browser pays the processor directly there, using a credential issued when the form loaded. That credential stays valid for up to an hour and cannot be recalled, so expiring the session stops new forms loading and refuses everything that goes through our servers, but it cannot stop a payment the buyer had already submitted. Remove or disable the payment form on your page and confirm the buyer is not mid-submission before you expire it. If a payment completes anyway it is not reported to you today, and it carries no payment intent, so it cannot be refunded through this API; contact support with the session id. Reporting for that case is being built. Sessions in elements mode, hosted checkout and wallet payments all go through our servers, where the guarantee holds.
Change an open session's total
PATCH /v1/sessions/{sessionId} changes the total of an open, unpaid checkout session. You call it when the buyer picks a delivery address or a shipping option inside the Apple Pay or Google Pay sheet and your store re-prices the order. Requires a secret key.
curl -X PATCH https://checkout.vonpay.com/v1/sessions/vp_cs_live_k7x9m2n4p3 \
-H "Authorization: Bearer $VON_PAY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 5498 }'
| Field | Required | Notes |
|---|---|---|
amount | Yes | The new total in minor units (5498 = $54.98), from 1 to 99,999,999. |
currency | No | ISO 4217 code. Omit it to keep the session's currency. |
lineItems | No | Same shape as on POST /v1/sessions. When sent, it replaces the displayed line items. |
A 200 returns the same object as GET /v1/sessions/{sessionId}, carrying the new amount. The server SDKs call it sessions.update() (2.11.0 and later). The call sends no webhook, and an Idempotency-Key is optional because setting a total is idempotent on its own.
Only elements payment sessions can change their total. Anything else returns 409 session_not_modifiable with a reason, and nothing is changed:
reason | What it means | What to do |
|---|---|---|
integration_mode | Hosted checkout and the embedded payment form fix the total when the form loads. | Create a new session with the new total. |
setup_session | A no-charge setup session has no total. | Nothing to change. |
store_order | The session is linked to a connected-store order (mirror or order), so the total is the store's. | Change the order in the store instead. |
in_page_bank_check | Your account runs the buyer's bank check (3-D Secure) on the page before paying. The bank checks the amount, and the charged total must match the checked one. | Create a new session with the new total. |
not_open | A payment is in progress, the session is paid, held, declined or expired, or it has passed its time limit (it can still read pending and unpaid for a short while after). | Read session.status and session.paymentStatus in the response. Do not take payment for the same order another way while a payment is in progress. |
A change can never land under a payment already in progress. The check and the change are one atomic step, and payment attempts go through the same step. The payment form also tells the server which total the buyer approved: the express-checkout element sends it for you, and a bring-your-own-button integration sends it as expected_amount. If the session no longer holds that total, the payment is refused with 409 expected_amount_mismatch and the buyer is not charged. A total that changes while a payment request is being processed is refused the same way, even when no expected_amount was sent.
Session statuses
A checkout session has one of five statuses: pending, processing, succeeded, failed, or expired. The normal flow is pending → processing → succeeded (or → failed / → expired); a session may also move directly pending → succeeded/failed/expired. Not all transitions are one-way: a processing session can revert to pending, and a failed session can later converge to succeeded on a successful retry (success takes precedence). Only succeeded and expired are terminal. See Session Object — Status Lifecycle.
Payment intent statuses
Payment intents progress through a discrete lifecycle of six statuses: requires_action → authorized → captured → succeeded (or voided / failed). On the manual-capture path, authorized → captured → succeeded runs as three discrete steps; on the automatic-capture path, an authorized intent can move straight to succeeded when settlement is synchronous. See the Payment Intents guide for the full state machine, transition rules, and error envelope.
Endpoints
This lists the merchant endpoints in the published contract. API error responses carry a docs link that points into this table. Where an endpoint is operator-enabled rather than always available, the row says so — being in the contract is not the same as being switched on for your account.
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /v1/sessions | Publishable or Secret | Create a checkout session (Hosted Checkout) — the only merchant-facing endpoint a publishable key can write through. Eleven /v1/public/* routes require one; the three below are the commonly-used ones, see API Key Types for all of them |
POST | /v1/sessions?dry_run=true | Publishable or Secret | Validate params without creating a session |
GET | /v1/sessions/{sessionId} | Secret | Get session status |
PATCH | /v1/sessions/{sessionId} | Secret | Change the total of an open, unpaid elements session, typically after the buyer picks a shipping address or option in the Apple Pay / Google Pay sheet. Refused with 409 session_not_modifiable (nothing changed) on hosted and embedded-form sessions, setup sessions, sessions linked to a store order, and sessions with an in-page bank check; create a new session for those. Sends no webhook. See Change an open session's total |
POST | /v1/sessions/{sessionId}/expire | Secret | Close an open, unpaid session so no payment can start on it afterwards. Call it before taking payment for the same order another way, such as when a buyer whose card was declined switches to a card you saved earlier. Reading the session does not claim it, and a payment the browser sent before the buyer gave up can still arrive after your read. Idempotent on an already-expired session whose expiredFrom is pending or failed; any other state returns 409 session_not_expirable and changes nothing |
POST | /v1/payment_intents | Secret | Create a payment intent (auth or auth+capture) — see guide |
GET | /v1/payment_intents/{id} | Secret | Retrieve a payment intent's persisted state. This is where you read back the full metadata — the provider truncates long values on the charge itself, but the intent keeps what you sent. Because it comes back verbatim, keep card numbers, bank details and government IDs out of metadata |
POST | /v1/payment_intents/{id}/capture | Secret | Capture an authorized intent (full or partial via amount_to_capture) |
POST | /v1/payment_intents/{id}/void | Secret | Void an authorized intent (before capture) |
GET | /v1/payment_intents/{id}/mirror-order | Secret | Mirrored store-order reference for a discrete-flow intent — see Connected platforms |
GET | /v1/refunds | Secret | List one payment's refunds, newest first, with its refund totals — see Refunds |
POST | /v1/refunds | Secret | Refund a succeeded payment intent (full or partial via amount) |
POST | /v1/tokens | Secret | Mint a vp_pmt_* payment-method token from a provider vault reference (provider_reference) |
GET | /v1/payment_methods | Secret | List a buyer's saved cards by your own customer reference — see Tokens |
PATCH | /v1/tokens/{id} | Secret | Replace the billing address stored with a saved card, or clear it with null (see Tokens) |
DELETE | /v1/tokens/{id} | Secret | Revoke a stored payment method so it can never be charged again — see Tokens |
POST | /v1/buyers | Secret | Create or update a buyer record (upsert on external_id / email) — see Buyers |
GET | /v1/buyers | Secret | Exact buyer lookup by ?external_id= or ?email= (at least one required; no unfiltered listing) |
GET | /v1/buyers/{id} | Secret | Retrieve a buyer by vp_by_* id |
PATCH | /v1/buyers/{id} | Secret | Sparse buyer profile update (external_id not patchable) |
GET | /v1/capabilities | Publishable or Secret | Per-merchant capability matrix (supported_operations, settlement_currencies, rate_limits) |
GET | /v1/descriptor_config | Secret | Your statement-descriptor configuration — see Statement descriptors |
POST | /v1/mirror/quote | Secret | Price a basket against the connected store before charging — see Connected platforms |
GET | /v1/webhook_subscriptions | Secret | List your webhook subscriptions — see Webhook verification |
POST | /v1/webhook_subscriptions | Secret | Create a webhook subscription |
GET | /v1/webhook_subscriptions/{id} | Secret | Retrieve one subscription |
PATCH | /v1/webhook_subscriptions/{id} | Secret | Update a subscription's events, description or status (its URL cannot change) |
DELETE | /v1/webhook_subscriptions/{id} | Secret | Delete a subscription |
POST | /v1/webhook_subscriptions/{id}/rotate_signing_secret | Secret | Rotate a subscription's whsec_* signing secret — see Webhook secrets |
POST | /v1/webhook_subscriptions/{id}/send_test_event | Secret | Send a test event to a subscription |
GET | /v1/webhook_events/{id} | Secret | Retrieve a single webhook event |
GET | /v1/webhook_listen | Secret | Stream webhook delivery transcripts (Server-Sent Events) — what the CLI's listen command uses. ⚠️ Not generally available — see Streaming endpoints below. Listed because it is in the published contract and the CLI references it |
GET | /v1/wallet-domains | Secret | List your Apple Pay / Google Pay domain verifications — see Apple Pay setup |
POST | /v1/wallet-domains | Secret | Register a wallet domain |
GET | /v1/wallet-domains/{id} | Secret | Retrieve one wallet-domain verification |
DELETE | /v1/wallet-domains/{id} | Secret | Remove a wallet-domain verification (soft delete) |
POST | /v1/wallet-domains/{id}/verify | Secret | Re-run verification for one wallet domain |
GET | /v1/wallet-domains/{id}/file | Secret | Download the Apple Pay domain-verification file to host on your site |
POST | /v1/sdk-telemetry | Secret | SDK telemetry (on by default with a test key, off with a live key). Your code does not call this — the server SDKs post to it for you when telemetry is enabled |
GET | /v1/public/sessions/{sessionId} | Publishable | Browser-safe session lookup — used by Embedded Fields SDK |
POST | /v1/public/binder-load | Publishable | Per-processor element capability map (Embedded Fields) |
POST | /v1/public/tokens | Publishable | Browser-side vp_pmt_* minting (Embedded Fields) |
GET | /v1/public/payment_intents/{id}/events | Signed token | Live payment-status stream for the buyer's browser (Server-Sent Events). ⚠️ Not generally available — see Streaming endpoints below |
GET | /api/health | None | Health check (add ?deep=true for deep variant) |
POST | /v1/public/sessions/{sessionId}/charge | Publishable | Charge the card the buyer just entered, from the browser, on an Embedded Fields elements-mode session — the charge-at-submit path. Your server never sees the card. See Custom layout; on this path do not also call POST /v1/payment_intents, or you charge twice. |
POST | /v1/public/sessions/{sessionId}/complete | Publishable | Resolve the outcome of an embedded payment after submit. The SDK calls this for you — you read the result it returns. A result here is a UX signal, not proof of payment: confirm from the webhook before fulfilling. |
GET | /v1/public/sessions/{sessionId}/embed-token | Publishable | Poll whether an embedded session is ready to mount. The SDK handles this during load(); you would only call it directly when diagnosing a card form that never appears — see Embedded Fields errors. |
GET | /v1/public/sessions/{sessionId}/mirror-order | Publishable | Read the connected-store order reference for a completed session — for showing an order number on your thank-you page. Safe from the browser; it returns the reference only, never buyer identity. |
POST | /v1/public/wallets/apple-session | Publishable | Apple Pay merchant validation, for a bring-your-own-button wallet integration. Not needed with the Express Checkout element, which handles validation itself — see Standalone wallets. |
POST | /v1/public/wallets/google-session | Publishable | Google Pay session setup for a bring-your-own-button wallet integration. Not needed with the Express Checkout element — see Standalone wallets. |
Streaming endpoints are not generally available
Two endpoints in the contract stream over Server-Sent Events, and both answer 503 to every caller — use webhook subscriptions for delivery instead. They fail in different shapes: GET /v1/webhook_listen returns a JSON envelope with service_unavailable and Retry-After: 5 (waiting changes nothing); GET /v1/public/payment_intents/{id}/events returns a 503 with no body, so code that parses every error as JSON throws instead of reading a code.
Authentication
Merchant-facing endpoints use Bearer token auth:
Authorization: Bearer vp_sk_live_xxx
Test keys use the vp_sk_test_ prefix. Live keys use vp_sk_live_.
Most endpoints require a secret key (vp_sk_*). The exceptions are session creation (POST /v1/sessions, including its dry_run variant) and GET /v1/capabilities, which accept either key type, and the /v1/public/* endpoints, which require a publishable key (vp_pk_*). So a publishable key can create sessions, read capabilities, and call the public browser endpoints; it is rejected on every secret-only route with auth_key_type_forbidden (HTTP 403).
Request Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer API key. The accepted key type depends on the endpoint — secret (vp_sk_*) for most routes, publishable (vp_pk_*) for the /v1/public/* endpoints, and either type for session creation and GET /v1/capabilities. |
Content-Type | Conditional | application/json on the browser/public POST endpoints (/v1/public/sessions/{id}/charge, /v1/public/tokens), which reject a missing or non-JSON type with HTTP 415 unsupported_media_type. The authenticated REST POST endpoints (/v1/sessions, /v1/payment_intents, /v1/refunds, /v1/tokens) parse the JSON body without a Content-Type check, but sending application/json is recommended for all POST requests. |
Von-Pay-Version | No | API version date string (e.g. 2026-04-14). If omitted, the request uses the current API version. Pin this header to avoid breaking changes when the API evolves. |
Idempotency-Key | Conditional | One key per logical operation, reused on every retry — a replay with the same key returns the original response. Required on every POST /v1/payment_intents from the 28 October 2026 cut-off, after which a keyless charge can be refused at any time; until then a keyless charge succeeds but its response carries Deprecation, Sunset and Link headers. Already required on POST /v1/refunds and POST /v1/payment_intents where the processor connection keeps no queryable record of its own (a keyless request is refused with 400 idempotency_key_required); optional elsewhere, but a keyless retry issues a second real refund or charge. Never mint a fresh key to get past a 409 charge_in_progress. Max 255 printable-ASCII characters. See Idempotency. |
Response Headers
Every response includes:
| Header | Description |
|---|---|
X-Request-Id | Unique request ID for debugging |
The three rate-limit headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset are returned on every rate-limited response — both successful responses and 429 responses. Retry-After is the only one that also appears outside rate limiting: a 503 carries it too, both during a maintenance write-freeze and on the disabled-/v1/webhook_listen state described above. Do not read Retry-After as "you were rate limited" — branch on the status code. See Rate Limits.
Idempotency
Idempotency-Key is enforced per-resource: a key is unique to (merchant_id, idempotency_key) on the resource it created (session, payment intent, refund). A replayed key with a matching request body returns the original response. A replayed key with a different request body returns HTTP 422 idempotency_replay_incompatible — the SDK surfaces this loudly so a buggy retry that changed amount, cart, or redirect URLs cannot silently inherit a prior resource.
Retention. Idempotency keys are bound to the row they created, not to a separate cache with a fixed TTL. A replayed key returns the same resource for as long as the row is retained on our side — effectively the lifetime of the resource record. Use one key per logical operation and reuse it across every retry of that operation — that reuse is what makes the retry safe. Use a different key only when the operation is genuinely different (a second, intentional refund of the same payment; a new order), not merely because it is a later attempt. One exception: a refund that came back 200 with status: "canceled" is terminal and returned no money, and reissuing it under the same key replays that response forever — see Refunds. Full rule: Idempotency.
Format. Max 255 characters, printable ASCII only (codepoints 0x20–0x7E). Whitespace-only keys are treated as no key.
The server SDKs retry an ambiguous failure (5xx, timeout) on a money-moving call (create, capture, void, refund) only when you sent a key — see Auto-retry.
Errors
session_already_completed is returned as HTTP 409 Conflict, distinct from session_expired's HTTP 410 Gone. It comes from any endpoint that would settle an already-succeeded session again — the public completion and read endpoints, and POST /v1/payment_intents when the session_id you pass already charged at submit — never from POST /v1/sessions itself. Branch on code: never create a new session for a 409 (the buyer already paid — that would re-charge), whereas a 410 means the session TTL elapsed and you may create a fresh one.
Rate Limits
Full bucket list and handling rules: Rate Limits.