> ## Documentation Index
> Fetch the complete documentation index at: https://conto.finance/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Handle payment denials, authentication errors, rate limits, and timeouts with the Conto SDK error system

# 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:

```typescript theme={null}
interface ContoError extends Error {
  code: string;    // Error code (e.g., 'PAYMENT_DENIED')
  status: number;  // HTTP status code
  message: string; // Human-readable message
}
```

<Warning>
  `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.
</Warning>

## Error Codes

### Authentication Errors

| Code                 | Status | Description                                             | Solution                        |
| -------------------- | ------ | ------------------------------------------------------- | ------------------------------- |
| `AUTH_FAILED`        | 401    | Missing, invalid, inactive, revoked, or expired API key | Check key is correct and active |
| `INSUFFICIENT_SCOPE` | 403    | Key lacks required permission                           | Use key with required scope     |

### Payment Errors

| Code                        | Status | Description                                                | Solution                                                  |
| --------------------------- | ------ | ---------------------------------------------------------- | --------------------------------------------------------- |
| `PAYMENT_DENIED`            | 403    | SDK `pay()` helper saw a denied request                    | Inspect the request response's `reasons` and `violations` |
| `REQUIRES_APPROVAL`         | 202    | SDK `pay()` helper saw an approval-required request        | Wait for human approval                                   |
| `INSUFFICIENT_BALANCE`      | 400    | Wallet has insufficient funds                              | Fund the wallet                                           |
| `EXPIRED`                   | 400    | Payment request expired                                    | Request new approval                                      |
| `NOT_FOUND`                 | 404    | Request ID not found                                       | Check the request ID                                      |
| `INVALID_STATUS`            | 400    | Cannot execute in current status                           | Check payment status                                      |
| `NO_WALLET`                 | 400    | No wallet assigned                                         | Link a wallet to the agent                                |
| `LIMIT_EXCEEDED`            | 400    | Spend limit or balance re-check failed at execution time   | Re-request after reducing amount or freeing budget        |
| `WALLET_CONFIG_ERROR`       | 400    | Wallet is misconfigured (e.g. missing custody provider)    | Contact your admin                                        |
| `CUSTODY_NOT_CONFIGURED`    | 503    | The custody provider is not configured on the server       | Contact support                                           |
| `MANUAL_EXECUTION_REQUIRED` | 400    | External or smart contract wallets cannot be auto-executed | Use the approve/confirm flow instead                      |
| `ALREADY_EXECUTED`          | 409    | This payment request was already executed                  | Check transaction status instead                          |

### Validation Errors

| Code               | Status | Description              | Solution                                              |
| ------------------ | ------ | ------------------------ | ----------------------------------------------------- |
| `VALIDATION_ERROR` | 400    | Invalid request body     | Check request parameters                              |
| `INVALID_AMOUNT`   | 400    | Amount must be positive  | Use a positive number                                 |
| `INVALID_ADDRESS`  | 400    | Malformed wallet address | Use valid 0x address (EVM) or base58 address (Solana) |
| `INVALID_JSON`     | 400    | Malformed request body   | Send valid JSON                                       |

### System Errors

| Code             | Status | Description       | Solution                  |
| ---------------- | ------ | ----------------- | ------------------------- |
| `RATE_LIMITED`   | 429    | Too many requests | Wait and retry            |
| `TIMEOUT`        | 0      | Request timeout   | Retry with longer timeout |
| `INTERNAL_ERROR` | 500    | Server error      | Retry or contact support  |

## Handling Errors

### Basic Error Handling

```typescript theme={null}
import { Conto } from '@conto_finance/sdk';
import type { ContoError } from '@conto_finance/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

```typescript theme={null}
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 'LIMIT_EXCEEDED':
        console.log('Limit or balance changed before execution');
        // Re-check setup and request a smaller payment
        break;

      case 'RATE_LIMITED':
        console.log('Too many requests. The SDK already retried automatically before throwing.');
        // Back off before issuing more requests.
        break;

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

## Enriched Denial And Error Responses

Some SDK responses include additional context to help agents recover programmatically. Policy denials
from `POST /api/sdk/payments/request` are normal `200` responses with `status: "DENIED"`, plus
`hint`, `context`, and `nextSteps` fields. Execution failures are non-2xx error responses with an
`error` and `code`.

### Denial Response Structure

```typescript theme={null}
interface EnrichedDenial {
  requestId: string;
  status: 'DENIED';
  reasons: string[];
  violations?: object[];
  hint?: string;
  context?: {
    wallets?: Array<{
      id: string;
      address: string;
      chainId: string;
      custodyType: string;
      balance: number;
    }>;
    nextSteps?: string[];
  };
}
```

### Example: Manual Execution Required

When trying to `/execute` a payment assigned to an external wallet:

```json theme={null}
{
  "error": "EXTERNAL wallets require manual execution. Execute the transfer from your custody provider and confirm with a txHash.",
  "code": "MANUAL_EXECUTION_REQUIRED"
}
```

### Example: Policy Denial With Recovery Context

```json theme={null}
{
  "requestId": "cmm...",
  "status": "DENIED",
  "reasons": ["Would exceed daily limit"],
  "hint": "Review the violations below. You may need to request a policy exception or adjust the payment parameters.",
  "context": {
    "nextSteps": [
      "Reduce the payment amount",
      "Request a policy exception",
      "Use GET /api/sdk/setup to check current balances"
    ]
  }
}
```

### Handling Enriched Errors

```typescript theme={null}
const response = await fetch(`/api/sdk/payments/${id}/execute`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.CONTO_API_KEY}` },
});

const body = await response.json();

if (!response.ok) {
  if (body.code === 'MANUAL_EXECUTION_REQUIRED') {
    // Execute with your own wallet and confirm with txHash.
    console.log('Use approve/confirm for this wallet');
  }

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

  throw new Error(body.error || 'Payment execution failed');
}
```

### Using Request and Execute for Better Control

The `pay()` method throws on denial. For more control, use separate request/execute:

```typescript theme={null}
// 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

The SDK already handles rate limits for you before throwing (see [Retry Strategy](#retry-strategy) below for the exact behavior). `retryAfter` is consumed internally and is not exposed on the thrown error, so do not read `error.retryAfter`.

If you need an additional application-level retry after the SDK has exhausted its own, wrap the call and back off on your own schedule:

```typescript theme={null}
async function payWithRetry(params: PaymentRequestInput) {
  const maxRetries = 3;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await conto.payments.pay(params);
    } catch (error) {
      const isRateLimited = error instanceof Error && 'code' in error && error.code === 'RATE_LIMITED';
      if (isRateLimited && i < maxRetries - 1) {
        const waitTime = 2 ** i; // 1s, 2s, 4s
        console.log(`Still rate limited. Waiting ${waitTime}s...`);
        await new Promise((r) => setTimeout(r, waitTime * 1000));
        continue;
      }
      throw error;
    }
  }
}
```

## Handling Timeouts

For long-running requests, handle timeouts:

```typescript theme={null}
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

```typescript theme={null}
// 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:

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

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

```typescript theme={null}
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) {
    const sdkError = error as ContoError;
    console.error('Payment failed', {
      code: sdkError.code,
      message: sdkError.message,
      params: {
        amount: params.amount,
        recipient: params.recipientAddress,
        purpose: params.purpose
      }
    });
    throw error;
  }
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Always Catch Errors">
    Never let payment errors crash your application:

    ```typescript theme={null}
    // Bad
    await conto.payments.pay({ ... });

    // Good
    try {
      await conto.payments.pay({ ... });
    } catch (error) {
      // Handle gracefully
    }
    ```
  </Accordion>

  <Accordion title="Check Specific Error Codes">
    Don't just catch generic errors:

    ```typescript theme={null}
    // Bad
    catch (error) {
      console.log('Something went wrong');
    }

    // Good
    catch (error) {
      if (error.code === 'INSUFFICIENT_BALANCE') {
        // Specific handling
      }
    }
    ```
  </Accordion>

  <Accordion title="Don't Retry Payment Execution">
    Be careful with retries on payment execution:

    ```typescript theme={null}
    // 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);
    }
    ```
  </Accordion>

  <Accordion title="Log for Debugging">
    Always log error details for debugging:

    ```typescript theme={null}
    catch (error) {
      console.error('Payment error', {
        code: error.code,
        status: error.status,
        message: error.message
      });
    }
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Examples" icon="code" href="/sdk/examples">
    See complete integration examples
  </Card>

  <Card title="API Reference" icon="server" href="https://conto.finance/api-docs">
    View the REST API (Swagger UI)
  </Card>
</CardGroup>
