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 type | Prefix | Scope |
|---|---|---|
| Standard organisation API key | cap_ | One organisation |
| Agency API key | cpa_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?
| Role | Generate keys? | Revoke keys? | Use keys to call the API? |
|---|---|---|---|
| Agency billing account Owner | Yes | Yes | Yes |
| Agency billing account Admin | Yes | Yes | Yes |
| Agency billing account Member / Viewer | No | No | Yes (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
| Segment | Purpose |
|---|---|
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 ID | Key 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 secret | The 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
}
}
plainTextKeyis 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
- Go to Agency → Settings → API Access.
- Find the key by its masked prefix.
- Click Revoke.
- 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
| Method | Path | Description |
|---|---|---|
GET | /api/v1/agency/accounts | List all agency billing accounts the authenticated user administers. |
GET | /api/v1/agency/{billingAccountId}/settings | Get agency account details and branding configuration. |
PATCH | /api/v1/agency/{billingAccountId}/settings | Update white-label branding (name, logo). |
API key management
| Method | Path | Description |
|---|---|---|
GET | /api/v1/agency/{billingAccountId}/api-keys | List all keys (masked) for this billing account. |
POST | /api/v1/agency/{billingAccountId}/api-keys | Create a new key. Returns plainTextKey once. |
DELETE | /api/v1/agency/{billingAccountId}/api-keys/{keyId}/revoke | Revoke a key immediately. |
Client management
| Method | Path | Description |
|---|---|---|
GET | /api/v1/agency/{billingAccountId}/clients | List all linked client orgs with health scores. Supports ?status=, ?tag=, ?sortBy=. |
POST | /api/v1/agency/{billingAccountId}/clients | Create a new client org. |
POST | /api/v1/agency/{billingAccountId}/clients/invite | Invite 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}/allocations | Set resource quotas for a client org. |
POST | /api/v1/agency/{billingAccountId}/clients/{clientId}/reports | Generate a client report for a time window. |
GET | /api/v1/agency/{billingAccountId}/clients/{clientId}/reports | List reports for a client. |
Aggregated cross-org views
| Method | Path | Description |
|---|---|---|
GET | /api/v1/agency/{billingAccountId}/overview | Summary: client count, server count, active alerts, cost. |
GET | /api/v1/agency/{billingAccountId}/servers | All servers across all client orgs. Supports ?orgId=, ?status=, ?search=, ?cursor=, ?limit=. |
GET | /api/v1/agency/{billingAccountId}/sites | All sites and apps. Supports ?orgId=, ?status=, ?cursor=, ?limit=. |
GET | /api/v1/agency/{billingAccountId}/databases | All managed databases. Supports ?orgId=, ?engine=, ?status=, ?cursor=, ?limit=. |
GET | /api/v1/agency/{billingAccountId}/backups | All backups. Supports ?orgId=, ?status=, ?cursor=, ?limit=. |
GET | /api/v1/agency/{billingAccountId}/alerts | All firing alerts. Supports ?orgId=, ?severity=, ?status=, ?cursor=, ?limit=. |
GET | /api/v1/agency/{billingAccountId}/activity | Audit log events. Supports ?orgId=, ?action=, ?cursor=, ?limit=. |
Notifications
| Method | Path | Description |
|---|---|---|
GET | /api/v1/agency/{billingAccountId}/notifications | Paginated notification feed. Supports ?limit=, ?page=, ?unreadOnly=, ?orgId=. |
GET | /api/v1/agency/{billingAccountId}/notifications/unread-count | Fast count of unread notifications. |
PATCH | /api/v1/agency/{billingAccountId}/notifications/{id}/read | Mark one notification as read. |
POST | /api/v1/agency/{billingAccountId}/notifications/mark-all-read | Mark 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.
| Parameter | Applies to | Example |
|---|---|---|
orgId | servers, sites, databases, backups, alerts, activity, notifications | ?orgId=abc-123 — scope to one client org |
status | servers, sites, databases, backups, alerts | ?status=running |
severity | alerts | ?severity=critical |
engine | databases | ?engine=postgresql |
search | servers (hostname), sites (name), databases (name) | ?search=prod |
tag | clients | ?tag=priority |
sortBy | clients | ?sortBy=health or ?sortBy=name |
cursor | all list endpoints | cursor value from previous response |
limit | all list endpoints | default 50, max 100 (activity: max 200) |
unreadOnly | notifications | ?unreadOnly=true |
Filters can be combined: ?status=firing&severity=critical&orgId=abc-123.
HTTP error reference
| Status | Code | Meaning |
|---|---|---|
200 OK | — | Success. |
201 Created | — | Key created. |
400 Bad Request | VALIDATION_ERROR | Request body failed schema validation. Check the details array. |
401 Unauthorized | UNAUTHORIZED | Key is missing, malformed, revoked, or belongs to a different billing account. |
403 Forbidden | FORBIDDEN | The authenticated user is not an owner/admin of this billing account. |
404 Not Found | NOT_FOUND | Key ID or resource not found. |
402 Payment Required | CLIENT_QUOTA_EXCEEDED | Plan 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:
| Field | Value |
|---|---|
| Method | GET |
| URL | https://app.cloudaipilot.com/api/v1/agency/{billingAccountId}/clients |
| Header name | X-Api-Key |
| Header value | Your key from Agency → Settings → API Access |
| Response format | JSON |
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
| Dimension | Browser session | Agency API key |
|---|---|---|
| Expiry | Typically 7–30 days; requires re-login | No expiry; revoke manually |
| Server-to-server use | Not practical | Designed for this |
| Rotation | Tied to user login lifecycle | Independent; create and revoke without logging out |
| Audit trail | Tied to user ID | Tied to creator's user ID; lastUsedAt tracked per key |
| CI/CD and automation | Impractical | First-class use case |
| Multiple integrations | One shared session | One key per integration, independently revocable |
| Scope | User's full account | Agency 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
lastUsedAttimestamps and revoke any that have gone unused.