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"
}
}
}| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code — use this in your logic |
error.message | string | Human-readable description |
error.details | object | Optional 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> | undefinedCatching 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:
| Code | When to Retry | How |
|---|---|---|
RATE_LIMIT_EXCEEDED | After the rate limit window | Use err.details.retryAfter seconds, then retry with exponential backoff |
| Network errors (non-Wallgent) | Immediately | Retry with backoff; use idempotencyKey to prevent duplicates |
Terminal — do not retry:
| Code | Reason |
|---|---|
INSUFFICIENT_FUNDS | Balance will not increase on its own |
POLICY_DENIED | Policy rules are deterministic; same request will always fail |
WALLET_FROZEN | Wallet must be unfrozen by an admin before retrying |
APPROVAL_REQUIRED | Not an error — wait for approval, do not re-send |
UNAUTHORIZED | Key is invalid or missing |
API_KEY_REVOKED | Key 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 Code | HTTP Status | SDK Class | When It Occurs |
|---|---|---|---|
INSUFFICIENT_FUNDS | 402 | InsufficientFundsError | Payment amount exceeds wallet balance |
POLICY_DENIED | 403 | PolicyDeniedError | A spend policy blocked the payment |
APPROVAL_REQUIRED | 202 | ApprovalRequiredError | Policy requires human approval; payment is queued |
WALLET_FROZEN | 403 | WalletFrozenError | Wallet is frozen; no payments allowed |
WALLET_NOT_FOUND | 404 | WalletNotFoundError | Wallet ID does not exist or is not accessible |
WALLET_LIMIT_EXCEEDED | 403 | WalletLimitExceededError | Plan wallet count limit reached |
INVALID_AMOUNT | 400 | InvalidAmountError | Amount is zero, negative, or malformed |
VALIDATION_ERROR | 400 | ValidationError | Request body failed schema validation |
UNAUTHORIZED | 401 | UnauthorizedError | API key is missing or invalid |
API_KEY_REVOKED | 401 | ApiKeyRevokedError | API key has been revoked or expired |
PERMISSION_DENIED | 403 | WallgentError | Key lacks required permission |
ENVIRONMENT_MISMATCH | 403 | EnvironmentMismatchError | Key environment does not match resource environment |
RATE_LIMIT_EXCEEDED | 429 | RateLimitExceededError | Too many requests; back off and retry |
IDEMPOTENCY_KEY_REUSE | 409 | IdempotencyKeyReuseError | Same idempotency key used with different parameters |
MERCHANT_NOT_FOUND | 404 | MerchantNotFoundError | Merchant ID or domain not found |
MERCHANT_SUSPENDED | 403 | MerchantSuspendedError | Merchant is suspended and cannot receive payments |
CLOSED_LOOP_UNAVAILABLE | 400 | ClosedLoopUnavailableError | Closed-loop payment not available for this merchant |
TRANSFER_NOT_FOUND | 404 | TransferNotFoundError | Transfer ID does not exist |
WEBHOOK_NOT_FOUND | 404 | WebhookNotFoundError | Webhook ID does not exist |
POLICY_NOT_FOUND | 404 | PolicyNotFoundError | Policy ID does not exist |
REVERSAL_NOT_ALLOWED | 422 | ReversalNotAllowedError | Transaction cannot be reversed (already settled or reversed) |
PAYMENT_LINK_NOT_FOUND | 404 | PaymentLinkNotFoundError | Payment link ID or slug does not exist |
BATCH_PARTIAL_FAILURE | 207 | BatchPartialFailureError | Batch payment: some succeeded, some failed |
SANDBOX_ONLY | 403 | SandboxOnlyError | Endpoint is only available in sandbox environment |
CARD_NOT_FOUND | 404 | CardNotFoundError | Card 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.