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

SDK failures use the exported `ContoError` or `ContoAdminError` classes. They include stable error
metadata alongside the standard `Error` fields:

```typescript theme={null}
class ContoError extends Error {
  code: string; // Error code (e.g., 'PAYMENT_DENIED')
  status: number; // HTTP status code
  message: string; // Human-readable message
  requestId?: string;
  retryAfter?: number;
  details?: unknown;
  remediation?: string;
  context?: unknown;
}
```

Use `error instanceof ContoError` for agent-scoped calls and `error instanceof ContoAdminError` for
organization-scoped calls. The structured fields are preserved from the API response when present.

## 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, ContoError } from '@conto_finance/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);
    console.error('Request ID:', error.requestId);
    console.error('Next step:', error.remediation);
  } 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 handles rate limits during its built-in retry budget for safe requests (see [Retry Strategy](#retry-strategy) below). If those retries are exhausted, the thrown `ContoError` exposes `retryAfter` in seconds when the server supplied that guidance.

Use that value to schedule a later retry only for a read or another operation that is safe to repeat:

```typescript theme={null}
import { ContoError } from '@conto_finance/sdk';

try {
  return await conto.payments.status(requestId); // Safe, read-only request
} catch (error) {
  if (error instanceof ContoError && error.code === 'RATE_LIMITED') {
    console.log(`Try the status read again after ${error.retryAfter ?? 'a few'} seconds`);
  }
  throw error;
}
```

Do not wrap `payments.pay()` or `payments.execute()` in a generic retry loop. If execution has an ambiguous failure, reconcile the known `requestId` with `payments.status(requestId)` before deciding whether another execution attempt is safe. Prefer separate `request()` and `execute()` calls, with a stable `idempotencyKey`, whenever the workflow needs deterministic recovery.

## 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
});

const request = await conto.payments.request({
  amount: 100,
  recipientAddress: '0x...',
  idempotencyKey: 'job-123-payment-1',
});

if (request.status === 'APPROVED') {
  try {
    await conto.payments.execute(request.requestId);
  } catch (error) {
    if (error instanceof ContoError && error.code === 'TIMEOUT') {
      console.log('Execution timed out');
      // Do not execute again until the known request is reconciled.
      const status = await conto.payments.status(request.requestId);
      if (status.transaction) {
        console.log('Payment was submitted:', status.transaction.txHash);
      }
    }
  }
}
```

## Retry Strategy

### Built-in Automatic Retry

The SDK automatically retries transient failures only when repeating the request cannot create an
additional side effect.

**Built-in behavior:**

* Retries read-only requests up to **3 times** on network failures, `429`, and `5xx`
* Retries payment authorization requests with the same stable `idempotencyKey`
* Treats x402 and MPP pre-authorization as retryable, non-mutating policy checks
* Respects `Retry-After` headers from the server
* Exponential backoff: 1s → 2s → 4s (capped at 10s)
* **Never automatically retries** payment execution, protocol record calls, or
  `ContoAdmin` mutations
* Does not retry client errors (`4xx` except retryable `429` responses) or auth failures

```typescript theme={null}
// A stable key protects request creation across retries and process restarts.
const request = await conto.payments.request({
  amount: 100,
  recipientAddress: '0x...',
  idempotencyKey: 'job-123-payment-1',
});

if (request.status === 'APPROVED') {
  try {
    await conto.payments.execute(request.requestId);
  } catch (error) {
    // Execution is not retried automatically. Reconcile before acting again.
    const status = await conto.payments.status(request.requestId);
    console.error(error, status);
  }
}
```

For workflows that require deterministic recovery, prefer the separate `request` and `execute`
methods over the `pay` convenience method so the application retains the `requestId`.

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