Errors
The Canopy SDKs draw a hard line between infrastructure failures and policy outcomes:
- Network drops, bad API keys, unexpected server statuses → throw
- A
deniedorpending_approvaloutcome 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
└── CanopyApprovalTimeoutErrorError classes
| Error | When it's thrown | Key fields |
|---|---|---|
CanopyConfigError | Missing apiKey, missing agentId, or other constructor misconfiguration | dashboardUrl (TS) / dashboard_url (Py) |
CanopyApiError | Server returned an unexpected HTTP status (not a known policy outcome) | status, body, dashboardUrl |
CanopyNetworkError | DNS / TLS / connection timeout / other transport-level problem | cause |
CanopyApprovalTimeoutError | waitForApproval() exhausted its timeout without a decision | approvalId (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.
}
}