Canopy

Errors

The Canopy SDKs draw a hard line between infrastructure failures and policy outcomes:

  • Network drops, bad API keys, unexpected server statuses → throw
  • A denied or pending_approval outcome from the policy engine → return value

Your code handles policy outcomes with a switch / if, and reserves try/catch for infrastructure problems.

Error hierarchy

CanopyError
├── CanopyConfigError
├── CanopyApiError
├── CanopyNetworkError
└── CanopyApprovalTimeoutError

Error classes

ErrorWhen it's thrownKey fields
CanopyConfigErrorMissing apiKey, missing agentId, or other constructor misconfigurationdashboardUrl (TS) / dashboard_url (Py)
CanopyApiErrorServer returned an unexpected HTTP status (not a known policy outcome)status, body, dashboardUrl
CanopyNetworkErrorDNS / TLS / connection timeout / other transport-level problemcause
CanopyApprovalTimeoutErrorwaitForApproval() exhausted its timeout without a decisionapprovalId (TS) / approval_id (Py)

Most actionable errors include a dashboardUrl (TS) / dashboard_url (Py) pointing directly at the dashboard page that fixes the problem.

TypeScript example

import { CanopyError, CanopyApiError, CanopyNetworkError, CanopyConfigError } from "@canopy-ai/sdk";
 
try {
  const result = await canopy.pay({ to, amountUsd });
  // handle result.status here — no throws for denied / pending_approval
} catch (err) {
  if (err instanceof CanopyConfigError) {
    console.error("Config:", err.message, "→", err.dashboardUrl);
  } else if (err instanceof CanopyApiError && err.status === 401) {
    console.error("Bad API key. Regenerate at:", err.dashboardUrl);
  } else if (err instanceof CanopyApiError) {
    console.error(`API error ${err.status}:`, err.body);
  } else if (err instanceof CanopyNetworkError) {
    console.error("Network failure:", err.cause);
  } else if (err instanceof CanopyError) {
    console.error("Canopy:", err.message);
  } else {
    throw err;
  }
}

Python example

from canopy_ai import CanopyError, CanopyApiError, CanopyNetworkError, CanopyConfigError
 
try:
    result = canopy.pay(to=to, amount_usd=amount_usd)
    # handle result["status"] here
except CanopyConfigError as err:
    print("Config:", err, "→", err.dashboard_url)
except CanopyApiError as err:
    if err.status == 401:
        print("Bad API key. Regenerate at:", err.dashboard_url)
    else:
        print(f"API error {err.status}:", err.body)
except CanopyNetworkError as err:
    print("Network failure:", err.cause)
except CanopyError as err:
    print("Canopy:", err)

Resuming after CanopyApprovalTimeoutError

When waitForApproval() times out, the approval request is still alive. Use the approvalId to resume polling:

try {
  const decided = await canopy.waitForApproval(approvalId);
} catch (err) {
  if (err instanceof CanopyApprovalTimeoutError) {
    const status = await canopy.getApprovalStatus(err.approvalId);
    // Or just call pay() again with the same idempotencyKey — Canopy returns
    // the cached allowed result without re-charging.
  }
}