Audience: developers. API keys need a Pro or Enterprise plan (legacy Pro and Scale plans included) or an AppSumo Tier 5, 6 or 7 license, and they never work during a trial. Support agent: confirm the user’s plan grants API access before giving API advice.
TL;DR: Every API v2 request uses Authorization: Bearer clapi_... (or a CluGTM CLI sign-in, or a Clerk session JWT). On top of the token, each endpoint checks one OAuth scope (for example enrich:write); a missing scope returns 403 insufficient_scope. Sends, CRM writes and deletes also need an opt-in scope ticked on the key. Create keys under Settings → API keys → Create API key. Up to 10 active keys per user per organization.
Authentication
CluGTM API v2 accepts three credential types:
| Credential | Header | Used by | whoami.auth_type |
|---|---|---|---|
API key (clapi_...) | Authorization: Bearer clapi_... | servers, scripts, your own agents | api_key |
| CluGTM CLI sign-in (OAuth access token) | Authorization: Bearer <token> | the CluGTM CLI after cleanlist login | oauth |
| Clerk session JWT | Authorization: Bearer <jwt> | the CluGTM app, and the /api/v1/api-keys management endpoints | clerk_jwt |
OAuth access tokens are refused with 401 and the message “OAuth access tokens are not accepted here. Use an API key.” The one exception is the CluGTM CLI: its token is accepted on /api/v2/ and /api/v1/public/ only, for the workspace picked at sign-in. It carries your scopes in that workspace, less the opt-in scopes, and needs the same plan as an API key. It has its own rate-limit bucket of 30 requests per minute for each user, like a key (see Rate limits). Each CLI token is checked with the sign-in service, at most a minute apart, so a sign-out (cleanlist logout) or a replaced token stops working within 60 seconds with 401 “This sign-in was revoked or replaced. Sign in again.”
Authorization: Bearer clapi_your_api_key_hereThe CluGTM MCP for Claude doesn’t use an API key: it signs in with your CluGTM account through OAuth and carries the same scopes, less the opt-in ones. For a delete it asks you in the host app’s approval prompt instead.
Who can create API keys
| Plan | API keys |
|---|---|
| Pro, Enterprise | Yes |
| Legacy Pro, legacy Scale (shown as Enterprise) | Yes |
| AppSumo Tiers 5 to 9 | Yes |
| Free, Starter (current or legacy) | No |
| AppSumo Tiers 1 to 4 | No |
| Any trial, including the Pro trial | No |
On a plan without API access, the API keys page shows an upgrade card (“The CluGTM API.”) instead of the create form.
OAuth scopes
Authentication answers “who are you?”; scopes answer “what may you do?”. Every /api/v2 endpoint checks one scope before it runs. If your credential doesn’t carry it, the request fails with 403 and code insufficient_scope.
| Scope | Grants |
|---|---|
people:read | People Finder, people filters, compile, find-at-companies, find-people status |
people:write | Reserved; no endpoint checks it today |
companies:read | Company search, company filters, similar companies |
companies:write | Reserved; no endpoint checks it today |
lists:read | Read lead lists and their leads; read actions in the lists and skills groups |
lists:write | Create and update lists, add leads, CSV import, find-people jobs; other actions in the lists and skills groups. Deleting a list or removing leads also needs lists:delete |
enrich:read | Poll enrichment status |
enrich:write | Run person, company, by-task and bulk enrichment |
smart_agents:read | List smart-agent runs and read results; read GTM agents, their runs and records; read actions in the agents and plays groups |
smart_agents:write | Run a smart agent; create, edit and turn off GTM agents; other actions in the agents and plays groups |
sync:write | Sync a list to a CRM or sequencer |
export:read | CSV signed URLs and JSON export |
credits:read | Credit balance and cost estimates |
admin:api_keys | List keys and read usage reports through the API; manage webhook subscriptions |
crm:read | Read CRM records through CluGTM (CRM exclusions in company search, the CluGTM MCP); read actions in the crm group |
sequences:read | Read actions in the sequences group: sequences, mailboxes, LinkedIn accounts, a lead’s calls |
sequences:write | Opt-in per key. Every other action in the sequences group: create and edit sequences, launch, pause and resume them, enroll people, send email and LinkedIn actions |
crm:write | Opt-in per key. Every other action in the crm group: log a CRM task or note, merge, dismiss or skip duplicates, add field mappings |
lists:delete | Opt-in per key. DELETE /lead-lists/{list_id}, DELETE /lead-lists/{list_id}/leads, and the lists.delete and lists.delete_leads actions |
Your user has a set of scopes in each organization, and a key holds those scopes narrowed to the key’s own list. A key created without a list of its own (every key made before per-key scopes, and any key created with no boxes ticked) holds your scopes less the three opt-in scopes, so it can’t send, write to your CRM or delete lists and leads. A user granted the full set before crm:read and sequences:read existed receives both automatically, but never an opt-in scope.
The five legacy v1-only endpoints (/api/v1/public/auth/validate-key, /enrich/bulk, /enrich/status, /webhooks/deliveries, POST /folders) don’t check scopes. Every v2 endpoint does.
A 403 insufficient_scope error includes the required and granted scopes in details:
{
"error": {
"code": "insufficient_scope",
"problem": "This call requires the 'enrich:write' scope, which is not granted to the authenticating credential.",
"fix": "Either re-issue the API key with the required scope, or sign in with an identity that has it. The full list of granted scopes is visible via /api/v2/whoami.",
"retryable": false,
"docs_url": "https://docs.cleanlist.ai/errors/insufficient-scope",
"request_id": "req_...",
"details": { "required_scope": "enrich:write", "granted_scopes": ["lists:read", "people:read"] }
}
}To see what your credential can do, call GET /whoami; it returns the granted scopes list.
Opt-in scopes
Sends, CRM writes and deletes reach outside CluGTM or can’t be undone, so an API key holds their scopes only when someone opts in on that key:
| Box in the app | Scope | Lets the key |
|---|---|---|
| Send email and LinkedIn, and change sequences | sequences:write | Use the sequences actions that aren’t reads |
| Write to your CRM | crm:write | Use the crm actions that aren’t reads |
| Delete lead lists and leads | lists:delete | Delete lists and remove leads, by REST or by action |
To opt in, open Settings → API keys. When you create a key, tick the boxes under This key may also. For an existing key, click Permissions on it under Your API keys, tick the boxes and click Save. Each key shows the opt-in scopes it holds. A key never holds more than your user’s scopes; with the full set, you can opt into all three.
Only a person signed in to CluGTM sets a key’s scopes. An API key, a CluGTM CLI sign-in or the CluGTM MCP that tries returns 403 “Set an API key’s scopes in CluGTM, signed in.”, so no credential can widen its own reach.
A call that needs an opt-in scope the key lacks fails with 403 opt_in_scope_required and details.required_scope, before anything runs:
{
"error": {
"code": "opt_in_scope_required",
"problem": "Deleting a list or its leads needs a key that opted into 'lists:delete'.",
"fix": "Use a key with the 'lists:delete' scope turned on, or delete it in Cleanlist.",
"retryable": false,
"docs_url": "https://docs.cleanlist.ai/errors/opt-in-scope-required",
"request_id": "req_...",
"details": { "required_scope": "lists:delete" }
}
}The opt-in scopes are set per API key. A CluGTM CLI browser or device sign-in doesn’t get them; to delete lists or leads from the CLI, sign in with an API key that has lists:delete (cleanlist login --api-key -). The CluGTM MCP asks the person instead: it runs a delete only after the person approves it in the host app’s own approval prompt.
Key format
| Property | Value |
|---|---|
| Prefix | clapi_ |
| Ownership | The user who created it, in the organization it was created in |
| Secret body | 48 random URL-safe bytes |
| Visibility | Shown once at creation; afterwards only the prefix and last 4 characters are displayed |
| Limit | 10 active keys per user per organization |
The full key is only returned once, at creation. Store it in a secret manager, env var or vault. If you lose it, revoke it and create a new one.
Creating a key
The fastest path is the app:
- Go to Settings → API keys.
- Under Create API key, enter a Name (for example “CI integration”) and, if you want, an Expiry.
- If the key must send, write to your CRM or delete, tick those boxes under This key may also (see Opt-in scopes). Each is off unless you tick it.
- Click Create API key and copy the key shown under New API key.
Your keys are listed under Your API keys with their status and request count.
You can also create keys programmatically with your Clerk session token. These management endpoints live at /api/v1/api-keys (not under /public or /api/v2) and authenticate with the Clerk session JWT, not a clapi_ key.
Creating and listing keys, and changing their scopes, need API access on your plan (see Who can create API keys). Without it, POST /api/v1/api-keys returns 403 with code PERMISSION_DENIED, the message “This feature requires the Pro plan or higher.” and details.error: "feature_gated". Revoking keys and reading a key’s request log are not gated.
POST /api/v1/api-keys
curl -X POST https://api.cleanlist.ai/api/v1/api-keys \
-H "Authorization: Bearer <clerk_session_jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "Production server",
"expires_at": "2027-01-01T00:00:00Z"
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | A human-readable label |
expires_at | ISO-8601 datetime | No | Optional expiration; omit for a non-expiring key |
scopes | string[] | No | The key’s own scope list. The key holds your scopes narrowed to it, so include every scope the key needs, not only the opt-in ones. Omit it for your scopes less the opt-in scopes. An unknown scope returns 422 VALIDATION_ERROR naming it. Only an app session can send it; any other credential gets 403 |
Response
{
"api_key": "clapi_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"created_at": "2026-04-06T15:00:00Z",
"expires_at": "2027-01-01T00:00:00Z"
}If you already have 10 active keys in this organization, creation returns 400 with “Maximum active API keys reached (10). Revoke an existing key before creating a new one.”
Listing your keys
GET /api/v1/api-keys
Lists your active and inactive keys in the current organization (Clerk session JWT, needs API access on the plan). The full secret is never returned, only the first 10 characters (key_prefix) and last 4 (key_last4).
curl https://api.cleanlist.ai/api/v1/api-keys \
-H "Authorization: Bearer <clerk_session_jwt>"[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Production server",
"key_prefix": "clapi_AbCd",
"key_last4": "wXyZ",
"created_at": "2026-04-06T15:00:00Z",
"expires_at": null,
"last_used_at": "2026-04-06T15:42:11Z",
"request_count": 1547,
"is_active": true,
"scopes": null
}
]scopes is the key’s own list, or null for a key that has none: it holds your scopes less the opt-in scopes.
There is also a clapi_-authenticated GET /api/v2/api-keys (scope admin:api_keys) that returns key_id, key_prefix, key_last4, name, scopes, created_at, last_used_at and is_active for each key. Its scopes field shows the calling credential’s scopes on every key, not each key’s own; read a key’s own list from GET /api/v1/api-keys, or call GET /whoami with that key.
Changing a key’s scopes
In the app, click Permissions on the key under Your API keys, tick or clear the boxes, and click Save.
PUT /api/v1/api-keys/{key_id}/scopes
Sets one of your active keys’ scope list (Clerk session JWT only).
curl -X PUT https://api.cleanlist.ai/api/v1/api-keys/550e8400-e29b-41d4-a716-446655440000/scopes \
-H "Authorization: Bearer <clerk_session_jwt>" \
-H "Content-Type: application/json" \
-d '{"scopes": ["people:read", "lists:read", "lists:write", "lists:delete"]}'The body is {"scopes": [...]}, with the same rules as scopes on create. {"scopes": null} puts the key back on your scopes less the opt-in scopes. The response is the key, as GET /api/v1/api-keys lists it. A key that isn’t yours, or isn’t active, returns 404 “Active API key not found”.
Revoking keys
In the app, open Settings → API keys, find the key under Your API keys and click Revoke, or click Revoke all.
POST /api/v1/api-keys/{key_id}/revoke
Revoke a single key by its UUID:
curl -X POST https://api.cleanlist.ai/api/v1/api-keys/550e8400-e29b-41d4-a716-446655440000/revoke \
-H "Authorization: Bearer <clerk_session_jwt>"POST /api/v1/api-keys/revoke-all
Revoke every active key for the authenticated user (useful if you suspect a leak):
curl -X POST https://api.cleanlist.ai/api/v1/api-keys/revoke-all \
-H "Authorization: Bearer <clerk_session_jwt>"{ "success": true, "message": "3 API key(s) revoked successfully" }Revoked keys can’t be reactivated; create a new one.
Inspecting key usage
In the app, click View requests on a key under Your API keys.
GET /api/v1/api-keys/{key_id}/requests
Pull the most recent requests made with a specific key:
curl "https://api.cleanlist.ai/api/v1/api-keys/550e8400-e29b-41d4-a716-446655440000/requests?limit=100" \
-H "Authorization: Bearer <clerk_session_jwt>"| Query param | Default | Range |
|---|---|---|
limit | 100 | 1 to 500 |
[
{
"id": "log-uuid",
"method": "POST",
"path": "/api/v2/enrichment/bulk",
"status_code": 200,
"duration_ms": 184,
"ip_address": "203.0.113.42",
"user_agent": "python-requests/2.32.0",
"created_at": "2026-04-06T15:42:11Z"
}
]Using a key
Once you have a clapi_ key, every API v2 call uses the same Bearer header:
curl https://api.cleanlist.ai/api/v2/credits/balance \
-H "Authorization: Bearer clapi_your_api_key"With an SDK: you don’t build this header yourself. Pass your key to the client
(Cleanlist(access_token="clapi_…"), or the CLEANLIST_API_KEY env var) and every call is
authenticated. See the SDKs.
Verifying a key
To confirm a key is valid (and inspect its scopes and tier) without spending credits, call GET /whoami:
curl https://api.cleanlist.ai/api/v2/whoami \
-H "Authorization: Bearer clapi_your_api_key"A 401 means the key is missing, malformed, expired or revoked.
Security best practices
- Never commit
clapi_keys to source control. - Store keys in a secret manager (AWS Secrets Manager, Doppler, 1Password, Vault and similar).
- Use a Name and an Expiry to make keys easy to rotate and audit.
- If a key leaks, revoke it at once (Revoke in the app, or
POST /api/v1/api-keys/{key_id}/revoke). - Watch the request count and View requests log for unexpected use.
- Use one key per environment (dev, staging, production), and never share keys across systems.
Common authentication errors
| Status | Code | Cause | Fix |
|---|---|---|---|
401 | AUTHENTICATION_INVALID | Missing, malformed, expired or revoked key (“Invalid or expired API key”, or “Not authenticated” when the header is missing) | Check the header format and the key’s status under Your API keys |
403 | insufficient_scope | Credential lacks the endpoint’s scope | Check /whoami; if a scope you need is missing, contact support@cleanlist.ai |
403 | opt_in_scope_required | The call sends, writes to your CRM or deletes, and the key hasn’t opted into details.required_scope | Tick it on the key under Settings → API keys → Permissions (Opt-in scopes) |
401 | AUTHENTICATION_INVALID | A CluGTM CLI sign-in that was signed out or replaced (“This sign-in was revoked or replaced. Sign in again.”) | Run cleanlist login again |
401 | AUTHENTICATION_INVALID | A CluGTM CLI sign-in that couldn’t be checked just then (“Couldn’t confirm this sign-in right now. Try again shortly.”), with Retry-After: 5 | Retry after 5 seconds |
403 | PERMISSION_DENIED (details.error: feature_gated) | Plan has no API access (key creation or listing) | Move to Pro or Enterprise under Settings → Plans & billing |
401 | AUTHENTICATION_INVALID | An OAuth access token other than the CluGTM CLI’s, or the CLI token on a non-public route (“OAuth access tokens are not accepted here. Use an API key.”) | Use a clapi_ key, or sign in with the CluGTM CLI |
403 | PERMISSION_DENIED | Key used while a trial is active (“Public API access is not included during the trial…”) | Use the app or the MCP during the trial, or start a paid plan |
403 | PERMISSION_DENIED | Key or CLI sign-in used on a plan without API access, for example after a trial ended (“Public API access is not included in the current plan. Choose an eligible plan to use the public API.”) | Move to Pro or Enterprise under Settings → Plans & billing |
429 | RATE_LIMITED | Rate limit hit | Back off using Retry-After; see Errors |
Authentication, plan and rate-limit errors use the generic error shape (code + message); see Errors & Rate Limits.