Docs
Open the console →
API reference

API reference: Cloud accounts

Cushy cloud-connections API: list and create accounts (AWS, GCP, Azure, Alibaba), verify health, disconnect/purge/reconnect, sync, read synced inventory, reachability analysis, cross-cloud connections, provisioning opt-in and per-resource metrics endpoints.

Connect and read cloud accounts. Reads are open to any member; mutations require manage_cloud_accounts (SRE · Cloud Admin or Org Admin). Cross-org ids return 404.

GET/api/cloud/accountsSession or Bearer

List your org's cloud accounts with status, health detail and per-type sync runs. Never includes the encrypted connection payload.

Example response
json
{ "accounts": [ { "id": 1, "provider": "AWS", "name": "prod",
  "externalKey": "123456789012", "status": "healthy",
  "healthDetail": { "checks": [ { "name": "sts:AssumeRole", "status": "ok", "checkedAt": "…" } ] },
  "lastValidatedAt": "…", "lastSyncedAt": "…", "runs": [ { "resourceType": "ec2-instance", "stats": {} } ] } ] }
Errors
StatusWhen
401Not signed in.
POST/api/cloud/accountsmanage_cloud_accounts

Create a connection for a provider. Returns 201 with the new account and onboarding references.

Parameters
NameInRequiredDescription
providerbodyyesAWS, GCP, AZR or ALI — all four adapters are real (keyless: role assumption / workload-identity federation only).
namebodynoOptional label.
awsbodyno{ accountId } for AWS (cross-account role + ExternalId).
gcpbodyno{ projectId, projectNumber } for GCP (workload identity federation).
azrbodyno{ tenantId, subscriptionId, clientId? } for Azure (Entra app federated credential — no client secret).
alibodyno{ accountUid, roleArn?, oidcProviderArn? } for Alibaba (RAM OIDC provider + AssumeRoleWithOIDC — no AccessKeySecret).
Example request
json
{ "provider": "AWS", "name": "prod", "aws": { "accountId": "123456789012" } }
Example response
json
{ "id": 1, "…": "onboarding references (ExternalId, role/script details)" }
Errors
StatusWhen
400Unknown provider or invalid input.
403Caller lacks manage_cloud_accounts (audited cloud.rbac_denied).
409Already connected — includes existingId.
503Could not create the account.
POST/api/cloud/accounts/{id}/validatemanage_cloud_accounts

Verify / re-check health. First-ever healthy fires the cutover and kicks the first sync.

Parameters
NameInRequiredDescription
idpathyesAccount id in your org.
Example request
json
{}  // no body — the stored connection payload is re-checked
Example response
json
{ "status": "healthy", "identity": { "…": "…" }, "checks": [ { "name": "…", "status": "ok" } ] }
Errors
StatusWhen
403Caller lacks manage_cloud_accounts.
404No such account in your org.
409Account disconnected, or a check failed — carries missing for a permission error.
503Transient provider error — retry.
POST/api/cloud/accounts/{id}/syncmanage_cloud_accounts

Bounded on-demand sync of one healthy account. Returns 202 with the pass summary.

Parameters
NameInRequiredDescription
idpathyesAccount id in your org.
Example request
json
{}  // no body — queues a bounded on-demand sweep
Example response
json
{ "queued": 4, "completed": 3, "deferred": 1 }
Errors
StatusWhen
403Caller lacks manage_cloud_accounts.
404No such account in your org.
409Connection is not healthy — fix health first.
DELETE/api/cloud/accounts/{id}manage_cloud_accounts

Disconnect (never purge). Syncing stops; discovered rows are marked account_disconnected.

Parameters
NameInRequiredDescription
idpathyesAccount id in your org.
Example response
json
{ "ok": true, "reminder": "delete the IAM role / federation config to fully de-provision." }
Errors
StatusWhen
403Caller lacks manage_cloud_accounts.
404No such account in your org.
409Already disconnected.
GET/api/cloud/resourcesSession or Bearer

Keyset-paginated synced inventory. Filter and page through your resources.

Parameters
NameInRequiredDescription
accountIdquerynoOwning cloud-account id (positive integer) — return only that connected account's resources.
providerquerynoAWS / GCP / AZR / ALI.
typequerynoResource type filter.
regionquerynoRegion filter.
qquerynoFree-text search over name and native id.
limitqueryno1–200 (default 100).
cursorquerynoKeyset cursor from a prior nextCursor.
includeDeletedqueryno1 to include soft-deleted rows with reason.
Example response
json
{ "resources": [ { "id": 42, "accountId": 7, "provider": "AWS", "resourceType": "network",
  "nativeId": "vpc-0abc…", "name": "prod-use1", "region": "us-east-1", "status": "ok",
  "attributes": {}, "providerDetail": {}, "tags": {}, "firstSeenAt": "…", "lastSeenAt": "…",
  "deletedAt": null, "deletedReason": null,
  "managed": false, "managedByWorkspaceId": null } ], "nextCursor": 42 }
Errors
StatusWhen
400limit out of 1–200, a negative cursor, or a non-positive-integer accountId.
401Not signed in / invalid token.
POST/api/cloud/reachabilitySession

Network reachability / path analysis (NET), provider-aware across all four clouds. Given a source and destination IP, returns whether traffic is reachable and the ordered per-layer hop list (on AWS: security-group egress → NACL egress → route → gateway → NACL ingress → security-group ingress + the stateless return path; Azure NSGs / GCP VPC firewalls / Alibaba security groups use their own layers and defaults), naming the blocking layer.

Parameters
NameInRequiredDescription
sourceIpbodyyesSource IPv4 address.
destIpbodyyesDestination IPv4 address.
protocolbodynotcp | udp | icmp | all (default tcp).
portbodyno0–65535 (default 443).
sourceComponentIdbodynoA resource nativeId to disambiguate the source.
destComponentIdbodynoA resource nativeId to disambiguate the destination.
Example request
json
{ "sourceIp": "10.20.1.14", "destIp": "172.31.8.41", "protocol": "tcp", "port": 443 }
// or anchor on discovered resources instead of raw IPs:
// { "sourceComponentId": "i-0a1b2c3d…", "destComponentId": "i-0f9e8d7c…", "port": 5432 }
Example response
json
{ "reachable": false, "protocol": "tcp", "port": 443,
  "source": { "kind": "instance", "id": "i-0a…", "name": "prod-node-01", "ip": "10.0.1.41" },
  "destination": { "kind": "instance", "id": "i-0a…", "name": "prod-node-02", "ip": "10.0.2.41" },
  "hops": [ { "layer": "Destination security group (ingress)", "direction": "ingress",
    "component": { "id": "sg-0a…app1", "name": "app", "kind": "securitygroup" },
    "verdict": "deny", "explanation": "No ingress rule allows tcp/443 from 10.0.1.41." } ],
  "blockedAt": 5, "summary": "Not reachable: blocked at destination security group (ingress) …" }
Errors
StatusWhen
400sourceIp/destIp is not a valid IPv4, bad protocol, or port out of range.
401Not signed in.
200A never-connected org (no topology) returns { empty: true }.
POST/api/cloud/reachability/probemutate_infra

REACH-PROBE: verify a static reachability verdict with a LIVE probe executed FROM a discovered compute instance. The probe runs the SAME server-composed script (bash /dev/tcp or ping on Linux, Test-NetConnection on Windows — selected from the discovered osPlatform) through the source cloud's native command channel: AWS SSM Run Command, Azure Run Command (async operation polled; this channel records NO exit code — evidence.exitCode is null there and the marker output is the verdict), or Alibaba ECS Cloud Assistant. The response carries the provider's own execution record as evidence: the per-cloud execution id (SSM commandId / Azure Run Command operation id / Cloud Assistant InvokeId — each verifiable in the customer's own console), document, exit code, capped stdout/stderr, timestamps and parsed latency. expected (the analyzer's verdict) enables the agreement field: confirmed = proven with evidence at that moment; contradiction = a first-class finding (audited network.probe_contradiction). A refused/timed-out connection is a not_connected RESULT with evidence, never an error. Audited network.probe_executed.

Parameters
NameInRequiredDescription
sourceComponentIdbodyyesThe SOURCE — a synced compute instance's nativeId (the probe runs FROM a managed instance, never from a bare IP).
destIpbodyyesDestination IPv4 address.
portbodyno1–65535 (default 443; ignored for icmp).
protocolbodynotcp | icmp (default tcp; udp is refused honestly — no reliable udp test).
expectedbodyno"reachable" | "not_reachable" — the static analyzer's verdict, enabling the agreement computation.
Example request
json
{ "sourceComponentId": "i-0a1b2c3d4e5f60001", "destIp": "10.0.2.41", "port": 443, "protocol": "tcp", "expected": "not_reachable" }
Example response
json
{ "ok": true, "verdict": "not_connected", "agreement": "confirmed", "expected": "not_reachable", "real": true,
  "source": { "nativeId": "i-0a…0001", "name": "prod-node-01", "provider": "AWS", "region": "us-east-1" },
  "target": { "ip": "10.0.2.41", "port": 443, "protocol": "tcp" },
  "evidence": { "commandId": "6c1f…-…", "instanceId": "i-0a…0001", "documentName": "AWS-RunShellScript",
    "exitCode": 1, "stdout": "CC_PROBE_START …\nCC_PROBE_FAIL tcp 10.0.2.41 443 rc=124\n…", "stderr": "",
    "startedAt": "…", "finishedAt": "…", "detail": "The connection attempt timed out …" },
  "summary": "Live probe from prod-node-01 could NOT connect to 10.0.2.41:443 — … proven with evidence (command 6c1f…)." }
Errors
StatusWhen
400Missing sourceComponentId, bad destIp/port, udp, a non-compute source, a draft-lifecycle source, or the instance's command agent isn't reachable (the message names the exact prerequisite: SSM agent + AmazonSSMManagedInstanceCore / Azure VM agent + a RUNNING VM / the Cloud Assistant agent).
401Not signed in.
403Role lacks mutate_infra — a probe executes a command on an instance (denial audited cloud.rbac_denied).
404Unknown or cross-org source id (no existence leak).
409The source's cloud account is disconnected, or the connector role/principal lacks the execution permission (ssm:SendCommand / Microsoft.Compute/virtualMachines/runCommand/action / ecs:RunCommand).
501A GCP source — Google Cloud has no ad-hoc run-command API (VM Manager / OS Config is patch + declarative policy + inventory only); the message names SSH over IAP (gcloud compute ssh --tunnel-through-iap) as the manual path.
POST/api/cloud/accounts/{id}/provisioningmanage_cloud_accounts

Enable or disable REAL Terraform provisioning for this account (default OFF). Enabling requires an explicit acknowledgement — it authorizes real, billable resource creation with short-lived write credentials. Audited cloud.provisioning_enabled/disabled.

Parameters
NameInRequiredDescription
idpathyesAccount id in your org.
enabledbodyyestrue or false.
ackbodynoRequired (true) when enabling.
Example request
json
{ "enabled": true, "ack": true }
Example response
json
{ "ok": true, "account": { "id": 1, "provisioningEnabled": true, "…": "…" } }
Errors
StatusWhen
400Enabling without the acknowledgement.
403Caller lacks manage_cloud_accounts (Viewer/Deployer).
404No such account in your org.
GET/api/cloud/connectionsSession

List your org's cross-cloud / peering connections (site-to-site VPN across any of the six cloud pairs, or same-provider peering drawn on the topology map). The pre-shared key is never returned — the DTO carries secretSet only.

Example response
json
{ "connections": [ { "id": 3, "connType": "s2s_vpn", "status": "up",
  "a": { "provider": "AWS", "networkNativeId": "vpc-0a…", "region": "us-east-1" },
  "b": { "provider": "AZR", "networkNativeId": "/subscriptions/…/vnet-prod", "region": "eastus" },
  "workspaceId": 9, "secretSet": true, "tunnels": [ { "state": "up" }, { "state": "up" } ] } ] }
Errors
StatusWhen
401Not signed in.
POST/api/cloud/connectionsmanage_cloud_accounts

Create a connection between two healthy accounts (different providers ⇒ s2s_vpn; same provider ⇒ peering) and kick auto-provisioning through the Terraform engine. Also accepts the topology editor's draw payload (aNativeId/bNativeId endpoints + direction + rules + emitEgress). DELETE /api/cloud/connections/{id} soft-deletes.

Parameters
NameInRequiredDescription
aAccountId / bAccountIdbodynoThe two account ids (classic path).
aNativeId / bNativeIdbodynoTwo network/subnet native ids (draw-a-connector path).
connTypebodynos2s_vpn | peering | interconnect (interconnect is tracked model-only, never auto-provisioned).
direction / rules / emitEgressbodynoConnector direction, allow rules and egress emission (topology editor).
Example request
json
{
  "name": "tokyo-to-default",
  "aNativeId": "vpc-0b63273…",
  "bNativeId": "vpc-039447d…",
  "connType": "peering",
  "direction": "bidirectional"
}
// cross-cloud instead: { "aAccountId": 1, "bAccountId": 4, "connType": "s2s_vpn" }
Example response
json
{ "connection": { "id": 3, "status": "planned", "…": "…" } }
Errors
StatusWhen
400Unhealthy/unknown account, bad CIDR/port, or an invalid connType.
403Caller lacks manage_cloud_accounts (Viewer).
404Cross-org account/endpoint ids.
GET/api/cloud/resources/{nativeId}/metrics-endpointmanage_cloud_accounts (writes); managers view

Per-resource metrics endpoint: GET returns the resolved non-secret config + per-provider collector install instructions; PUT saves { baseUrl, token?, labelFilter? } (https-only; the bearer token is encrypted and never echoed); DELETE removes it. The endpoint is keyed to the synced resource's native id — the collector agent lives on that resource.

Example response
json
{ "endpoint": { "baseUrl": "https://prom.internal:9090", "labelFilter": "instance=\"i-0a1b…\"", "tokenSet": true, "updatedAt": "2026-08-14T09:00:00.000Z" },
  "profile": { "provider": "AWS", "identityLabels": ["instance"], "install": "…" } }
// never configured → { "endpoint": null, "profile": { … } }
Errors
StatusWhen
400Non-https base URL or a malformed config.
403Write without manage_cloud_accounts.
404Unknown/cross-org native id.
GET/api/cloud/settingsmanage_cloud_accounts (view)

Cushy's trust anchors for onboarding (the platform AWS account id and OIDC/WIF issuer your cloud-side role or federation must trust). Managers view them so the connect instructions render real values; they are managed by Cushy.

Example response
json
{ "awsAccountId": "123456789012", "oidcIssuer": "https://cushy.example.com", "canEdit": false }
Errors
StatusWhen
401Not signed in.
403Your role cannot manage cloud connections.

Disconnect vs delete. DELETE /api/cloud/accounts/{id} is a SOFT disconnect (rows retained, marked account_disconnected); DELETE /api/cloud/accounts/{id}?purge=1 is the explicit hard delete of an already-disconnected account — it removes the account row and all its synced data, and answers 409 for a live account or one that still has Terraform workspaces or a live connection. A disconnected account can also be reconnected: a healthy POST …/validate flips it back to active and kicks a fresh sync.