Authentication and security
API keys
Every API request is authenticated with an organization API key, sent as a bearer token:
curl -H "Authorization: Bearer cqa_1a2b3c4d_Zr0..." https://app.callqa.dev/api/v1/me
- Format:
cqa_<prefix>_<secret>. The 8-characterprefixidentifies the key in lists and logs (cqa_1a2b3c4d_...). The secret is 43 random URL-safe characters. - A key belongs to one organization, not to a person. It keeps working when the admin who created it leaves (except for invitations, see below).
- The full key is shown once, at creation. CallQA stores only a keyed hash (HMAC-SHA256 with a server-side pepper): nobody, including CallQA staff, can read it back. Lost key: create a new one and revoke the old one.
- Keys can expire (
expires_in_days, 1 to 3650). No expiry by default.
Creating a key
In the dashboard, an admin opens Developer > API keys and clicks + API key. Or, with a
key that has the admin scope:
curl -sS -X POST https://app.callqa.dev/api/v1/api-keys \
-H "Authorization: Bearer $CALLQA_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "GitHub Actions", "scopes": ["read", "run"], "expires_in_days": 180}'
{
"id": 12, "name": "GitHub Actions", "prefix": "9f8e7d6c", "scopes": ["read", "run"],
"created_at": "2026-09-26T10:00:00Z", "expires_at": "2027-03-25T10:00:00Z",
"last_used_at": null, "last_used_ip": null, "revoked_at": null, "allowed_cidrs": [],
"rotated_from_id": null,
"key": "cqa_9f8e7d6c_..."
}
Default scopes when you omit scopes: read and run.
Scopes
A key can only do what its scopes allow. admin implies all the others.
| Scope | Allows |
|---|---|
read |
Read scenarios, runs, calls, recordings, traces, tags, versions, environments, usage, members; export replay bundles; read the API journal, webhook endpoints and their delivery logs |
write |
Create and edit scenarios, variants, numbers, tags, versions, environments; archive scenarios; sign a number's attestation |
run |
Trigger runs, cancel runs, replay calls; verify numbers (codes and verification calls) |
admin |
Billing, wallet and overage settings, members and invitations, API keys, webhook endpoints, alert channels, audit log, data exports, hard deletes (scenarios, numbers, versions, environments, runs and calls) |
A request without the needed scope gets 403:
{ "detail": "This API key lacks the 'run' scope (run.trigger).", "code": "missing_scope" }
The scope of every endpoint is listed in the API reference.
Recommended scopes
| Use | Scopes |
|---|---|
| CI/CD pipeline that runs tests | read, run |
| Script that syncs scenarios from a repository | read, write |
| Dashboard or reporting integration | read |
| Infrastructure as code (webhooks, keys) | admin, kept in a vault |
Roles
People in the dashboard have a role. API keys act at the organization level and are limited only by their scopes; only admins can create keys.
| Action | admin | member | guest |
|---|---|---|---|
| View reports, runs, calls, scenarios | yes | yes | yes |
| Create and edit scenarios, numbers, tags, versions, environments | yes | yes | no |
| Trigger runs and replays, export bundles | yes | yes | no |
| Archive a scenario | yes | yes | no |
| Delete scenarios, numbers, variants, tags, versions, environments, runs and calls | yes | no | no |
| Verify a number, sign its attestation | yes | yes | no |
| Billing, wallet, members, API keys, webhooks, alerts, audit log | yes | no | no |
| View the API journal, webhook endpoints and their deliveries | yes | yes | no |
An organization can have several admins.
Rotating a key
Rotate a key in place: CallQA issues a replacement with the same name, scopes, allowed networks and expiry, and the old key keeps working for a grace period so you can deploy the new value without downtime.
curl -sS -X POST https://app.callqa.dev/api/v1/api-keys/12/rotate \
-H "Authorization: Bearer $CALLQA_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"grace_period_hours": 24}'
The answer is 201 with the new key (in key, shown once) and rotated_from_id. The old key
now has an expires_at at most 24 hours away (grace_period_hours: 0 to 168, default 24;
0 revokes it at once). In the dashboard: Developer > API keys > Edit > Rotate key.
- Rotate (
POST /api-keys/{id}/rotate). - Update the secret in your CI or vault and deploy.
- Check that the old key is no longer used:
GET /api-keysshowslast_used_atper key (updated at most once per minute), andGET /api-logs?key_prefix=<old prefix>lists its latest requests. - Optional: revoke it before the end of the grace period with
DELETE /api-keys/{id}(effective immediately, answers204).
A revoked or expired key gets 401. Revoking is idempotent. Scopes never change on an
existing key: to change them, create a new key. The name and the allowed networks can change:
PATCH /api-keys/{id}.
Restricting a key to IP addresses
A key can be limited to a list of networks (IPv4 or IPv6, CIDR notation or single addresses,
at most 20). A request from anywhere else gets 403 with code: ip_not_allowed, even with a
valid key. Use it for keys used from fixed egress addresses: self-hosted CI runners, a NAT
gateway, an office.
curl -sS -X PATCH https://app.callqa.dev/api/v1/api-keys/12 \
-H "Authorization: Bearer $CALLQA_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"allowed_cidrs": ["203.0.113.0/24", "2001:db8::/32"]}'
Send "allowed_cidrs": [] to remove the restriction. Set it at creation with the same field
on POST /api-keys, or in the dashboard (Developer > API keys > Edit). Cloud CI runners (GitHub
hosted, GitLab SaaS) change addresses all the time: do not restrict their keys, or run your
own runners. The address checked is the one that reaches our load balancer; an
X-Forwarded-For header you send is ignored.
IP addresses and the API journal
- Every authenticated request is recorded in the API journal with its method, path, status,
duration, key prefix, client IP, user agent and request id. Bodies and headers are never
stored. Read it with
GET /api-logs(filters:since,status_code,key_prefix) or on the Developer > API journal page. - Every
/api/v1response carries anX-Request-Idheader. Quote it when you contact support. - Each key records
last_used_ip, shown in the dashboard and inGET /api-keys. - Brute force protection: more than 30 failed authentications (unknown, revoked or expired
keys) from one IP address within a minute get
429until the end of the minute. Valid keys from the same address are not blocked: they only count against their own rate limit.
Audit log
Admins can read who did what in the organization: key creation, rotation and revocation,
webhooks, alert channels, members, billing settings, number verification and deletions. On the
Organization > Audit log page, or with an admin key:
curl -sS -H "Authorization: Bearer $CALLQA_ADMIN_KEY" \
"https://app.callqa.dev/api/v1/audit-log?action=apikey.&since=2026-09-01T00:00:00Z"
action is an exact action (webhook.create) or a prefix ending with a dot (apikey.).
Other filters: actor_api_key_prefix, since, until, limit, offset. Entries never
contain secrets.
Organization isolation
A key only sees its own organization. Objects of other organizations answer 404, exactly
like objects that do not exist. A replay bundle can only call a number registered in your
organization. If your organization is suspended, its keys get 401.
Invitations sent with a key
POST /invitations sends the invitation on behalf of the admin who created the key. If that
person no longer exists, the request fails with 400: create a new key.
Webhook security
Outgoing webhooks are signed with HMAC-SHA256 and a per-endpoint secret. Webhook URLs must use HTTPS and resolve to public IP addresses. See Webhooks.
Single sign-on (SAML)
Scale plan. Your team signs in to the dashboard with your identity provider (Okta, Microsoft Entra ID, Google Workspace, OneLogin, any SAML 2.0 IdP). API keys are not affected.
- An admin opens Organization > Single sign-on (SSO). It shows the values to give your IdP:
entity id (audience) and assertion consumer service URL,
https://app.callqa.dev/accounts/saml/<your org slug>/acs/. Send the email address as the NameID (formatemailAddress) or as theemailattribute, and sign the responses. - On the same page, enter the IdP entity id, single sign-on URL, signing certificate and your email domains, then Request SSO setup.
- CallQA checks that your organization owns the domains and activates the connection, usually within one business day.
Then people sign in at https://app.callqa.dev/sso/ with their work email. Only addresses of
your domains are accepted. Someone signing in for the first time becomes a member with the role
you chose (member by default); an existing account with the same email is connected. When no
seat is left in your plan (pending invitations count), that first sign-in is refused with a
page saying so: an admin frees a seat in Organization > Members or upgrades the plan, then
the person signs in again. Existing members always sign in. If the plan no longer includes
SSO, sign-ins through the IdP are refused and passwords keep working.
Good practices
- One key per system (one for GitHub Actions, one for the reporting job): you can revoke one without breaking the others, and the journal tells you who did what.
- Give the smallest set of scopes. A CI key never needs
admin. - Store keys in your CI secret store (GitHub secrets, GitLab masked variables, Jenkins credentials), never in the repository or in scenario files.
- Set an expiry on keys given to contractors or temporary jobs.
- Never send a key to a browser. Call the API from a backend.
- Review the API journal after an incident and revoke unknown keys.