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 — the API surface mapped against the gateway-adapter shape your platform already uses for other third-party gateways.

What VORA is

VORA exposes two front-end integration paths (Hosted Checkout and Embedded Fields — the latter rendering in embed or elements mode) that both 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) → four 2xx shapes, only one of which moved money: 201+succeeded (money moved), 202+requested (accepted, still in progress), 200+canceled (voided before it left — buyer NOT repaid), 200+idempotent:true (a replay). Branch on status, not the code — a 202 is not a failure and retrying it refunds the buyer twice. See Refunds.

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; the key prefix (vp_sk_test_* vs vp_sk_live_*) selects the environment.

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> — send one on every POST; required on some refund and payment routes, see Idempotency.

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)
GET/v1/payment_intents/{id}Retrieve an intent's persisted state. Use it as a polling fallback when a webhook may have been missed — the outcome is not webhook-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. A plain call answers 200 status: "ok", or 503 status: "error" when the service takes itself out of rotation — that branch alone carries reason and detail. Add ?deep=true to also probe dependencies: 200 "healthy", or 503 "degraded" with a checks map and no reason. Both failure shapes are a 503 and only one has reason — branch on the status code, and treat any non-200 as "do not route traffic".
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.

The events below are the ones a gateway adapter normally maps; the complete list and field-by-field payloads are on Webhook Events.

EventWhen firedMaps to
Charges
charge.succeededBuyer completed a hosted-checkout payment OR a direct /v1/payment_intents charge succeededadapter sale / capture success
charge.failedCharge attempt failedadapter sale / capture decline
charge.refundedA refund settled against a prior charge (full or partial; fires once per refund)adapter refund success
refund.failedA refund did not complete — no money went back to the buyer. The failure twin of charge.refunded; mirrors its payload so one mapping handles both. ⚠️ Delivery is not guaranteed on every deployment, and not every processor connection emits it. Subscribe, but treat an unresolved refund as still open in your own ledger rather than waiting for this event — see Refunds.adapter refund failure
Payment Intents
payment_intent.succeededIntent reached succeeded (auto-capture, manual capture, or post-3DS settle)adapter auth+capture settle
payment_intent.failedIntent reached failedadapter auth decline
payment_intent.cancelledIntent was voided before capture (the object's status field reports voided; the event name is cancelled — same terminal state)adapter void

Every event is wrapped in the envelope {id, type, created, livemode, merchant_id, data}. Each of the seven above carries payment_intent_id in its data — correlate across them on that, not on session_id, which one payment can reuse across two events. (The dispute events do not carry it; they correlate on transaction_id.)

⚠️ On the refund events, read amount_refunded_total for how much of the charge has been returned in total. Do not accumulate amount yourself: what it means varies by processor connection — cumulative on some, per-refund on others — so summing it marks an order fully refunded early on the connections where it is already a running total. See Refunds.

There is no session.expired / "buyer abandoned" event. For abandoned-cart detection, 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)
  • Single secret: the header carries exactly one v1= entry and there is no grace window — after a rotation every delivery is signed with only the new secret, and a 4xx you return is not retried. Accept both secrets during your switch-over, then drop the old one (sequence)

Reference verifier code in five languages (Node, Python, Go, Ruby, PHP) is on the Webhook Verification page. Compare with a constant-time helper (hmac.compare_digest, crypto.timingSafeEqual, subtle.ConstantTimeCompare).

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; your webhook fires. The decline detail arrives as failure_reason / failure_code on the charge.failed / session.failed webhook — a GET /v1/sessions/{id} poll returns status: failed but carries no failure-code field.

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, and use GET /v1/payment_intents/{id} to read the persisted state directly when you need a polling fallback or want to confirm an outcome you may have missed. 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 + the operation name — see the key shape and the retry rule below). On a provider connection that keeps no queryable record of its own, POST /v1/refunds and POST /v1/payment_intents refuse a keyless request with 400 idempotency_key_required; send the header on every connection and the distinction never matters to you.

  • Replaying the same Idempotency-Key returns the original response — including the original checkoutUrl — for as long as the session record is retained — 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}_{operation}

Example: sticky_order-789012_capture. The last segment names which operation on that order this is — capture, refund, refund-2 for a deliberate second refund. It must not carry an attempt or retry counter: retrying is the case the key exists to collapse, so a counter there defeats it.

Reuse the key when you retry — never increment it

A transient failure is a retry of the same logical operation. Sending a fresh key on a money endpoint skips the duplicate check entirely: on POST /v1/refunds that is a second real refund, and on POST /v1/payment_intents a second real charge (plus a second connected-store order).

Change the key only for an operation that is genuinely different — a second, intentional refund of the same payment, or a new order. See Refunds and Connected Platforms.

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/security#key-types"
}

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. The outcome is unknown — retry a charge only with the same Idempotency-Key; keyless, reconcile instead.
402provider_charge_failedCard declined — surface the decline to the merchant's UI

A connector that handles the auth_*, merchant_*, validation_*, provider_* and session_* families is complete — session_expired (410) means create a new session, not retry; the remaining codes are operational (rate limits, idempotency conflicts, internal errors).

Sandbox

Every Von Payments account has test-mode keys that never move real money. What a test request reaches and what decides its outcome is on Test mode & test cards; the Platform Integrator Sandbox guide covers provisioning.

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_* and per-endpoint whsec_* stored on the platform side; lib/tenants.ts is shaped like a real DB query). Do not collect a per-tenant ss_* — nothing in the integration uses it, and it cannot verify a return: returns carry a platform-wide signature. See Handle the return.
  • Tenant-scoped session creation with Idempotency-Key and pinned Von-Pay-Version
  • Return handling through confirmReturn — the redirect is confirmed by an authenticated server-side read of the session, per tenant, rather than by trusting the query string
  • 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 whsec_* webhook signing secret (the endpoint's whsec_, not the vp_sk_ API key)
  • Composite-key event idempotency dedup — outbound events DO carry a top-level id (vp_evt_*); dedupe on that, not on the sample's sessionId:event:timestamp composite; store in 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 conversation with your Von Payments contact.

  • Platform integration runbook — this page is the API surface; the runbook is the build order, the settings screen your merchants fill in, and the evidence we need before listing you
  • Platform Integrator Sandbox — get keys, no KYC, in under a minute
  • Quickstart — the 5-minute walkthrough; the vp_sk_test_* you create there is the same one your connector will exercise
  • Webhook Verification — full HMAC verification scheme + reference code in 5 languages
  • Error Codes — full ErrorCode catalog
  • API Keys — key types, rotation, revocation
  • Sandbox & Test Mode — keys, environments, webhook setup
  • REST API — full request/response shapes + OpenAPI spec