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.
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); } }}
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.
When trying to /execute a payment assigned to an external wallet:
{ "error": "EXTERNAL wallets require manual execution. Execute the transfer from your custody provider and confirm with a txHash.", "code": "MANUAL_EXECUTION_REQUIRED"}
{ "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" ] }}
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');}
The SDK already handles rate limits for you before throwing (see 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:
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; } }}
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 methodconst result = await conto.payments.pay({ amount: 100, recipientAddress: '0x...'});// If the server returns 429 or 503, the SDK waits and retries automatically