Errors

The structured error object and every error type VINR returns.

View as Markdown

Every failed VINR API call returns a consistent, machine-readable error object alongside a meaningful HTTP status code. This page documents that object, the full set of error types and codes, and patterns for handling them reliably in production.

HTTP status codesAsk

VINR follows conventional HTTP semantics. The status code tells you the class of failure; the JSON body tells you the specifics.

StatusMeaningRetry?
200 / 201Success
400Invalid request — malformed JSON or failed validationNo (fix the request)
401Authentication failed — missing or invalid API keyNo
402Payment required — the underlying charge was declinedMaybe (depends on decline reason)
403Forbidden — key lacks permission for this resourceNo
404Resource not foundNo
409Conflict — idempotency key reused with a different payloadNo
422Unprocessable — request is valid but cannot be fulfilled in the current stateNo
429Rate limited — too many requestsYes (with backoff)
500 / 502 / 503Server error on VINR's sideYes (with backoff)

A 2xx status always means the request succeeded. VINR never returns 200 with an error body, so you can branch on the status code before parsing.

Error objectAsk

Failed responses return a single top-level error object. The fields are stable and safe to switch on programmatically.

{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "The card was declined due to insufficient funds.",
    "param": "payment_method",
    "doc_url": "https://docs.vinr.com/docs/api-reference/errors#card_declined",
    "request_id": "req_8Fq2zX1m4Kd",
    "resource": "pay_3Nf0kLp9aQ"
  }
}

Prop

Type

Error types and codesAsk

The type field groups errors into categories. The code field is the value you should branch on.

typeCommon code valuesStatus
authentication_errorinvalid_api_key, expired_api_key401
invalid_request_errorresource_missing, unknown_parameter, livemode_mismatch400 / 404
validation_errorparameter_missing, parameter_invalid, amount_too_small400
card_errorcard_declined, expired_card, incorrect_cvc, processing_error402
idempotency_erroridempotency_key_in_use, idempotency_payload_mismatch409
rate_limit_errortoo_many_requests429
api_errorinternal_error, service_unavailable5xx

Card decline codes

When code is card_declined, inspect decline_code to decide whether a retry could ever succeed.

decline_codeMeaningCustomer action
insufficient_fundsNot enough balanceTry another card
do_not_honorIssuer rejected without detailContact issuing bank
lost_card / stolen_cardCard reported compromisedUse a different card
authentication_required3DS challenge neededComplete authentication, then retry
expired_cardCard past expiryUpdate card details

Never surface decline_code or the raw message directly to a cardholder. Issuer reason codes are intentionally vague to prevent fraud probing. Show a generic "Your card was declined" message and prompt for an alternative method.

Handling errorsAsk

The SDK throws a typed VinrError you can narrow on type and code. The example below distinguishes permanent failures from retryable ones.

import { Vinr, VinrError } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

async function charge() {
  try {
    return await vinr.payments.create({
      amount: 1000,
      currency: 'EUR',
      returnUrl: 'https://shop.example.com/return',
    });
  } catch (err) {
    if (err instanceof VinrError) {
      switch (err.type) {
        case 'card_error':
          // Surface a generic message; log the precise reason.
          console.warn(`Declined (${err.decline_code}) req=${err.request_id}`);
          throw new Error('Your card was declined. Please try another method.');
        case 'rate_limit_error':
        case 'api_error':
          // Transient — caller should retry with backoff.
          throw err;
        default:
          // Programming or auth error — do not retry.
          console.error(`Unrecoverable: ${err.code} (${err.message})`);
          throw err;
      }
    }
    throw err;
  }
}

Retrying safely

Only retry 429 and 5xx responses, and always retry with the same idempotency key so VINR can deduplicate. Use exponential backoff with jitter.

async function withRetry<T>(fn: () => Promise<T>, max = 4): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const retryable =
        err instanceof VinrError &&
        (err.type === 'rate_limit_error' || err.type === 'api_error');
      if (!retryable || attempt >= max) throw err;
      const delay = Math.min(2 ** attempt * 200, 4000) + Math.random() * 200;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

The request_id is your fastest path to a resolution. When you open a support ticket, include it verbatim — it lets us trace the exact call through our logs without you sharing payloads.

Next stepsAsk

Was this page helpful?