Back to app

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-character prefix identifies 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.

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.

  1. Rotate (POST /api-keys/{id}/rotate).
  2. Update the secret in your CI or vault and deploy.
  3. Check that the old key is no longer used: GET /api-keys shows last_used_at per key (updated at most once per minute), and GET /api-logs?key_prefix=<old prefix> lists its latest requests.
  4. Optional: revoke it before the end of the grace period with DELETE /api-keys/{id} (effective immediately, answers 204).

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/v1 response carries an X-Request-Id header. Quote it when you contact support.
  • Each key records last_used_ip, shown in the dashboard and in GET /api-keys.
  • Brute force protection: more than 30 failed authentications (unknown, revoked or expired keys) from one IP address within a minute get 429 until 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.

  1. 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 (format emailAddress) or as the email attribute, and sign the responses.
  2. On the same page, enter the IdP entity id, single sign-on URL, signing certificate and your email domains, then Request SSO setup.
  3. 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.