Skip to main content

Error Handling

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

ContoError Class

All SDK errors extend the ContoError class:
interface ContoError extends Error {
  code: string;    // Error code (e.g., 'PAYMENT_DENIED')
  status: number;  // HTTP status code
  message: string; // Human-readable message
}

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

Validation Errors

CodeStatusDescriptionSolution
VALIDATION_ERROR400Invalid request bodyCheck request parameters
INVALID_AMOUNT400Amount must be positiveUse a positive number
INVALID_ADDRESS400Malformed Ethereum addressUse valid 0x address

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, ContoError } from '@conto/sdk';

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

Handling Specific Error Codes

try {
  await conto.payments.pay({ ... });
} catch (error) {
  if (error instanceof ContoError) {
    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

With Exponential Backoff

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 ContoError) {
        if (['AUTH_FAILED', 'PAYMENT_DENIED', 'VALIDATION_ERROR'].includes(error.code)) {
          throw error;  // Non-retryable
        }

        if (error.code === 'RATE_LIMITED') {
          const delay = error.retryAfter * 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
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