Wallgent
Guides

Error Handling

Typed SDK errors, machine-readable error codes, retries, and REST error format.

Error Response Format

All API errors follow a consistent JSON envelope regardless of HTTP status code:

{
  "error": {
    "code": "INSUFFICIENT_FUNDS",
    "message": "Wallet balance of 45.00 is less than the requested amount 100.00",
    "details": {
      "walletId": "wal_01J...",
      "balance": "45.00000000",
      "requested": "100.00"
    }
  }
}
FieldTypeDescription
error.codestringMachine-readable error code — use this in your logic
error.messagestringHuman-readable description
error.detailsobjectOptional structured context (varies by error type)

Always check error.code, not error.message — messages may change; codes are stable.


SDK Error Hierarchy

The SDK maps every API error code to a typed class. All error classes extend WallgentError.

import { WallgentError } from '@wallgent/sdk'

// Base class fields available on every error:
err.code        // string  — e.g. 'INSUFFICIENT_FUNDS'
err.message     // string  — human-readable description
err.statusCode  // number  — HTTP status code
err.details     // Record<string, unknown> | undefined

Catching Typed Errors

Use instanceof checks to handle specific error types and fall through to a generic handler for the rest:

import Wallgent, {
  InsufficientFundsError,
  PolicyDeniedError,
  ApprovalRequiredError,
  RateLimitExceededError,
  WalletFrozenError,
  WallgentError,
} from '@wallgent/sdk'

const wg = new Wallgent({ apiKey: process.env.WALLGENT_API_KEY })

try {
  await wg.payments.send({
    from: 'wal_01J...',
    to: 'wal_01J...',
    amount: '50.00',
  })
} catch (err) {
  if (err instanceof InsufficientFundsError) {
    // Top up the wallet or reduce the amount
    console.error('Not enough balance:', err.details)
  } else if (err instanceof PolicyDeniedError) {
    // A spend policy blocked this payment — do not retry
    console.error('Policy blocked payment:', err.message)
  } else if (err instanceof ApprovalRequiredError) {
    // Not an error — payment is queued for human review (HTTP 202)
    const approvalId = err.details?.approvalId as string
    console.log('Awaiting approval:', approvalId)
  } else if (err instanceof WalletFrozenError) {
    // The wallet is frozen — contact your admin
    console.error('Wallet is frozen')
  } else if (err instanceof RateLimitExceededError) {
    // Retryable — wait and try again
    const retryAfter = err.details?.retryAfter as number ?? 60
    console.log(`Rate limited. Retry in ${retryAfter}s`)
  } else if (err instanceof WallgentError) {
    // Unknown Wallgent error — log and surface
    console.error(`Wallgent error ${err.code} (${err.statusCode}):`, err.message)
  } else {
    // Non-Wallgent error (network failure, etc.) — re-throw
    throw err
  }
}

Special Case: ApprovalRequired

ApprovalRequiredError is unique: it corresponds to HTTP 202, not a 4xx error. The payment was accepted and is queued — it is not a failure.

import { ApprovalRequiredError } from '@wallgent/sdk'

try {
  const payment = await wg.payments.send({ ... })
  // HTTP 200 — payment posted immediately
} catch (err) {
  if (err instanceof ApprovalRequiredError) {
    // HTTP 202 — payment queued, do not retry
    const approvalId = err.details?.approvalId as string
    // Track the approval and wait for approval.approved or approval.rejected webhook
  }
}

The payment will execute automatically when approved. Use GET /v1/approvals/:id to poll the status, or subscribe to the approval.approved and approval.rejected webhook events.


Retryable vs Terminal Errors

Not all errors should be retried. Retrying terminal errors (like INSUFFICIENT_FUNDS) will always fail.

Retryable:

CodeWhen to RetryHow
RATE_LIMIT_EXCEEDEDAfter the rate limit windowUse err.details.retryAfter seconds, then retry with exponential backoff
Network errors (non-Wallgent)ImmediatelyRetry with backoff; use idempotencyKey to prevent duplicates

Terminal — do not retry:

CodeReason
INSUFFICIENT_FUNDSBalance will not increase on its own
POLICY_DENIEDPolicy rules are deterministic; same request will always fail
WALLET_FROZENWallet must be unfrozen by an admin before retrying
APPROVAL_REQUIREDNot an error — wait for approval, do not re-send
UNAUTHORIZEDKey is invalid or missing
API_KEY_REVOKEDKey has been revoked — provision a new one

For idempotent retries on payment endpoints, always include an idempotencyKey. If you retry without one, a new transaction is created each time.


All Error Codes

Error CodeHTTP StatusSDK ClassWhen It Occurs
INSUFFICIENT_FUNDS402InsufficientFundsErrorPayment amount exceeds wallet balance
POLICY_DENIED403PolicyDeniedErrorA spend policy blocked the payment
APPROVAL_REQUIRED202ApprovalRequiredErrorPolicy requires human approval; payment is queued
WALLET_FROZEN403WalletFrozenErrorWallet is frozen; no payments allowed
WALLET_NOT_FOUND404WalletNotFoundErrorWallet ID does not exist or is not accessible
WALLET_LIMIT_EXCEEDED403WalletLimitExceededErrorPlan wallet count limit reached
INVALID_AMOUNT400InvalidAmountErrorAmount is zero, negative, or malformed
VALIDATION_ERROR400ValidationErrorRequest body failed schema validation
UNAUTHORIZED401UnauthorizedErrorAPI key is missing or invalid
API_KEY_REVOKED401ApiKeyRevokedErrorAPI key has been revoked or expired
PERMISSION_DENIED403WallgentErrorKey lacks required permission
ENVIRONMENT_MISMATCH403EnvironmentMismatchErrorKey environment does not match resource environment
RATE_LIMIT_EXCEEDED429RateLimitExceededErrorToo many requests; back off and retry
IDEMPOTENCY_KEY_REUSE409IdempotencyKeyReuseErrorSame idempotency key used with different parameters
MERCHANT_NOT_FOUND404MerchantNotFoundErrorMerchant ID or domain not found
MERCHANT_SUSPENDED403MerchantSuspendedErrorMerchant is suspended and cannot receive payments
CLOSED_LOOP_UNAVAILABLE400ClosedLoopUnavailableErrorClosed-loop payment not available for this merchant
TRANSFER_NOT_FOUND404TransferNotFoundErrorTransfer ID does not exist
WEBHOOK_NOT_FOUND404WebhookNotFoundErrorWebhook ID does not exist
POLICY_NOT_FOUND404PolicyNotFoundErrorPolicy ID does not exist
REVERSAL_NOT_ALLOWED422ReversalNotAllowedErrorTransaction cannot be reversed (already settled or reversed)
PAYMENT_LINK_NOT_FOUND404PaymentLinkNotFoundErrorPayment link ID or slug does not exist
BATCH_PARTIAL_FAILURE207BatchPartialFailureErrorBatch payment: some succeeded, some failed
SANDBOX_ONLY403SandboxOnlyErrorEndpoint is only available in sandbox environment
CARD_NOT_FOUND404CardNotFoundErrorCard ID does not exist

REST API Error Handling

If you are calling the REST API directly (without the SDK), check response.ok and parse the error body:

const response = await fetch('https://api.wallgent.com/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ from: 'wal_01J...', to: 'wal_01J...', amount: '50.00' }),
})

if (!response.ok) {
  const body = await response.json()
  const { code, message, details } = body.error

  switch (code) {
    case 'INSUFFICIENT_FUNDS':
      // handle
      break
    case 'POLICY_DENIED':
      // handle
      break
    default:
      throw new Error(`Wallgent API error ${code}: ${message}`)
  }
}

const payment = await response.json()

Note that APPROVAL_REQUIRED returns HTTP 202, so response.ok is true. Check for response.status === 202 separately if you need to detect the approval workflow.

On this page