Skip to main content

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.

  • 200 means the session is closed and no payment attempt that goes through our servers was in progress or can start. Expiring an already-expired session whose expiredFrom is pending or failed also returns 200, so the call is safe to retry.
  • 409 session_not_expirable means nothing changed. On a declined session it carries a reason: recent_activity comes with retryAfterSeconds and a Retry-After header, so call again after that many seconds; payment_may_be_in_flight may never clear. Treat an unrecognised reason as payment_may_be_in_flight. Without a recent_activity reason, a payment may exist on the session that has not been reported, which is also what you get when expiredFrom is processing or null. 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 an elements + chargeAtSubmit session, expiredFrom: "processing" can later change to failed once the provider confirms the payment was never authorized after a timed-out bank check, and that attempt's payment_intent.failed / charge.failed webhooks 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 }'
FieldRequiredNotes
amountYesThe new total in minor units (5498 = $54.98), from 1 to 99,999,999.
currencyNoISO 4217 code. Omit it to keep the session's currency.
lineItemsNoSame 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:

reasonWhat it meansWhat to do
integration_modeHosted checkout and the embedded payment form fix the total when the form loads.Create a new session with the new total.
setup_sessionA no-charge setup session has no total.Nothing to change.
store_orderThe 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_checkYour 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_openA 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.

MethodPathAuthDescription
POST/v1/sessionsPublishable or SecretCreate 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=truePublishable or SecretValidate params without creating a session
GET/v1/sessions/{sessionId}SecretGet session status
PATCH/v1/sessions/{sessionId}SecretChange 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}/expireSecretClose 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_intentsSecretCreate a payment intent (auth or auth+capture) — see guide
GET/v1/payment_intents/{id}SecretRetrieve 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}/captureSecretCapture an authorized intent (full or partial via amount_to_capture)
POST/v1/payment_intents/{id}/voidSecretVoid an authorized intent (before capture)
GET/v1/payment_intents/{id}/mirror-orderSecretMirrored store-order reference for a discrete-flow intent — see Connected platforms
GET/v1/refundsSecretList one payment's refunds, newest first, with its refund totals — see Refunds
POST/v1/refundsSecretRefund a succeeded payment intent (full or partial via amount)
POST/v1/tokensSecretMint a vp_pmt_* payment-method token from a provider vault reference (provider_reference)
GET/v1/payment_methodsSecretList a buyer's saved cards by your own customer reference — see Tokens
PATCH/v1/tokens/{id}SecretReplace the billing address stored with a saved card, or clear it with null (see Tokens)
DELETE/v1/tokens/{id}SecretRevoke a stored payment method so it can never be charged again — see Tokens
POST/v1/buyersSecretCreate or update a buyer record (upsert on external_id / email) — see Buyers
GET/v1/buyersSecretExact buyer lookup by ?external_id= or ?email= (at least one required; no unfiltered listing)
GET/v1/buyers/{id}SecretRetrieve a buyer by vp_by_* id
PATCH/v1/buyers/{id}SecretSparse buyer profile update (external_id not patchable)
GET/v1/capabilitiesPublishable or SecretPer-merchant capability matrix (supported_operations, settlement_currencies, rate_limits)
GET/v1/descriptor_configSecretYour statement-descriptor configuration — see Statement descriptors
POST/v1/mirror/quoteSecretPrice a basket against the connected store before charging — see Connected platforms
GET/v1/webhook_subscriptionsSecretList your webhook subscriptions — see Webhook verification
POST/v1/webhook_subscriptionsSecretCreate a webhook subscription
GET/v1/webhook_subscriptions/{id}SecretRetrieve one subscription
PATCH/v1/webhook_subscriptions/{id}SecretUpdate a subscription's events, description or status (its URL cannot change)
DELETE/v1/webhook_subscriptions/{id}SecretDelete a subscription
POST/v1/webhook_subscriptions/{id}/rotate_signing_secretSecretRotate a subscription's whsec_* signing secret — see Webhook secrets
POST/v1/webhook_subscriptions/{id}/send_test_eventSecretSend a test event to a subscription
GET/v1/webhook_events/{id}SecretRetrieve a single webhook event
GET/v1/webhook_listenSecretStream 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-domainsSecretList your Apple Pay / Google Pay domain verifications — see Apple Pay setup
POST/v1/wallet-domainsSecretRegister a wallet domain
GET/v1/wallet-domains/{id}SecretRetrieve one wallet-domain verification
DELETE/v1/wallet-domains/{id}SecretRemove a wallet-domain verification (soft delete)
POST/v1/wallet-domains/{id}/verifySecretRe-run verification for one wallet domain
GET/v1/wallet-domains/{id}/fileSecretDownload the Apple Pay domain-verification file to host on your site
POST/v1/sdk-telemetrySecretSDK 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}PublishableBrowser-safe session lookup — used by Embedded Fields SDK
POST/v1/public/binder-loadPublishablePer-processor element capability map (Embedded Fields)
POST/v1/public/tokensPublishableBrowser-side vp_pmt_* minting (Embedded Fields)
GET/v1/public/payment_intents/{id}/eventsSigned tokenLive payment-status stream for the buyer's browser (Server-Sent Events). ⚠️ Not generally available — see Streaming endpoints below
GET/api/healthNoneHealth check (add ?deep=true for deep variant)
POST/v1/public/sessions/{sessionId}/chargePublishableCharge 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}/completePublishableResolve 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-tokenPublishablePoll 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-orderPublishableRead 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-sessionPublishableApple 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-sessionPublishableGoogle 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​

HeaderRequiredDescription
AuthorizationYesBearer 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-TypeConditionalapplication/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-VersionNoAPI 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-KeyConditionalOne 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:

HeaderDescription
X-Request-IdUnique 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.