Authentication
API key authentication — generation, usage, and security.
Authentication
HeyCMO uses API key authentication. Every authenticated request must include your API key.
API Key Format
All API keys use the hcmo_live_ prefix followed by a 64-character hex string:
hcmo_live_a1b2c3d4e5f6789...Authentication Methods
HeyCMO uses two authentication methods depending on the endpoint:
Bearer Token (REST API)
For REST API endpoints (/api/*), include the key as a Bearer token in the Authorization header:
curl -X GET https://heycmo.ai/api/customer/CUSTOMER_ID \
-H "Authorization: Bearer hcmo_live_YOUR_KEY"The API extracts the key from the Authorization header by stripping the Bearer prefix.
Token Query Parameter (MCP)
For MCP endpoints (/mcp/sse, /mcp/message), pass the key as a token query parameter:
https://heycmo.ai/mcp/sse?token=hcmo_live_YOUR_KEYThis is the format used in Claude Desktop and Cursor MCP configuration.
Security Architecture
Key Generation
- Keys are generated using 32 bytes of cryptographically secure random data (
crypto.randomBytes) - The full key is shown once at generation time and cannot be retrieved afterward
Key Storage
- Keys are never stored in plaintext on the server
- Only the SHA-256 hash of the key is stored in the database
- Validation uses timing-safe comparison (
crypto.timingSafeEqual) to prevent timing attacks
Encryption
- Sensitive data at rest is encrypted with AES-256-GCM
- Encryption uses scrypt-derived keys with a unique IV per ciphertext
- Authentication tags prevent tampering
Customer Validation
After key validation, the system checks that the customer account is in an active or onboarding state. Inactive accounts are rejected even with a valid key.
Rate Limiting
All endpoints have rate limiting applied per API key or IP address:
| Scope | Limit |
|---|---|
| REST API | 60 requests/minute |
| MCP | 30 requests/minute per customer |
| Pre-auth | 20 attempts/minute (brute-force protection) |
Best Practices
- Never expose your API key in client-side code, public repositories, or logs
- Use environment variables to store your key in server applications
- Rotate keys periodically — generate a new key and revoke the old one
- Use separate keys for development and production environments
- Monitor usage via the dashboard to detect unauthorized access
For AI agents — programmatic key handling
If you're an AI agent (Claude, ChatGPT, Cursor, Devin, custom orchestrator) integrating with HeyCMO, see the dedicated For AI agents guide. The short version:
1. Obtain a key
There's no fully-programmatic key issuance today — a human still needs to complete Stripe checkout. The flow:
# Create a checkout session (no auth required)
curl -X POST https://heycmo.ai/api/checkout \
-H "Content-Type: application/json" \
-d '{"email":"agent-operator@example.com"}'
# → { "url": "https://checkout.stripe.com/c/pay/..." }The customer completes checkout in a browser, and the API key is delivered via the Stripe-redirect handler. After completion:
curl https://heycmo.ai/api/session/SESSION_ID
# → { "customerId": "...", "apiKey": "hcmo_live_...", "mcpEndpoint": "..." }Store the key in your agent's secret store and use it on every request.
For team-scoped agents, the customer can issue per-team-member keys from /team after signing in. Team-member keys validate the same way (Bearer header) and are restricted by role.
2. Discover auth requirements via well-known files
Per RFC 9728:
curl https://heycmo.ai/.well-known/oauth-protected-resourceCurrently declares:
{
"resource": "https://heycmo.ai",
"authorization_servers": [],
"bearer_methods_supported": ["header"],
"scopes_supported": [
"read:content", "write:content", "publish:social", "read:analytics", ...
]
}OAuth 2.0 is on the roadmap; today the empty authorization_servers array is intentional.
3. Retry strategy for 429s
Every 429 response includes:
HTTP/1.1 429 Too Many Requests
Retry-After: 13
X-Request-ID: 6e2f....
Content-Type: application/json
{ "error": "Rate limit exceeded", "code": "E_RATE_LIMITED", "requestId": "6e2f..." }Recommended backoff (Node):
async function call(url: string, init: RequestInit, attempt = 0): Promise<Response> {
const res = await fetch(url, init);
if (res.status === 429 && attempt < 5) {
const retryAfter = Number(res.headers.get('retry-after') ?? 1);
const jitter = Math.random() * 250;
await new Promise(r => setTimeout(r, retryAfter * 1000 + jitter));
return call(url, init, attempt + 1);
}
return res;
}Recommended backoff (Python):
import time, random, requests
def call(method, url, **kwargs):
for attempt in range(5):
r = requests.request(method, url, **kwargs)
if r.status_code == 429:
retry = int(r.headers.get('Retry-After', '1'))
time.sleep(retry + random.uniform(0, 0.25))
continue
return r
return r4. Quote requestId in support messages
Every response includes an X-Request-ID header (also returned in error JSON bodies). Quote this when contacting support so we can find the request in our logs without a needle-in-haystack search.
5. CLI
The official CLI handles login + per-call auth automatically:
npm install -g @heycmo/cli
heycmo auth login # paste hcmo_live_… key when prompted
heycmo auth whoami # confirms which key + endpoint
heycmo scan https://example.com
heycmo content list
heycmo mcp config # prints Claude/Cursor/Windsurf snippetSource: packages/cli/ in the repo.
Error Responses
Missing API Key
// HTTP 401
{
"error": "Authorization header required"
}Invalid API Key
// HTTP 401
{
"error": "Invalid API key"
}MCP Token Missing
// HTTP 401
{
"error": "MCP token required. Include ?token=hcmo_live_..."
}Inactive Account
// HTTP 401
{
"error": "Customer account is not active"
}Rate Limited
// HTTP 429
{
"error": "Rate limit exceeded. Try again later."
}