Skip to main content

Error Handling

The Conto SDK provides detailed error information to help you handle failures gracefully.

ContoError Type

All SDK errors include code and status properties alongside the standard Error fields:
interface ContoError extends Error {
  code: string;    // Error code (e.g., 'PAYMENT_DENIED')
  status: number;  // HTTP status code
  message: string; // Human-readable message
}
ContoError is a TypeScript interface, not a class. The SDK attaches code and status to standard Error objects. Use property checks ('code' in error) instead of instanceof to detect SDK errors.

Error Codes

Authentication Errors

CodeStatusDescriptionSolution
AUTH_FAILED401Invalid or expired API keyCheck key is correct and not revoked
EXPIRED_KEY401Key has expiredGenerate a new SDK key
INSUFFICIENT_SCOPE403Key lacks required permissionUse key with required scope

Payment Errors

CodeStatusDescriptionSolution
PAYMENT_DENIED403Payment blocked by policyCheck violations for details
REQUIRES_APPROVAL202Needs manual approvalWait for human approval
INSUFFICIENT_BALANCE400Wallet has insufficient fundsFund the wallet
DAILY_LIMIT_EXCEEDED400Daily limit exceededWait for reset or increase limit
PER_TX_LIMIT_EXCEEDED400Amount exceeds per-tx limitReduce amount or increase limit
EXPIRED400Payment request expiredRequest new approval
NOT_FOUND404Request ID not foundCheck the request ID
INVALID_STATUS400Cannot execute in current statusCheck payment status
NO_WALLET400No wallet assignedLink a wallet to the agent
EXTERNAL_WALLET400Cannot execute external wallet via /executeUse /confirm with your own txHash
POLICY_DENIED400Payment denied by policy (enriched)Check context.nextSteps
EXTERNAL_WALLET_ERROR400No eligible wallet with balance for this payment. The response includes alternatives showing other wallets.
WALLET_CONFIG_ERROR400Wallet is misconfigured (e.g., missing custody provider). Contact your admin.
CUSTODY_NOT_CONFIGURED500The custody provider (Sponge or Privy) is not configured on the server.
MANUAL_EXECUTION_REQUIRED400External or smart contract wallets cannot be auto-executed. Use the approve/confirm flow instead.
REQUEST_EXPIRED400The payment request expired before execution. Re-request.
ALREADY_EXECUTED400This payment request was already executed. Check transaction status instead.

Validation Errors

CodeStatusDescriptionSolution
VALIDATION_ERROR400Invalid request bodyCheck request parameters
INVALID_AMOUNT400Amount must be positiveUse a positive number
INVALID_ADDRESS400Malformed wallet addressUse valid 0x address (EVM) or base58 address (Solana)
INVALID_JSON400Malformed request bodySend valid JSON

System Errors

CodeStatusDescriptionSolution
RATE_LIMITED429Too many requestsWait and retry
TIMEOUT0Request timeoutRetry with longer timeout
INTERNAL_ERROR500Server errorRetry or contact support

Handling Errors

Basic Error Handling

import { Conto } from '@conto/sdk';
import type { ContoError } from '@conto/sdk';

try {
  const result = await conto.payments.pay({
    amount: 100,
    recipientAddress: '0x...'
  });
} catch (error) {
  if (error instanceof Error && 'code' in error) {
    const sdkError = error as ContoError;
    console.error(`Error [${sdkError.code}]:`, sdkError.message);
    console.error('HTTP Status:', sdkError.status);
  } else {
    console.error('Unexpected error:', error);
  }
}

Handling Specific Error Codes

try {
  await conto.payments.pay({ ... });
} catch (error) {
  if (error instanceof Error && 'code' in error) {
    switch (error.code) {
      case 'PAYMENT_DENIED':
        console.log('Payment was denied by policy');
        // Check why it was denied
        break;

      case 'REQUIRES_APPROVAL':
        console.log('Payment needs human approval');
        // Notify approvers
        break;

      case 'INSUFFICIENT_BALANCE':
        console.log('Wallet needs funding');
        // Alert treasury team
        break;

      case 'DAILY_LIMIT_EXCEEDED':
        console.log('Daily limit reached');
        // Wait until tomorrow or increase limit
        break;

      case 'RATE_LIMITED':
        console.log('Too many requests, waiting...');
        // Check retryAfter header
        break;

      default:
        console.error('Unhandled error:', error.code);
    }
  }
}

Enriched Error Responses

Some SDK error responses include additional context to help agents recover programmatically. These enriched errors include hint, context, and nextSteps fields.

Error Response Structure

interface EnrichedError {
  error: string;           // Human-readable message
  code: string;            // Machine-readable code
  hint?: string;           // Actionable suggestion
  details?: object;        // Violation details (for POLICY_DENIED)
  context?: {
    wallets?: Array<{      // Available wallet balances
      id: string;
      address: string;
      chainId: string;
      custodyType: string;
      balance: number;
    }>;
    alternatives?: Array<{  // Executable wallet alternatives (for EXTERNAL_WALLET)
      walletId: string;
      address: string;
      custodyType: string;
      chainId: string;
      balance: number;
      reason: string;
    }>;
    nextSteps?: string[];   // Suggested recovery actions
  };
}

Example: External Wallet Error

When trying to /execute a payment assigned to an external wallet:
{
  "error": "External wallets cannot use /execute. Use /confirm with your own transaction hash instead.",
  "code": "EXTERNAL_WALLET",
  "hint": "Executable wallets are available. Re-submit the payment request with a specific walletId, or use /confirm for external wallets.",
  "context": {
    "alternatives": [
      {
        "walletId": "wal_abc123",
        "address": "0x...",
        "custodyType": "PRIVY",
        "chainId": "42431",
        "balance": 500.00,
        "reason": "PRIVY wallet with sufficient balance"
      }
    ],
    "nextSteps": [
      "Re-request payment with walletId of an executable wallet",
      "Or execute externally and call POST /api/sdk/payments/{id}/confirm with txHash"
    ]
  }
}

Example: Insufficient Balance Error

{
  "error": "Insufficient wallet balance",
  "code": "INSUFFICIENT_BALANCE",
  "hint": "Requested $100 but max available balance is $45. Try a smaller amount or fund the wallet.",
  "context": {
    "nextSteps": [
      "Reduce the payment amount",
      "Fund the wallet with more stablecoins",
      "Use GET /api/sdk/setup to check current balances"
    ]
  }
}

Handling Enriched Errors

try {
  await fetch(`/api/sdk/payments/${id}/execute`, { method: 'POST', ... });
} catch (error) {
  const body = await error.json();

  if (body.code === 'EXTERNAL_WALLET' && body.context?.alternatives?.length) {
    // Re-request with an executable wallet
    const alt = body.context.alternatives[0];
    console.log(`Retrying with ${alt.custodyType} wallet: ${alt.address}`);
  }

  if (body.context?.nextSteps) {
    console.log('Suggested actions:', body.context.nextSteps);
  }
}

Using Request/Execute for Better Control

The pay() method throws on denial. For more control, use separate request/execute:
// Request first (never throws for denied payments)
const request = await conto.payments.request({
  amount: 100,
  recipientAddress: '0x...'
});

if (request.status === 'DENIED') {
  console.log('Denied reasons:', request.reasons);
  console.log('Violations:', request.violations);
  return null;
}

if (request.status === 'REQUIRES_APPROVAL') {
  console.log('Awaiting approval...');
  return { pending: true, requestId: request.requestId };
}

// Only execute if approved
try {
  return await conto.payments.execute(request.requestId);
} catch (error) {
  // Handle execution errors
  if (error.code === 'INSUFFICIENT_BALANCE') {
    // Balance changed between request and execute
  }
}

Handling Rate Limits

When rate limited, the SDK throws with retryAfter information:
async function payWithRetry(params: PaymentRequestInput) {
  const maxRetries = 3;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await conto.payments.pay(params);
    } catch (error) {
      if (error.code === 'RATE_LIMITED' && i < maxRetries - 1) {
        const waitTime = error.retryAfter || 5;
        console.log(`Rate limited. Waiting ${waitTime}s...`);
        await new Promise(r => setTimeout(r, waitTime * 1000));
        continue;
      }
      throw error;
    }
  }
}

Handling Timeouts

For long-running requests, handle timeouts:
const conto = new Conto({
  apiKey: process.env.CONTO_API_KEY!,
  timeout: 60000  // 60 seconds
});

try {
  await conto.payments.pay({ ... });
} catch (error) {
  if (error.code === 'TIMEOUT') {
    console.log('Request timed out');
    // Don't automatically retry payments - check status first!
    const status = await conto.payments.status(requestId);
    if (status.transaction) {
      console.log('Payment was actually submitted:', status.transaction.txHash);
    }
  }
}

Retry Strategy

Built-in Automatic Retry

The SDK automatically retries transient failures with exponential backoff. You don’t need to implement retry logic yourself for most cases. Built-in behavior:
  • Retries up to 3 times on 429 (rate limited) and 5xx (server errors)
  • Respects Retry-After headers from the server
  • Exponential backoff: 1s → 2s → 4s (capped at 10s)
  • Does not retry client errors (4xx except 429) or auth failures
// The SDK handles retries automatically — just call the method
const result = await conto.payments.pay({
  amount: 100,
  recipientAddress: '0x...'
});
// If the server returns 429 or 503, the SDK waits and retries automatically

Custom Retry for Application Logic

For application-level retry logic (e.g., re-requesting after a denial), use a custom wrapper:
async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  let lastError: Error;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      // Don't retry certain errors
      if (error instanceof Error && 'code' in error) {
        const sdkError = error as ContoError & { retryAfter?: number };
        if (['AUTH_FAILED', 'PAYMENT_DENIED', 'VALIDATION_ERROR'].includes(sdkError.code)) {
          throw error;  // Non-retryable
        }

        if (sdkError.code === 'RATE_LIMITED') {
          const delay = (sdkError.retryAfter ?? 0) * 1000 || baseDelay * Math.pow(2, i);
          await new Promise(r => setTimeout(r, delay));
          continue;
        }
      }

      // Exponential backoff for other errors
      const delay = baseDelay * Math.pow(2, i);
      await new Promise(r => setTimeout(r, delay));
    }
  }

  throw lastError!;
}

// Usage for application-level retries
const result = await withRetry(() => conto.payments.pay({
  amount: 100,
  recipientAddress: '0x...'
}));

Logging Errors

async function loggedPayment(params: PaymentRequestInput) {
  try {
    const result = await conto.payments.pay(params);
    console.log('Payment successful', {
      txHash: result.txHash,
      amount: result.amount
    });
    return result;
  } catch (error) {
    console.error('Payment failed', {
      code: error.code,
      message: error.message,
      params: {
        amount: params.amount,
        recipient: params.recipientAddress,
        purpose: params.purpose
      }
    });
    throw error;
  }
}

Best Practices

Never let payment errors crash your application:
// Bad
await conto.payments.pay({ ... });

// Good
try {
  await conto.payments.pay({ ... });
} catch (error) {
  // Handle gracefully
}
Don’t just catch generic errors:
// Bad
catch (error) {
  console.log('Something went wrong');
}

// Good
catch (error) {
  if (error.code === 'INSUFFICIENT_BALANCE') {
    // Specific handling
  }
}
Be careful with retries on payment execution:
// Dangerous - might double-pay
await withRetry(() => conto.payments.execute(requestId));

// Safe - check status first
const status = await conto.payments.status(requestId);
if (!status.transaction) {
  await conto.payments.execute(requestId);
}
Always log error details for debugging:
catch (error) {
  console.error('Payment error', {
    code: error.code,
    status: error.status,
    message: error.message
  });
}

Next Steps

Examples

See complete integration examples

API Reference

View the REST API (Swagger UI)