Skip to content

Python SDK & CLI

Maintained by: Aether365 Team Audience: Developers and DevOps engineers Scope: Installing and using the official aether365 Python SDK and command line interface

The aether365 package is the official Python SDK and command line interface for the Aether365 API. It wraps https://api.aether365.io, handles authentication, unwraps the response envelope, and retries transient failures automatically. Anything you can do with curl you can do with the SDK or the CLI.

The SDK and CLI use an API key (ak_live_...) and target the unified endpoint - requests are routed to your tenant's home region automatically. See Authentication for how keys work.

Installation

The package is distributed internally (not on PyPI). Install it directly from the repository or a local checkout.

bash
# From a local checkout of the monorepo
pip install ./packages/cli

# Or with Poetry, from a path
poetry add ./packages/cli

Python 3.12 or newer is required. Installing the package also puts the aether365 command on your PATH.

Configuration

Both the SDK and the CLI read the same environment variables:

VariablePurposeDefault
AETHER365_API_KEYYour API key (ak_live_...). Required.-
AETHER365_API_URLOverride the base URL (for example the dev endpoint).https://api.aether365.io
AETHER365_OUTPUTCLI output format: table or json.table
bash
export AETHER365_API_KEY="ak_live_..."

Region is automatic: the API resolves your tenant from the key and forwards to the correct region, so you never configure or pass a region.

Quickstart: SDK

python
from aether365 import Aether365Client

with Aether365Client() as client:          # reads AETHER365_API_KEY
    tenant = client.tenants.me()
    print(tenant["name"])

    # Trigger a scan and wait for it to finish
    scan = client.scans.create("compliance")
    for snapshot in client.scans.wait(scan["id"]):
        print(snapshot["status"])

    # Read the findings
    findings = client.scans.results(scan["id"], result="Failed")
    print(f"{len(findings)} failed checks")

Quickstart: CLI

bash
# Show the authenticated tenant
aether365 tenant me

# Trigger a scan and follow progress until it finishes
aether365 scan run --type compliance --watch

# List scans as JSON
aether365 --output json scan list

# Fail a CI job when a scan has failed findings (exit code 2)
aether365 scan results <SCAN_ID> --fail-on-findings

What an API key can and cannot do

An API key is scoped to a safe, read-plus-operational surface. It can read your data, trigger and manage scans, generate reports, and manage attack-surface targets. It cannot perform account-control or directory-lockout actions - API key management, billing, team membership, connection onboarding/offboarding, SSO, applying a remediation plan, or the AI Pilot conditional-access/break-glass writes. Those remain available only to a signed-in session in the dashboard. A request to a disallowed route returns 403 AUTH_INSUFFICIENT_SCOPE.

Command and method reference

Each row shows the same operation three ways: raw curl, the SDK method, and the CLI command.

Tenant and account

OperationcurlSDKCLI
Get tenant profileGET /tenants/meclient.tenants.me()aether365 tenant me
List connectionsGET /tenants/me/connectionsclient.connections.list()aether365 tenant connections
Notification prefsGET /tenants/me/notificationsclient.notifications.get()aether365 tenant notifications
Scheduled scansGET /tenants/me/scheduled-scansclient.scheduled_scans.list()aether365 tenant scheduled-scans

Scans

OperationcurlSDKCLI
List scansGET /tenants/me/scansclient.scans.list()aether365 scan list
Trigger a scanPOST /tenants/me/scansclient.scans.create("compliance")aether365 scan run --type compliance
Get a scanGET /scans/{id}client.scans.get(id)aether365 scan get <id>
Read resultsGET /scans/{id}/resultsclient.scans.results(id)aether365 scan results <id>
Cancel a scanPOST /scans/{id}/cancelclient.scans.cancel(id)aether365 scan cancel <id>
Hide a scanPATCH /scans/{id}/hideclient.scans.hide(id)aether365 scan hide <id>
Unhide a scanPATCH /scans/{id}/unhideclient.scans.unhide(id)aether365 scan unhide <id>
Delete a scanDELETE /scans/{id}client.scans.delete(id)aether365 scan delete <id>

The curl for triggering a scan:

bash
curl -X POST https://api.aether365.io/tenants/me/scans \
  -H "Authorization: Bearer $AETHER365_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scanType": "compliance"}'

Reports

OperationcurlSDKCLI
Report statusGET /scans/{id}/reportclient.reports.status(id)aether365 report status <id>
Generate a reportPOST /scans/{id}/report/generateclient.reports.generate(id)aether365 report generate <id>
List reportsGET /tenants/me/reportsclient.reports.list()aether365 report list
Download a reportGET /scans/{id}/report (presigned)client.reports.download(id, path)aether365 report download <id> -o out.pdf

Report status is one of ready, rendering, orgNameRequired, or locked. Generating a report may consume a report credit (a one-time charge when no allowance is included) - the key owner opts into this by calling generate.

Report listing is offset paginated: pass meta["nextCursor"] (an integer row offset, or None on the last page) back as cursor. client.reports.iter_all() follows the cursor for you.

Attack surface

OperationcurlSDKCLI
OverviewGET /tenants/me/attack-surfaceclient.attack_surface.summary()aether365 eas summary
HistoryGET /tenants/me/attack-surface/historyclient.attack_surface.history()aether365 eas history
List targetsGET /tenants/me/attack-surface/targetsclient.attack_surface.targets()aether365 eas targets list
Add a targetPOST /tenants/me/attack-surface/targetsclient.attack_surface.add_target(host)aether365 eas targets add <host>
Update a targetPATCH /tenants/me/attack-surface/targets/{id}client.attack_surface.update_target(id, ...)aether365 eas targets update <id> --label ...
Delete a targetDELETE /tenants/me/attack-surface/targets/{id}client.attack_surface.remove_target(id)aether365 eas targets remove <id>
Verify a targetPOST /tenants/me/attack-surface/targets/{id}/verifyclient.attack_surface.verify_target(id)aether365 eas targets verify <id>
Run an EAS scanPOST /tenants/me/attack-surface/scanclient.attack_surface.scan()aether365 eas scan

Posture, remediation, and AI Pilot

OperationcurlSDKCLI
Threat / risk viewGET /tenants/me/threatsclient.threats.list()aether365 threats
Policy postureGET /tenants/me/policiesclient.policies.list()aether365 policies
Remediation capabilitiesGET /tenants/me/remediation/capabilitiesclient.remediation.capabilities()aether365 remediation capabilities
List remediation plansGET /tenants/me/remediation-plansclient.remediation.plans()aether365 remediation plans
Get a remediation planGET /tenants/me/remediation-plans/{id}client.remediation.plan(id)aether365 remediation plan <id>
AI Pilot remediableGET /tenants/me/ai-pilot/remediableclient.ai_pilot.remediable()aether365 ai-pilot remediable
AI Pilot break-glassGET /tenants/me/ai-pilot/break-glassclient.ai_pilot.break_glass()aether365 ai-pilot break-glass
AI Pilot CA policiesGET /tenants/me/ai-pilot/conditional-accessclient.ai_pilot.conditional_access()aether365 ai-pilot conditional-access

Remediation is read only for API keys. Applying a plan is not exposed: a plan's action-registry items can include Conditional-Access / authorization-policy patches that would lock a tenant out of its own directory (AADSTS50097), so apply stays in the dashboard with an operator in the loop. AI Pilot conditional-access and break-glass are likewise exposed read only for the same reason.

Error handling and retries

Every API error maps to a typed exception subclass of Aether365Error:

python
from aether365 import Aether365Client
from aether365.exceptions import PermissionError_, PlanLimitError, NotFoundError

with Aether365Client() as client:
    try:
        client.scans.create("compliance")
    except PlanLimitError:
        print("Scan quota reached for this plan.")
    except PermissionError_:
        print("Route not allowed for this key, or plan lacks API access.")
    except NotFoundError:
        print("Resource not found.")

The client automatically retries 429 RATE_LIMITED and 503 SERVICE_STARTING (the dev cold-database warm-up) with exponential backoff, honoring the Retry-After header. Quota errors such as SCAN_PLAN_LIMIT_REACHED are raised immediately - they are not retried.

Using the CLI in CI

aether365 scan results <id> --fail-on-findings exits with code 2 when any failed finding is present, so a pipeline can gate on scan results:

bash
aether365 scan run --type compliance --watch
aether365 scan results "$SCAN_ID" --fail-on-findings

The CLI writes errors to stderr and exits 1 on any API error, 2 on the findings gate.

Was this page helpful?