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
Code Status Description Solution AUTH_FAILED401 Invalid or expired API key Check key is correct and not revoked EXPIRED_KEY401 Key has expired Generate a new SDK key INSUFFICIENT_SCOPE403 Key lacks required permission Use key with required scope
Payment Errors
Code Status Description Solution PAYMENT_DENIED403 Payment blocked by policy Check violations for details REQUIRES_APPROVAL202 Needs manual approval Wait for human approval INSUFFICIENT_BALANCE400 Wallet has insufficient funds Fund the wallet DAILY_LIMIT_EXCEEDED400 Daily limit exceeded Wait for reset or increase limit PER_TX_LIMIT_EXCEEDED400 Amount exceeds per-tx limit Reduce amount or increase limit EXPIRED400 Payment request expired Request new approval NOT_FOUND404 Request ID not found Check the request ID INVALID_STATUS400 Cannot execute in current status Check payment status NO_WALLET400 No wallet assigned Link a wallet to the agent EXTERNAL_WALLET400 Cannot execute external wallet via /execute Use /confirm with your own txHash POLICY_DENIED400 Payment denied by policy (enriched) Check context.nextSteps EXTERNAL_WALLET_ERROR400 No eligible wallet with balance for this payment. The response includes alternatives showing other wallets. WALLET_CONFIG_ERROR400 Wallet is misconfigured (e.g., missing custody provider). Contact your admin. CUSTODY_NOT_CONFIGURED500 The custody provider (Sponge or Privy) is not configured on the server. MANUAL_EXECUTION_REQUIRED400 External or smart contract wallets cannot be auto-executed. Use the approve/confirm flow instead. REQUEST_EXPIRED400 The payment request expired before execution. Re-request. ALREADY_EXECUTED400 This payment request was already executed. Check transaction status instead.
Validation Errors
Code Status Description Solution VALIDATION_ERROR400 Invalid request body Check request parameters INVALID_AMOUNT400 Amount must be positive Use a positive number INVALID_ADDRESS400 Malformed wallet address Use valid 0x address (EVM) or base58 address (Solana) INVALID_JSON400 Malformed request body Send valid JSON
System Errors
Code Status Description Solution RATE_LIMITED429 Too many requests Wait and retry TIMEOUT0 Request timeout Retry with longer timeout INTERNAL_ERROR500 Server error Retry 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
}
Check Specific Error Codes
Don’t just catch generic errors: // Bad
catch ( error ) {
console . log ( 'Something went wrong' );
}
// Good
catch ( error ) {
if ( error . code === 'INSUFFICIENT_BALANCE' ) {
// Specific handling
}
}
Don't Retry Payment Execution
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)