Skip to main content

Integrate VORA as a Payment Gateway

A one-page reference for platform engineering teams building a Von Payments connector inside their product. Audience: you've already had the partnership conversation (or you're scoping work before one) and you want the API surface mapped against the gateway-adapter shape your platform already uses for other third-party gateways.

What VORA is

VORA exposes three front-end integration paths (Hosted Checkout, Embedded Fields, and Elements) that all settle through Payment Intents, the server-side engine — see Choose your integration for the side-by-side comparison.

For most platform connectors, Payment Intents is the right integration because its discrete auth → capture → void → refund lifecycle maps 1:1 with the gateway-adapter contracts most platforms already have. The rest of this page assumes Payment Intents as your primary integration; the Sessions and Embedded Fields surfaces are covered where they intersect (3DS, webhooks).

Adapter contract mapping

If your platform's adapter contract has discrete auth/capture/void/refund methods, Payment Intents maps directly:

  • Auth-onlyPOST /v1/payment_intents with capture_method: "manual" → intent reaches authorized
  • CapturePOST /v1/payment_intents/{id}/capture (full or partial via amount_to_capture) → intent reaches succeeded
  • Auth + Capture (one-step charge) → POST /v1/payment_intents with capture_method: "automatic" → intent reaches succeeded
  • VoidPOST /v1/payment_intents/{id}/void (only before capture) → intent reaches voided
  • RefundPOST /v1/refunds (full or partial via amount) → refund returns synchronously with status: succeeded | pending plus async webhook confirmation

If you'd rather use Hosted Checkout, the mapping collapses to a single call:

  • Auth + Capture → one POST /v1/sessions call. Outcome arrives via webhook + signed redirect.
  • Void → not applicable. Sessions that don't complete transition to expired or failed.
  • Refund → initiated from the merchant's dashboard (or upstream); delivered to your webhook as a charge.refunded event.

API surface — what your connector calls

Base URL

  • Production: https://checkout.vonpay.com
  • Sandbox: same base URL; key prefix (vp_sk_test_* vs vp_sk_live_*) selects the environment. Test keys cannot accidentally hit live data and vice versa.

Headers

Required:

  • Authorization: Bearer <vp_sk_*> — the merchant's secret API key
  • Content-Type: application/json

Strongly recommended (server accepts requests without them, but you should always send):

  • Von-Pay-Version: 2026-04-14 — pin the API version. Omitting this header tracks the latest stable, which can change behind you.
  • Idempotency-Key: <unique-string> — see Idempotency below. Not server-enforced today, but every connector should send one to make retries safe.

Endpoints

VerbPathPurpose
Sessions (Hosted Checkout)
POST/v1/sessionsCreate a checkout session. Returns id, checkoutUrl, expiresAt (30-min TTL)
GET/v1/sessions/{id}Retrieve session status — pending / processing / succeeded / failed / expired
Payment Intents (discrete lifecycle)
POST/v1/payment_intentsCreate an intent with capture_method: "automatic" (auth+capture) or "manual" (auth-only)
POST/v1/payment_intents/{id}/captureCapture an authorized intent (full or partial via amount_to_capture)
POST/v1/payment_intents/{id}/voidVoid an authorized intent (before capture)
POST/v1/refundsCreate a refund (full or partial via amount); references the intent via payment_intent
POST/v1/tokensMint a vp_pmt_* payment-method token from an iframe-vault handle
Capability discovery
GET/v1/capabilitiesPer-merchant capability matrix — supported_operations, settlement_currencies, rate_limits (snake_case). Read once at startup; gate optional operations on this.
Public (publishable-key) endpoints — used by Embedded Fields browser SDK
GET/v1/public/sessions/{id}Session lookup with a publishable key (browser-safe)
POST/v1/public/binder-loadPer-processor element capability map
POST/v1/public/tokensBrowser-side vp_pmt_* minting
Operational
GET/api/healthLiveness probe. No auth required.
GET/.well-known/vonpay.jsonDiscovery metadata. No auth required.

The full request/response shapes are in the REST API reference (downloadable OpenAPI spec). For a typed SDK surface, see the Node and Python SDKs — your adapter doesn't need to use them, but they're a working reference for request shapes.

Webhook events your endpoint receives

Configured by the merchant in their dashboard — they register one or more endpoint URLs at /dashboard/developers/webhooks and subscribe each to the event types your platform needs. Your platform's webhook URL receives:

EventWhen fireddata payload
Charges
charge.succeededBuyer completed a hosted-checkout payment OR a direct /v1/payment_intents charge succeededsession_id, transaction_id, amount, currency
charge.failedCharge attempt failedsession_id, transaction_id, amount, currency, failure_reason
charge.refundedA refund settled against a prior charge (full or partial; fires once per refund)session_id, transaction_id, amount, currency
Payment Intents
payment_intent.succeededIntent reached succeeded (auto-capture, manual capture, or post-3DS settle)session_id, transaction_id, amount, currency
payment_intent.failedIntent reached failedsession_id, transaction_id, amount, currency, failure_reason
payment_intent.cancelledIntent was voided before capture (the object's status field reports voided; the event name is cancelled — same terminal state)session_id, transaction_id, amount, currency

Every event is wrapped in the envelope {id, type, created, livemode, merchant_id, data} — full per-event shapes at Webhook Events. New event families (dispute.*, application.*, payout.*) are in the subscription catalog but not in the public platform-integrator surface today.

There is no session.expired / "buyer abandoned" event today — closing this gap is on the roadmap. For abandoned-cart detection in the meantime, poll GET /v1/sessions/{id} after the 30-minute TTL.

Webhook signature format

HMAC-SHA256 over t.<raw-body>, keyed with the per-endpoint whsec_* signing secret minted at endpoint registration.

x-vonpay-signature: t=<unix_seconds>,v1=<lowercase_hex_hmac_sha256>
  • Algorithm: HMAC-SHA256
  • Key: the endpoint's whsec_* secret, raw UTF-8 bytes (do not base64-decode, do not strip the prefix)
  • Message: t + "." + raw_body — the t value from the header concatenated with the raw HTTP request body before any parsing
  • Replay window: asymmetric — reject if now - t > 300 (past) or t - now > 30 (future)
  • Multi-secret: during a rotation window the header carries two v1= entries; accept on any match. Reject headers with more than two v1= entries

Reference verifier code in five languages (Node, Python, Go, Ruby, PHP) is on the Webhook Verification page. Always use a constant-time comparison helper (hmac.compare_digest, crypto.timingSafeEqual, subtle.ConstantTimeCompare, etc.) — variable-time == leaks the secret a byte at a time under repeated-request timing attacks.

3DS handoff

How 3DS surfaces depends on which integration path the merchant uses.

Hosted Checkout (Sessions)

3DS is transparent. The hosted page renders the issuer's challenge inline:

  • On success, the session continues to succeeded and your webhook fires normally.
  • On failure, the session moves to failed with failureCode indicating 3DS rejection.

Your adapter's "3DS challenge" code path typically has nothing to do — the encapsulated flow swallows it. If your adapter contract requires a separate "3DS in progress" state, poll GET /v1/sessions/{id} while the session is pending.

Payment Intents

The connector handles the challenge. When the issuer requires 3DS, the intent returns status: "requires_action" with a next_action block:

{
"type": "redirect_to_url",
"redirect_to_url": { "url": "https://challenge.example/3ds/abc123" }
}

Redirect the buyer to next_action.redirect_to_url.url as a top-level navigation (not inside an iframe — banks block this). After the challenge, learn the outcome via the payment_intent.succeeded / payment_intent.failed webhook (there is no GET /v1/payment_intents/{id} retrieve endpoint). Don't trust the browser's return signal as authoritative.

Embedded Fields + Payment Intents

If the merchant pairs Embedded Fields with Payment Intents, the browser SDK can drive the 3DS modal inline using a client_confirm: { binder, client_secret } block that POST /v1/payment_intents returns on status === "requires_action". The connector forwards client_confirm to the browser; the SDK does the rest. See Embedded Fields — 3D Secure.

Idempotency

Every POST request (/v1/sessions, /v1/payment_intents, /v1/payment_intents/{id}/capture, /v1/payment_intents/{id}/void, /v1/refunds, /v1/tokens) should carry an Idempotency-Key header with a unique-per-operation value (typically your platform's internal order ID + a stable per-attempt suffix). The header is not server-enforced on every endpoint today — requests without it succeed — but a connector that omits it cannot make POST operations safely retryable, which matters under transient network failures.

  • Replaying the same Idempotency-Key returns the original response — including the original checkoutUrl — for as long as the session record is retained (today, effectively the lifetime of the row), not a fixed 24-hour cache. No duplicate session is created.
  • Reusing the same Idempotency-Key with a different request body returns 422 with code=idempotency_replay_incompatible.

Recommended pattern for adapters:

Idempotency-Key: {platform_short_name}_{merchant_internal_order_id}_{attempt_count}

Example: sticky_order-789012_attempt-1. On retry after a transient failure, increment the suffix to get a fresh session; on retry of the same logical operation, keep the suffix to dedupe.

Error code catalog

All errors return JSON with error, code, fix, and docs fields, plus an X-Request-Id header for correlation:

{
"error": "API key is malformed or does not exist",
"code": "auth_invalid_key",
"fix": "Check that your API key is correctly formatted and active",
"docs": "https://docs.vonpay.com/reference/error-codes#auth_invalid_key"
}

The full ErrorCode catalog is at Error Codes. The codes most relevant to a connector:

HTTPCodeCommon adapter handling
401auth_invalid_keySurface to the merchant — their pasted key is wrong or rotated past grace
401auth_key_expiredSurface to the merchant — they need to update the configured key
401auth_merchant_inactiveSurface to the merchant — Von Payments has disabled the account
403merchant_not_onboardedThe merchant hasn't completed KYC — surface a "complete onboarding" link
422merchant_not_configuredSurface to the merchant — payment routing isn't fully set up on their side
400validation_invalid_amountAdapter bug — fix the amount mapping (must be positive integer minor units)
400validation_missing_fieldAdapter bug — required field missing in the request body
409session_wrong_stateIdempotency — the session is in a state that disallows this operation
410session_expiredSession's 30-min TTL elapsed; create a new one
429rate_limit_exceeded / rate_limit_exceeded_per_keyBack off per the Retry-After header. Per-IP bucket is 10 req / 60 s; per-API-key is 30 req / 60 s on session-creates.
502provider_unavailableUpstream payment provider is unreachable. Retry with exponential backoff.
402provider_charge_failedCard declined — surface the decline to the merchant's UI

A connector that handles auth_*, merchant_*, validation_*, and provider_* error families idiomatically is structurally complete. The remaining codes are operational (rate limits, idempotency conflicts, internal errors) and should be retried per standard exponential-backoff practice.

Sandbox

Every Von Payments merchant has a sandbox with deterministic mock outcomes. See Sandbox & Test Mode for the outcome matrix, and the Platform Integrator Sandbox guide for the provisioning walkthrough — keys in under a minute, no KYC.

Reference adapter implementations

A multi-tenant Node.js reference adapter is published at github.com/Von-Payments/vonpay-samples/tree/main/platform-integrator-nextjs (MIT-licensed). It demonstrates the full session lifecycle for a platform serving many merchants:

  • Per-tenant credential lookup (each onboarded merchant has its own vp_sk_* + ss_* stored on the platform side; lib/tenants.ts is shaped like a real DB query)
  • Tenant-scoped session creation with Idempotency-Key and pinned Von-Pay-Version
  • Tenant-scoped return-URL signature verification (uses the right tenant's ss_*)
  • Multi-tenant webhook routing — single /api/webhooks endpoint receives events for all tenants, peeks at merchantId to route, then verifies HMAC with the tenant's vp_sk_*
  • Composite-key event idempotency dedup (Webhooks v1 doesn't emit a top-level event id; the sample composes sessionId:event:timestamp and stores in-memory — swap for Redis in production)
  • A small CRM-style UI with 3 simulated tenants → customers → "Charge $X" flow

The general-purpose Node.js single-merchant samples (checkout-nextjs, checkout-express, checkout-paybylink-nextjs) cover the SDK surface against a single merchant if your platform ships its own multi-tenancy layer.

PHP integrators

The Node adapter at platform-integrator-nextjs documents the patterns to port into a PHP gateway-adapter contract. Use the REST API reference directly for raw HTTP, or use vonpay-checkout (Python SDK) if your platform is Python-based.

Account model

Each merchant of yours is a top-level Von Payments merchant with their own keys, dashboard, and webhook routing — same shape as every gateway integration your platform already supports. The merchant brings their per-merchant credentials; your platform stores them per-merchant; your adapter calls VORA server-to-server with the right merchant's key for each transaction.

Partnership

The technical spec on this page is what your eng team needs to build the connector. Getting it listed in your platform's gateway dropdown is a separate partnership conversation — see Going from sandbox to a partnership.