Agency API Keys

What are agency API keys?

Agency API keys are long-lived credentials scoped to your agency billing account. They let your own software — scripts, automation tools, CI/CD pipelines, dashboards, and custom portals — talk directly to CloudAIPilot on your behalf, any time, without a browser or an active login session.

Think of the CloudAIPilot dashboard as the front door to the platform. An agency API key is a side door: your applications can walk in, read data, and take action programmatically. You control exactly which tools get a key, and you can revoke any key at any time without affecting anything else.

Agency API keys are separate from the standard CloudAIPilot API keys used for per-organisation automation:

Key typePrefixScope
Standard organisation API keycap_One organisation
Agency API keycpa_agy_Your entire agency billing account

Never use a standard org key to call agency endpoints, and never use an agency key to call per-org endpoints.


Who can generate agency API keys?

RoleGenerate keys?Revoke keys?Use keys to call the API?
Agency billing account OwnerYesYesYes
Agency billing account AdminYesYesYes
Agency billing account Member / ViewerNoNoYes (if given the key by an owner)

Key management — create, list, revoke — is available only to owners and admins of the agency billing account. Keys themselves can be shared manually with any system or person that needs to call the API server-to-server.


Key format and anatomy

Every agency API key follows this exact format:

cpa_agy_[12-char hex ID]_[64-char hex secret]

Example (not a real key):

cpa_agy_a1b2c3d4e5f6_7890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12
SegmentPurpose
cpa_agy_Fixed human-readable prefix. Makes the key identifiable in logs, code reviews, and secret scanners (such as GitHub secret scanning).
12-character hex IDKey identifier — used to look up the key row quickly. Shown in your key list as the masked prefix. Safe to include in audit logs.
64-character hex secretThe actual credential. Never log or expose this portion.

Security properties

  • Shown exactly once. The full key is displayed immediately after creation. It is never retrievable again. If you lose it, revoke it and create a new one.
  • Never stored as plain text. Only a salted hash of the key is stored. An attacker who accessed the database could not reconstruct any key from what is stored.
  • Timing-safe verification. The server uses constant-time comparison when validating keys, preventing timing-based inference attacks.
  • Scoped to one billing account. A leaked key can only access your agency billing account's data — it cannot access other billing accounts or other users' organisations.
  • Tied to a creator. Requests authenticated with a key are attributed in the audit log to the user who created the key.

How authentication works

The API server accepts an agency API key in two places on every request.

Option A — X-Api-Key header (recommended)

GET /api/v1/agency/{billingAccountId}/clients HTTP/1.1
Host: app.cloudaipilot.com
X-Api-Key: cpa_agy_a1b2c3d4e5f6_7890abcdef...

Option B — Authorization Bearer token

GET /api/v1/agency/{billingAccountId}/clients HTTP/1.1
Host: app.cloudaipilot.com
Authorization: Bearer cpa_agy_a1b2c3d4e5f6_7890abcdef...

The server automatically distinguishes agency API keys from user session tokens by detecting the cpa_agy_ prefix. Session-cookie-based browser requests continue to work exactly as before — API keys are an additional authentication path, not a replacement.

When a valid key is presented, the server: looks up the key by billing account and key prefix, verifies the key using constant-time comparison, loads the user account that created the key (so audit logs correctly attribute the action), and records the lastUsedAt timestamp on the key.


Generate an agency API key

Via the dashboard (UI)

Step 1 — Open Agency → Settings in the sidebar.

Step 2 — Scroll to the API Access card.

Step 3 — Type a descriptive name in the New API Key Name field (for example: Internal Dashboard, n8n Automation, Monitoring Bot).

Step 4 — Click Create API Key.

Step 5 — A modal displays the full key. Copy it now — it will not be shown again.

Step 6 — Store the key in your secrets manager (AWS Secrets Manager, GitHub Actions secrets, Doppler, 1Password, or similar). Never store it in source code or a plain-text config file.

Via the REST API (programmatic)

If you already have an active session or an existing agency API key, you can create a new key programmatically:

POST /api/v1/agency/{billingAccountId}/api-keys
Content-Type: application/json
X-Api-Key: cpa_agy_existing_key_here

{
  "name": "CI Pipeline Key"
}

Response:

{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "CI Pipeline Key",
    "maskedKey": "cpa_agy_3a7f9c002b11_••••••••",
    "plainTextKey": "cpa_agy_3a7f9c002b11_e8d4f2a1b9c8...",
    "createdAt": "2026-05-23T12:00:00.000Z",
    "lastUsedAt": null,
    "revokedAt": null
  }
}

plainTextKey is returned only in this response. Store it immediately. Subsequent requests to list keys return only the masked form.


List your keys

GET /api/v1/agency/{billingAccountId}/api-keys
X-Api-Key: cpa_agy_...

Response:

{
  "data": [
    {
      "id": "550e8400-...",
      "name": "CI Pipeline Key",
      "maskedKey": "cpa_agy_3a7f9c002b11_••••••••",
      "createdAt": "2026-05-23T12:00:00.000Z",
      "lastUsedAt": "2026-05-23T14:30:00.000Z",
      "revokedAt": null
    }
  ]
}

The lastUsedAt timestamp updates every time the key is used. Use it to audit which keys are actively in use and revoke any that have gone unused.


Revoke a key

Via the dashboard

  1. Go to Agency → Settings → API Access.
  2. Find the key by its masked prefix.
  3. Click Revoke.
  4. Confirm.

Via the REST API

DELETE /api/v1/agency/{billingAccountId}/api-keys/{keyId}/revoke
X-Api-Key: cpa_agy_...

Response:

{
  "data": {
    "id": "550e8400-...",
    "name": "CI Pipeline Key",
    "maskedKey": "cpa_agy_3a7f9c002b11_••••••••",
    "revokedAt": "2026-05-23T15:00:00.000Z"
  }
}

Revocation is instantaneous — any request carrying the revoked key immediately receives 401 Unauthorized. This action cannot be undone. Generate a new key if the integration still needs access.


Complete API endpoint reference

Replace {billingAccountId} with your agency billing account UUID (found in Agency → Settings → General).

Agency account

MethodPathDescription
GET/api/v1/agency/accountsList all agency billing accounts the authenticated user administers.
GET/api/v1/agency/{billingAccountId}/settingsGet agency account details and branding configuration.
PATCH/api/v1/agency/{billingAccountId}/settingsUpdate white-label branding (name, logo).

API key management

MethodPathDescription
GET/api/v1/agency/{billingAccountId}/api-keysList all keys (masked) for this billing account.
POST/api/v1/agency/{billingAccountId}/api-keysCreate a new key. Returns plainTextKey once.
DELETE/api/v1/agency/{billingAccountId}/api-keys/{keyId}/revokeRevoke a key immediately.

Client management

MethodPathDescription
GET/api/v1/agency/{billingAccountId}/clientsList all linked client orgs with health scores. Supports ?status=, ?tag=, ?sortBy=.
POST/api/v1/agency/{billingAccountId}/clientsCreate a new client org.
POST/api/v1/agency/{billingAccountId}/clients/inviteInvite an existing org to join as a client.
GET/api/v1/agency/{billingAccountId}/clients/{clientId}Get a single client org with full health detail.
PATCH/api/v1/agency/{billingAccountId}/clients/{clientId}Update client name, notes, tags, contact info.
POST/api/v1/agency/{billingAccountId}/clients/{clientId}/allocationsSet resource quotas for a client org.
POST/api/v1/agency/{billingAccountId}/clients/{clientId}/reportsGenerate a client report for a time window.
GET/api/v1/agency/{billingAccountId}/clients/{clientId}/reportsList reports for a client.

Aggregated cross-org views

MethodPathDescription
GET/api/v1/agency/{billingAccountId}/overviewSummary: client count, server count, active alerts, cost.
GET/api/v1/agency/{billingAccountId}/serversAll servers across all client orgs. Supports ?orgId=, ?status=, ?search=, ?cursor=, ?limit=.
GET/api/v1/agency/{billingAccountId}/sitesAll sites and apps. Supports ?orgId=, ?status=, ?cursor=, ?limit=.
GET/api/v1/agency/{billingAccountId}/databasesAll managed databases. Supports ?orgId=, ?engine=, ?status=, ?cursor=, ?limit=.
GET/api/v1/agency/{billingAccountId}/backupsAll backups. Supports ?orgId=, ?status=, ?cursor=, ?limit=.
GET/api/v1/agency/{billingAccountId}/alertsAll firing alerts. Supports ?orgId=, ?severity=, ?status=, ?cursor=, ?limit=.
GET/api/v1/agency/{billingAccountId}/activityAudit log events. Supports ?orgId=, ?action=, ?cursor=, ?limit=.

Notifications

MethodPathDescription
GET/api/v1/agency/{billingAccountId}/notificationsPaginated notification feed. Supports ?limit=, ?page=, ?unreadOnly=, ?orgId=.
GET/api/v1/agency/{billingAccountId}/notifications/unread-countFast count of unread notifications.
PATCH/api/v1/agency/{billingAccountId}/notifications/{id}/readMark one notification as read.
POST/api/v1/agency/{billingAccountId}/notifications/mark-all-readMark all notifications as read (optionally scoped by ?orgId=).

Cursor-based pagination

All list endpoints that can return large result sets use cursor-based pagination. Pass the nextCursor value from one response as ?cursor= in the next request.

GET /api/v1/agency/{billingAccountId}/servers?limit=50

Response:

{
  "data": [ ... ],
  "nextCursor": "server-uuid-last-item"
}

Next page:

GET /api/v1/agency/{billingAccountId}/servers?limit=50&cursor=server-uuid-last-item

When nextCursor is null, you have reached the last page. Default page size is 50; maximum is 100 (activity log: maximum 200).


Filtering and searching

Most list endpoints accept query-string parameters to narrow results.

ParameterApplies toExample
orgIdservers, sites, databases, backups, alerts, activity, notifications?orgId=abc-123 — scope to one client org
statusservers, sites, databases, backups, alerts?status=running
severityalerts?severity=critical
enginedatabases?engine=postgresql
searchservers (hostname), sites (name), databases (name)?search=prod
tagclients?tag=priority
sortByclients?sortBy=health or ?sortBy=name
cursorall list endpointscursor value from previous response
limitall list endpointsdefault 50, max 100 (activity: max 200)
unreadOnlynotifications?unreadOnly=true

Filters can be combined: ?status=firing&severity=critical&orgId=abc-123.


HTTP error reference

StatusCodeMeaning
200 OKSuccess.
201 CreatedKey created.
400 Bad RequestVALIDATION_ERRORRequest body failed schema validation. Check the details array.
401 UnauthorizedUNAUTHORIZEDKey is missing, malformed, revoked, or belongs to a different billing account.
403 ForbiddenFORBIDDENThe authenticated user is not an owner/admin of this billing account.
404 Not FoundNOT_FOUNDKey ID or resource not found.
402 Payment RequiredCLIENT_QUOTA_EXCEEDEDPlan limit reached (for example: max client orgs).

All error responses follow this format:

{
  "error": {
    "code": "FORBIDDEN",
    "message": "You do not have agency admin access to this billing account"
  }
}

Practical integration examples

Shell / cURL

export CLOUDPILOT_API_KEY="cpa_agy_a1b2c3d4e5f6_..."
export BILLING_ACCOUNT_ID="your-billing-account-uuid"
BASE="https://app.cloudaipilot.com"

# List all clients
curl -s -H "X-Api-Key: $CLOUDPILOT_API_KEY" \
  "$BASE/api/v1/agency/$BILLING_ACCOUNT_ID/clients" | jq .

# Get all firing critical alerts
curl -s -H "X-Api-Key: $CLOUDPILOT_API_KEY" \
  "$BASE/api/v1/agency/$BILLING_ACCOUNT_ID/alerts?severity=critical&status=firing" | jq .

# Paginate through all servers
cursor=""
while true; do
  url="$BASE/api/v1/agency/$BILLING_ACCOUNT_ID/servers?limit=50"
  [ -n "$cursor" ] && url="$url&cursor=$cursor"
  response=$(curl -s -H "X-Api-Key: $CLOUDPILOT_API_KEY" "$url")
  echo "$response" | jq '.data[]'
  cursor=$(echo "$response" | jq -r '.nextCursor // empty')
  [ -z "$cursor" ] && break
done

Node.js / TypeScript

const CLOUDPILOT_API_KEY = process.env.CLOUDPILOT_API_KEY!;
const BILLING_ACCOUNT_ID = process.env.CLOUDPILOT_BILLING_ACCOUNT_ID!;
const BASE = "https://app.cloudaipilot.com";

async function agencyFetch<T>(path: string): Promise<T> {
  const res = await fetch(`${BASE}${path}`, {
    headers: { "X-Api-Key": CLOUDPILOT_API_KEY },
  });
  if (!res.ok) throw new Error(`API error: ${res.status} ${await res.text()}`);
  return res.json();
}

// Get agency overview
const overview = await agencyFetch<{ data: { clientCount: number; serverCount: number } }>(
  `/api/v1/agency/${BILLING_ACCOUNT_ID}/overview`
);
console.log(`Clients: ${overview.data.clientCount}, Servers: ${overview.data.serverCount}`);

// Paginate through all firing alerts
async function* paginateAlerts(severity?: string) {
  let cursor: string | null = null;
  do {
    const qs = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}), ...(severity ? { severity } : {}) });
    const page = await agencyFetch<{ data: unknown[]; nextCursor: string | null }>(
      `/api/v1/agency/${BILLING_ACCOUNT_ID}/alerts?${qs}`
    );
    yield* page.data;
    cursor = page.nextCursor;
  } while (cursor !== null);
}

for await (const alert of paginateAlerts("critical")) {
  console.log(alert);
}

Python

import os
import requests
from typing import Generator, Any

API_KEY = os.environ["CLOUDPILOT_API_KEY"]
BILLING_ACCOUNT_ID = os.environ["CLOUDPILOT_BILLING_ACCOUNT_ID"]
BASE = "https://app.cloudaipilot.com"
HEADERS = {"X-Api-Key": API_KEY}

def agency_get(path: str, **params) -> dict:
    r = requests.get(f"{BASE}{path}", headers=HEADERS, params=params)
    r.raise_for_status()
    return r.json()

def paginate(path: str, **params) -> Generator[Any, None, None]:
    cursor = None
    while True:
        data = agency_get(path, limit=100, **({"cursor": cursor} if cursor else {}), **params)
        yield from data["data"]
        cursor = data.get("nextCursor")
        if not cursor:
            break

# Get all clients
clients = agency_get(f"/api/v1/agency/{BILLING_ACCOUNT_ID}/clients")["data"]
for c in clients:
    print(f"{c['name']}: health {c.get('healthScore', 'n/a')}%")

# Get all firing critical alerts across every client org
for alert in paginate(f"/api/v1/agency/{BILLING_ACCOUNT_ID}/alerts", severity="critical", status="firing"):
    print(f"[{alert['org']['name']}] CRITICAL: {alert['rule']['metric']}")

GitHub Actions — nightly report

name: Nightly Agency Report
on:
  schedule:
    - cron: '0 6 * * *'

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch agency overview
        run: |
          curl -sf \
            -H "X-Api-Key: ${{ secrets.CLOUDPILOT_API_KEY }}" \
            "https://app.cloudaipilot.com/api/v1/agency/${{ secrets.CLOUDPILOT_BILLING_ACCOUNT_ID }}/overview" \
            | jq '.data'

      - name: Check for critical alerts
        run: |
          COUNT=$(curl -sf \
            -H "X-Api-Key: ${{ secrets.CLOUDPILOT_API_KEY }}" \
            "https://app.cloudaipilot.com/api/v1/agency/${{ secrets.CLOUDPILOT_BILLING_ACCOUNT_ID }}/alerts?severity=critical&status=firing" \
            | jq '.data | length')
          echo "Critical alerts: $COUNT"
          if [ "$COUNT" -gt "0" ]; then
            echo "::warning::$COUNT critical alert(s) are firing across client orgs"
          fi

n8n / Zapier / Make (no-code automation)

All three platforms support HTTP request nodes. Use the following settings:

FieldValue
MethodGET
URLhttps://app.cloudaipilot.com/api/v1/agency/{billingAccountId}/clients
Header nameX-Api-Key
Header valueYour key from Agency → Settings → API Access
Response formatJSON

The data array from any list endpoint maps naturally to n8n's item-splitting or Zapier's looping features.


Security best practices

One key per integration. Create a separate named key for each system that calls the API. When you retire an integration, revoke its key — other integrations are unaffected.

Store keys in a secrets manager. Never hardcode a key in source code, a config file, or a repository. Use a dedicated secrets tool: AWS Secrets Manager, HashiCorp Vault, GitHub Actions Secrets, Doppler, 1Password Secrets Automation, or similar.

Never commit a key to git. Use .gitignore and a tool such as git-secrets or trufflehog to catch accidental commits.

Rotate keys periodically. Create a new key, update your secrets store, verify the integration works, then revoke the old key. CloudAIPilot writes each creation and revocation to the audit log, so rotations are traceable.

Monitor lastUsedAt. Review the key list in Agency Settings occasionally. A key unused for months may be safe to revoke. An unexpected spike in activity could indicate a key has been shared unintentionally.

Never use agency keys in client-facing code. Agency API keys must stay server-side. Never include them in browser-accessible JavaScript, mobile app bundles, or any code that ships to end users.


API keys vs. browser session access

DimensionBrowser sessionAgency API key
ExpiryTypically 7–30 days; requires re-loginNo expiry; revoke manually
Server-to-server useNot practicalDesigned for this
RotationTied to user login lifecycleIndependent; create and revoke without logging out
Audit trailTied to user IDTied to creator's user ID; lastUsedAt tracked per key
CI/CD and automationImpracticalFirst-class use case
Multiple integrationsOne shared sessionOne key per integration, independently revocable
ScopeUser's full accountAgency billing account only

What you can build with agency API access

If you are new to APIs, here is a plain-English explanation of what the agency API actually enables — and some concrete things you can build.

Think of CloudAIPilot as a building. The dashboard is the front door — you log in, click around, and manage things visually. The API is a side door to the same building. Your own software can walk in through that side door, ask for data, and give instructions — automatically, without anyone clicking anything.

Real integrations people build

A custom agency dashboard. You want your company's branding, extra columns, or a completely different layout. Build a private web page or internal tool that fetches data from the Agency API and displays it however you want.

An automated morning health report. Every morning at 8 AM, a script fetches the /overview endpoint (client counts, server counts) and the /alerts?severity=critical endpoint, then sends a formatted summary to Slack, WhatsApp Business, or email. Your team starts the day knowing exactly what needs attention — without logging into anything.

A client-facing status page. Pull server and alert data from the API and display a simplified "all systems green / one issue detected" page for your clients — under your own domain, with your own branding. CloudAIPilot runs invisibly in the background.

A billing integration. Pull cost data from the agency overview and push it into your invoicing or accounting software automatically. No more manual data entry at month end.

An automated client onboarding pipeline. When a new client signs up on your website, your server calls the Agency API to create a new client org, assign resource quotas, and configure branding — all before any human has touched CloudAIPilot. The client goes from "signed up" to "fully provisioned" in seconds.

A CI/CD monitoring gate. Before promoting code to production for a client, your pipeline queries the client's server health and alert status. If critical alerts are active, the deployment is blocked automatically until the issue is resolved.

An operations bot. A Slack or Microsoft Teams bot that answers: "How many servers does Acme Corp have running?" The bot calls the Agency API, formats the answer, and replies in the chat thread.

A white-labelled client portal. You build a web application under your own domain (for example, portal.youragency.com). When clients log in, they see their servers, backups, and alerts — all sourced from CloudAIPilot via the API, all styled with your branding. CloudAIPilot is the infrastructure engine they never see.

The key insight

CloudAIPilot manages the hard parts — talking to cloud providers, running backup jobs, collecting metrics, monitoring servers, and storing data securely. You use the API as a bridge to that engine and build whatever experience makes sense for your business. You are not locked into the CloudAIPilot interface as your only interface.


Frequently asked questions

I lost the key. Can I retrieve it? No. The full key is shown only once, immediately after creation. Revoke the lost key and create a new one.

Can I create more than one key? Yes. There is no fixed limit on the number of active keys per billing account. Name each key clearly so you know which integration it belongs to.

Does revoking a key affect the user account that created it? No. Revoking a key only blocks API requests that carry that key. The user's login session, other keys they created, and their org membership are all unaffected.

Can a key be used to create or revoke other keys? Yes. The key management endpoints (GET, POST, DELETE /api-keys) are protected by the same middleware as all other agency endpoints. A valid key can therefore create additional keys or revoke itself — useful for automated rotation scripts. Ensure such a key is stored especially securely.

Does using an API key affect my plan quotas? No. API keys are authentication credentials, not a billable resource. Quotas (max client orgs, max servers, and so on) are enforced per plan regardless of whether the request comes from a browser session or an API key.

Is HTTPS required? Yes in production. All requests to https://app.cloudaipilot.com are TLS-encrypted. Never transmit API keys over plain HTTP in production.

What will be added to agency API keys in future releases? The Phase 23/24 roadmap includes: optional key expiry/TTL, per-key scope restrictions (read-only keys), IP allow-list per key, webhook payload signing with key material, and independent rate limits per key.


Quick-start checklist

  • [ ] Log into CloudAIPilot and open Agency Mode.
  • [ ] Go to Agency → Settings → API Access and create a key with a descriptive name.
  • [ ] Copy the full key immediately and store it in your secrets manager.
  • [ ] Verify connectivity: curl -s -H "X-Api-Key: YOUR_KEY" https://app.cloudaipilot.com/api/v1/agency/YOUR_BILLING_ACCOUNT_ID/overview | jq .
  • [ ] Build your integration using the endpoint reference above.
  • [ ] Create one key per integration and name them clearly.
  • [ ] Schedule a periodic review of key lastUsedAt timestamps and revoke any that have gone unused.

Related articles