Error Codes
Errors from the /v1 API return JSON with error, code, fix, docs, and selfHeal fields, plus an X-Request-Id response header.
{
"error": "Human-readable error message",
"code": "error_code",
"fix": "Suggested action to resolve the error",
"docs": "https://docs.vonpay.com/reference/error-codes#error_code",
"selfHeal": {
"retryable": false,
"nextAction": "no_action",
"llmHint": "Machine-readable guidance for SDKs and agents."
}
}
The selfHeal object is always present in the standard envelope and gives SDKs and agents a machine-readable hint about whether the request is retriable and what to do next. A small number of internal responses (for example a streaming connection that closes with a bare 503) fall outside this envelope and carry neither the JSON body nor the X-Request-Id header.
HTTP Status Codes
| Code | Meaning | Common Causes |
|---|---|---|
| 400 | Bad Request | Invalid request body, missing required fields, validation failure |
| 401 | Unauthorized | Missing or invalid Authorization: Bearer token |
| 404 | Not Found | Session ID doesn't exist |
| 409 | Conflict | Session is in the wrong state (e.g., already completed) |
| 410 | Gone | Session has expired (configurable TTL — 30-minute default) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
Error Codes Reference
| Code | HTTP | Description |
|---|---|---|
auth_missing_bearer | 401 | No Authorization: Bearer header provided |
auth_invalid_key | 401 | API key is malformed or does not exist |
auth_key_expired | 401 | Key has rotated past its grace window |
auth_key_type_forbidden | 403 | Publishable key used on a secret-only endpoint, or sandbox/live mode mismatch |
auth_merchant_inactive | 401 | Merchant account is disabled or suspended |
merchant_not_onboarded | 403 | Live-key creation is blocked until merchant-onboarding review completes |
auth_service_unavailable | 503 | Authentication service is temporarily unavailable |
session_not_found | 404 | Session ID does not exist |
session_expired | 410 | Session expired (TTL elapsed), or a terminal session with no successful charge — create a new one |
session_already_completed | 409 | A completion call hit an already-succeeded session — the buyer was charged once; do not retry or create a new session (would double-charge). Read the result via webhook or session retrieve |
session_wrong_state | 409 | Session is in the wrong state for this operation |
session_integrity_error | 500 | Internal session state mismatch — contact support |
validation_error | 400 | Request body failed schema validation |
validation_missing_field | 400 | A required field is missing from the request body |
validation_invalid_amount | 400 | Amount is not a positive integer or exceeds maximum |
merchant_not_configured | 422 | Merchant is missing required configuration (e.g., payment provider credentials) |
binder_unavailable | 409 | Merchant has no live payment provider configured and is not in sandbox mode — complete onboarding or use a test-mode key |
capability_not_supported | 422 | The merchant's payment provider does not support the requested operation — check GET /v1/capabilities |
rate_limit_exceeded | 429 | IP-axis rate limit — retry after the Retry-After interval |
rate_limit_exceeded_per_key | 429 | Per-API-key rate limit (30 session-creates/min) — contact support if you need a higher ceiling |
provider_unavailable | 502 | Upstream payment provider is not responding |
provider_attestation_failed | 403 | Payment provider rejected the session-bound attestation |
provider_charge_failed | 402 | Card declined or charge rejected by the upstream provider |
provider_request_rejected | 422 | Payment provider rejected the request as invalid before any money moved (not a decline, not an outage) — fix the offending field and retry |
internal_error | 500 | Unexpected server error |
webhook_missing_signature | 401 | Inbound provider webhook is missing its signature header |
webhook_invalid_signature | 401 | Webhook signature does not match the expected value |
webhook_not_configured | 503 | Webhook verification secret is not configured on the server |
webhook_test_delivery_failed | 502 | A synchronous webhook test-send probe could not be delivered — retriable |
origin_forbidden | 403 | Internal endpoint called from outside the checkout page |
transaction_verification_failed | 403 | Transaction could not be verified with the payment provider |
unsupported_media_type | 415 | Content-Type header is missing or not application/json |
endpoint_not_implemented | 501 | A payment-intent operation is not yet implemented for this merchant's provider (capability gate) |
idempotency_replay_incompatible | 422 | Idempotency key was reused with a different request body |
invalid_transition | 409 | Payment intent is in a state that forbids the requested operation (e.g. capture on succeeded) |
refund_intent_not_refundable | 422 | The parent payment intent is not in a refundable state (refunds require status: "succeeded") |
refund_amount_exceeds_remaining | 422 | Requested refund amount is greater than the remaining refundable balance |
refund_currency_mismatch | 422 | Refund currency does not match the parent intent's currency |
payment_method_required | 422 | This merchant's payment provider requires a vaulted payment method on the request |
payment_method_not_found | 404 | The payment_method.id does not exist or does not belong to this merchant |
This table covers the error codes you are most likely to encounter; the full catalog is larger and continues to grow.
Rate-limit buckets are documented on the Rate Limits page.
Validation Errors (400)
Validation errors include a descriptive message from the schema validator. The error field carries the raw validator output — a JSON array of issue objects, each with the failing field's path and a message:
{
"error": "[\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"amount\"\n ],\n \"message\": \"Invalid input: expected number, received string\"\n }\n]",
"code": "validation_error",
"fix": "Ensure 'amount' is a positive integer in minor units (e.g., 1499 for $14.99)",
"docs": "https://docs.vonpay.com/reference/error-codes#validation_error"
}
Common validation issues:
| Field | Rule |
|---|---|
amount | Must be a positive integer (1–99,999,999) |
currency | Must be exactly 3 characters |
country | Must be exactly 2 characters |
successUrl | Must be HTTPS (localhost exempt in sandbox/test mode) |
lineItems | Max 100 items |
metadata values | Max 500 characters each |
For POST /v1/sessions, currency is always required. amount is required for payment-mode sessions (the default); a setup-mode (card-on-file) session may omit it.
Debugging
Every /v1 response includes X-Request-Id (success or error). When contacting support, include this ID for fast issue resolution.
X-Request-Id: CezuRjYK_sos
The value is a short URL-safe identifier (mixed-case letters, digits, -, and _) on most routes; webhook-subscription and webhook-event routes return a full UUID instead.
Per-code reference
Each error code emitted in a response body's docs field links to its section below. Each section gives the HTTP status, the cause, and the fix.
auth_missing_bearer
HTTP: 401. The request did not include an Authorization: Bearer <key> header. Add the header with your vp_sk_* or vp_pk_* key. (A legacy vp_key_* key, treated as a secret key, is also accepted.)
auth_invalid_key
HTTP: 401. The API key is malformed, unknown, or has been revoked. Check the prefix (vp_sk_test_, vp_sk_live_, vp_pk_test_, vp_pk_live_) and confirm the key exists in /dashboard/developers/api-keys. If you just rotated, double-check the grace window hasn't expired.
auth_key_expired
HTTP: 401. The key has rotated past its grace window (default 24 hours, configurable per rotation). Distinct from auth_invalid_key so SDKs can detect rotation and fetch a fresh key instead of failing the request. Get a fresh key from /dashboard/developers/api-keys.
auth_key_type_forbidden
HTTP: 403. Primary cause: a publishable key (vp_pk_*) used against a secret-only endpoint like GET /v1/sessions/:id. Also fires on sandbox/live-mode mismatches — for example, a sandbox key attempting to create a live payment session. The fix field on the response tells you exactly what to switch to.
merchant_not_onboarded
HTTP: 403. You tried to create a live-mode API key, but your merchant account has not yet completed onboarding review, so live-key issuance is blocked. Test-mode keys (vp_sk_test_*) remain self-serve at any time via /dashboard/developers → Create sandbox; live keys can only be minted once your account reaches the ready-to-transact (or live) state. If review is still in progress, check /dashboard/settings for your application status and any outstanding requirements; if your application was declined, contact Von Payments support with the details provided in your denial notice. Distinct from auth_merchant_inactive (401), which fires on a previously-approved account that has since been deactivated.
auth_merchant_inactive
HTTP: 401. The merchant account has been disabled or suspended. Check /dashboard for status banners; contact support if unexpected.
auth_service_unavailable
HTTP: 503. The authentication service is temporarily unavailable. This is retriable — the SDK auto-retries with backoff.
session_not_found
HTTP: 404. The session ID does not exist. Sessions are scoped to the merchant; you cannot look up another merchant's session with your key. Confirm the ID was created with the same key mode you're now querying with.
session_expired
HTTP: 410. The session expired (TTL configurable at create — default 30 minutes, range 5 minutes to 7 days), or it reached a terminal state with no successful charge (failed / cancelled). Create a new session. (If the session already succeeded, you get session_already_completed — HTTP 409 — instead; see below.)
session_already_completed
HTTP: 409. A completion call (POST /v1/public/tokens or …/confirm) — or a session retrieve — hit a session that already succeeded. The buyer was charged exactly once and it's recorded. This is a duplicate or late call (a retry, double-submit, or page reload). Do not retry, and do not create a new session for the same purchase — either would charge the buyer again. Treat it as success: read the result from your payment_intent.succeeded / charge.succeeded webhook or GET /v1/public/sessions/:id (status succeeded). This is a distinct 409 Conflict (the session is in a terminal succeeded state) — not session_expired's 410 Gone — so even a status-only client won't "start over" into a re-charge. Branch on the code for the remediation.
session_wrong_state
HTTP: 409. The session is in a state that forbids this operation (e.g. the session already succeeded and cannot be cancelled). Read the response body — the fix field describes the allowed state transitions.
session_integrity_error
HTTP: 500. Internal session state mismatch. Rare; indicates session metadata in the database no longer matches an invariant the runtime expects. Capture the X-Request-Id and contact support — this is not safely retriable without investigation.
validation_error
HTTP: 400. The request body failed schema validation. The response error field contains the validator's output — a JSON array of issue objects, each carrying the path to the bad field (for example path: ["amount"]) and a message such as Invalid input: expected number, received string.
validation_missing_field
HTTP: 400. A required field is missing. See Create a Session for the required fields.
validation_invalid_amount
HTTP: 400. Amount is not a positive integer, is zero, or exceeds the 99,999,999 maximum. Remember: amounts are in minor units — 1499 = $14.99, not $1,499.
merchant_not_configured
HTTP: 422. The merchant has not completed onboarding for this operation — usually payment-provider credentials are not yet provisioned. Complete boarding via the merchant dashboard, or contact support.
binder_unavailable
HTTP: 409. The merchant has no live payment provider configured and is not in sandbox mode, so the requested operation cannot be routed to a provider. The merchant (not the integrator) must complete onboarding to attach a payment provider. To keep building in the meantime, switch to a sandbox API key (vp_sk_test_*) — test mode runs without a configured live provider. Not retriable as-is.
capability_not_supported
HTTP: 422. The merchant's payment provider does not support the operation you invoked — for example partial refunds, ACH, or network tokens — even though the route exists. Call GET /v1/capabilities for the merchant's supported_operations matrix and gate capability-dependent calls on it. The matrix is provider-dependent; an operation supported on one provider may not be on another, and switching providers requires re-onboarding. Not retriable without a different capability or provider.
rate_limit_exceeded
HTTP: 429. The generic rate-limit code, emitted on the IP-axis limiter (and the global primary limiter). Retry after the Retry-After interval. The SDK auto-retries up to maxRetries times. The per-API-key axis has its own distinct code, rate_limit_exceeded_per_key.
rate_limit_exceeded_per_key
HTTP: 429. Per-API-key bucket exceeded on POST /v1/sessions (30 session creates/min). A single key should not exceed this under normal traffic. If your integration legitimately does, contact support for a ceiling increase. Distinct from rate_limit_exceeded so SDKs can tell them apart.
provider_unavailable
HTTP: 502. Upstream payment provider is not responding. Retriable — the SDK auto-retries with backoff.
provider_attestation_failed
HTTP: 403. The payment provider rejected the session-bound attestation token during the charge step. The most common cause is amount or scope drift between session create and complete — the attested amount or scope no longer matches the session. Fix: verify the session amount matches the attestation payload, or create a new session and re-attest. Not safely retriable without investigation when the cause is a scope / integrity mismatch — capture the X-Request-Id before retrying. The specific provider-native reason (e.g. ATTESTATION_EXPIRED, ATTESTATION_INVALID, ATTESTATION_MERCHANT_MISMATCH) is included in the error message so the buyer can be shown a more specific hint in your UI.
provider_charge_failed
HTTP: 402. The upstream payment provider returned a terminal charge failure — card declined, insufficient funds, fraud-rule block, or a network-side decline (issuer, scheme, or processor). Fix: prompt the buyer to try a different payment method. Retrying the same card/session is unlikely to succeed and may trigger additional issuer-side flags. This is distinct from provider_unavailable (transient infrastructure) and transaction_verification_failed (post-charge reconciliation mismatch).
provider_request_rejected
HTTP: 422. The merchant's payment provider rejected the request as invalid before any money movement was attempted — this is neither a card decline (provider_charge_failed, 402) nor a transient outage (provider_unavailable, 502). It is a permanent request-validation failure: retrying the identical body will fail again. Inspect the fields a provider most commonly constrains beyond our own schema — currency (must be enabled on the merchant's provider account), amount (within the provider's minimum/maximum for that currency), and any statement-descriptor or metadata values (provider length/character limits) — then retry with corrected values. If every charge on this merchant fails this way, the merchant's provider configuration is likely incomplete; capture the X-Request-Id and escalate. Not retriable as-is.
internal_error
HTTP: 500. Unexpected server error. Capture the X-Request-Id and contact support.
webhook_missing_signature
HTTP: 401. An inbound provider webhook arrived without the provider's expected signature header. This error is internal to Von Payments' inbound webhook handling, not something your endpoint emits. (The signature on webhooks Von Payments sends to you is the x-vonpay-signature header — verify it with the SDK's webhooks.verifySignature; see below.)
webhook_invalid_signature
HTTP: 401. Webhook signature does not match the expected HMAC. When verifying webhooks Von Payments delivers to you, HMAC the raw request body (not the parsed JSON), using your endpoint or subscription signing secret (whsec_…) — not your API key. See Webhook Signature Verification.
webhook_not_configured
HTTP: 503. Webhook verification secret is not configured on the Von Payments server side. This is an infra-level misconfiguration, not a merchant-side issue. Capture the X-Request-Id and contact support.
webhook_test_delivery_failed
HTTP: 502. A synchronous webhook test-send probe ran but the delivery could not be completed — a transport error or timeout reaching your subscription endpoint. This is retriable: retry with exponential backoff starting at 3 seconds, and if it persists, confirm the subscription endpoint is reachable. Note the distinct outcome: if your endpoint is reached but returns a non-2xx, that comes back as a 200 with delivered: false and the endpoint's response_status — not this code.
origin_forbidden
HTTP: 403. The endpoint is internal and only callable from the hosted checkout page; an external caller hit it directly. If you are building a server-side integration, use the public API (e.g. POST /v1/sessions) instead of the internal checkout endpoints.
transaction_verification_failed
HTTP: 403. Transaction could not be verified with the payment processor. Contact support with the X-Request-Id — this is not safely retriable without investigation (a transaction either exists on the processor or it doesn't).
unsupported_media_type
HTTP: 415. The Content-Type header is missing or not application/json. This check is enforced on the browser-facing public endpoints — POST /v1/public/tokens and the public confirm endpoint — so set Content-Type: application/json when calling them. (A trailing ; charset=… parameter is tolerated.)
endpoint_not_implemented
HTTP: 501. The requested payment-intent operation is not yet available for this merchant's payment provider. Capability-gated — read client.capabilities.get() and check supportedOperations before invoking optional operations like paymentIntents.capture, paymentIntents.void, or refunds.create with partial amounts. The capability matrix is provider-dependent; an operation that works on one provider may not work on another. Available in SDK 0.5.0+. Not retriable — the operation will not succeed without a different capability or provider.
idempotency_replay_incompatible
HTTP: 422. The same Idempotency-Key was sent with a different request body. For payment-intent creation, the server compares the new request against the original on amount, currency, and capture_method; if any of those differ, the replay is rejected to prevent silent state corruption. Either retry with the original values or generate a fresh idempotency key for the new request. Available in SDK 0.5.0+.
invalid_transition
HTTP: 409. The payment intent is in a state that forbids the requested operation — e.g., trying to capture an intent that's already succeeded, or void one that's already voided. The error envelope carries current_status (the intent's actual state) + reject_reason — a discriminator the SDK exposes as currentStatus / rejectReason, with values such as already_captured, already_voided, not_authorized, and terminal_state — so the SDK / agent can branch without a follow-up retrieve. Available in SDK 0.6.0+. Not retriable as-is — fix the request (e.g., call refunds.create instead of paymentIntents.void once the intent has captured).
refund_intent_not_refundable
HTTP: 422. The payment intent named in payment_intent is not in a refundable state — refunds require the parent intent to have status: "succeeded". An intent in requires_action / authorized / captured / failed / voided cannot be refunded: call /capture or /void instead, or — if the intent failed — no refund applies because no money moved. Check the parent intent's current status before retrying. Not retryable as-is.
refund_amount_exceeds_remaining
HTTP: 422. The requested refund amount is greater than what's left to refund on this intent. The error envelope carries remaining_refundable (the captured amount minus what's already been refunded) so the SDK / agent can re-request with a valid amount, or you can omit amount to refund the full remaining balance. Fix the input and retry.
refund_currency_mismatch
HTTP: 422. The currency in the refund request differs from the parent intent's currency. Refunds always settle in the original capture currency — either drop the currency field (it defaults to the intent's currency) or supply the matching ISO-4217 code. Fix the input and retry.
payment_method_required
HTTP: 422. This merchant's payment provider requires a previously-vaulted payment method on every charge, but the request omitted one. Use the two-step flow: (1) POST /v1/tokens with the buyer's payment data to vault it, then (2) POST /v1/payment_intents with payment_method: { id: <vp_pmt_*> }. Some providers also support a browser-side confirmation flow (omit payment_method, then confirm in the buyer's browser) — call GET /v1/capabilities to see whether the merchant's current provider supports that path. Not retriable as-is.
payment_method_not_found
HTTP: 404. The payment_method.id is unknown to this merchant. Verify the id was not truncated, that it was created against the same merchant whose API key you're using, and that the test/live mode matches the key prefix. Payment-method ids are scoped to the merchant; you cannot reference another merchant's token. Not retriable as-is — fix the id and retry.
Decline reasons (failure_code)
A declined charge surfaces two ways:
- Synchronously — the API returns
402provider_charge_failed. - Asynchronously — a
charge.failedorpayment_intent.failedwebhook fires, carrying three fields (allstring | null):failure_code— the normalized decline reason to branch on.failure_reason— a human-readable summary to show the buyer.network_decline_code— the raw issuer code (e.g.05,51); for logging/analytics only.
Branch on failure_code. It is normalized to a fixed vocabulary of values. The full set and recommended handling:
failure_code | Meaning | Retry the same card? | Recommended next step |
|---|---|---|---|
card_declined | Generic issuer decline, no specific reason given. | Unlikely to help | Prompt for a different payment method. |
insufficient_funds | The card lacks available funds. | Not now | Suggest a different card; the same card may succeed later. |
expired_card | The card has passed its expiry date. | No | Ask the buyer to re-enter a valid card. |
incorrect_cvc | The CVC/security code was wrong. | Maybe | Ask the buyer to re-enter the security code. |
incorrect_zip | The billing postal code did not match. | Maybe | Ask the buyer to re-enter the billing ZIP/postal code. |
card_velocity_exceeded | The card hit an issuer velocity limit. | Not now | Suggest a different card or trying again later. |
fraudulent | The issuer or a fraud rule blocked the charge. | No | Show a generic decline message — do not reveal the fraud signal. Suggest a different method or contacting the issuer. |
stolen_card | The card was reported stolen. | No | Show a generic decline message; suggest a different method. |
lost_card | The card was reported lost. | No | Show a generic decline message; suggest a different method. |
do_not_honor | The issuer declined without a specific reason. | Unlikely to help | Prompt for a different payment method or contacting the issuer. |
issuer_unavailable | The issuer could not be reached. | Yes | Safe to retry shortly; if it persists, try a different method. |
processing_error | A transient processing failure. | Yes | Safe to retry; if it persists, try a different method. |
generic_decline | An unmapped or unrecognized raw decline. | Unlikely to help | Prompt for a different payment method. |
Show failure_reason to the buyer so they can act on it; keep network_decline_code out of buyer-facing copy. The failure_code vocabulary above is fixed; an unmapped raw code is normalized to generic_decline before it reaches you, so you will always receive one of these values.
Trigger each of these deterministically with the test cards. Note that the 3-D Secure / fraud decline test card surfaces failure_code as fraudulent on the wire.