Authentication and keys
3x has four kinds of credentials. Each has a recognizable prefix, so secret scanners (GitHub, GitLab) can find leaked keys, and a checksum, so a mistyped key is rejected before any database lookup.
| Credential | Prefix | Belongs to | Where it is used |
|---|---|---|---|
| Publishable key | pk_3x_ | one project | Browser SDK (data-key, ?k=), POST /v1/e, POST /v1/cb, GET /v1/c/{pk} – public by design |
| Secret key | sk_3x_ | an organization, optionally narrowed to projects | Your server, CI, Claude Code (Authorization: Bearer sk_3x_…) |
| Proxy key | sk_3x_ with scope ingest:proxy | one project | The first-party proxy (x-3x-proxy-key) |
| Dashboard session | x3s_ | a user | The dashboard; scripts that log in as a user |
OAuth access tokens (x3oat_…) exist as well, but are only accepted on /mcp – see
claude.ai connector.
Publishable keys
A publishable key identifies the project of a tracking request. It can only send data, and only from the origins in the project's tracking config (Settings → Tracking → Domains and origins). Requests from any other origin get 403 and are not stored. Because the key is public, every conversion that browser traffic can trigger is guarded (engaged session, once per session and definition, daily cap, optional server confirmation – see Server-side conversions).
# list and create publishable keys (project:admin)
curl -H "Authorization: Bearer $X3_SECRET_KEY" https://api.3x.example/v1/projects/my-site/publishable-keys
curl -X POST -H "Authorization: Bearer $X3_SECRET_KEY" -H "content-type: application/json" \
-d '{"name":"staging"}' https://api.3x.example/v1/projects/my-site/publishable-keysRolling a publishable key (POST …/publishable-keys/{id}/roll) issues a new key and keeps the old
one valid for an overlap period, so cached pages keep tracking while you deploy.
Secret keys
Secret keys belong to an organization and carry scopes. Create them in the dashboard
(Organization → API keys) or with POST /v1/orgs/{org}/keys. The value is shown once; 3x
stores only a SHA-256 hash.
curl -X POST https://api.3x.example/v1/orgs/acme/keys \
-H "Authorization: Bearer $X3_SESSION" -H "content-type: application/json" \
-d '{"name":"reporting job","scopes":["analytics:read"],"projectIds":["9b3c…"],"expiresAt":"2027-01-01T00:00:00Z"}'| Field | Meaning |
|---|---|
scopes | What the key may do – see Scopes and roles. Ask for the least you need. |
projectIds | Optional narrowing to some projects. A narrowed key never holds org:admin. |
expiresAt | Optional expiry. |
note | Free text for your colleagues (where the key is deployed). |
Rules that keep keys from escalating privileges:
- You can only grant scopes you hold yourself; a request for more is refused with **403
insufficient_scope** listing the scopes – nothing is silently dropped. - A key created by a narrowed key is narrowed the same way (or further).
- Keys created through another key or through an OAuth grant are derived: they stop working as soon as their parent key or grant is revoked, and they cannot create keys themselves.
- Creating, rolling and revoking keys from the dashboard needs a recent re-authentication ("sudo", 15 minutes).
Rolling (POST /v1/orgs/{org}/keys/{id}/roll) returns a new value with the same scopes and
keeps the old value valid for expireOldInHours (default 168 = one week, 0 = immediately) – deploy the new value, then let the old one
expire. Keys derived from the rolled key move to the new key (if they fit its scopes and projects), so
a rotation does not break them. Revoking (DELETE /v1/orgs/{org}/keys/{id}) is immediate on the instance that handles
the request; other API instances notice within 10 seconds (credential cache).
Keep secret keys in server-side environment variables (X3_SECRET_KEY), never in client bundles
or NEXT_PUBLIC_* variables, and never in git.
Proxy keys
The first-party proxy forwards the visitor's IP and country to 3x. 3x only trusts
those forwarded values when the request carries a valid proxy key in x-3x-proxy-key – a
secret key with the single scope ingest:proxy, bound to one project. Create it under
Settings → Tracking → Proxy or with POST /v1/projects/{project}/proxy-keys; store it as
X3_PROXY_KEY on the server that runs the proxy.
Dashboard sessions
POST /v1/auth/login with e-mail and password (or a magic link) returns an x3s_… session token.
Its scopes follow the member's role in each organization; personal-data scopes (leads:*)
additionally need the member's PII flag. Sessions expire after inactivity; POST /v1/auth/logout
ends them. Sensitive actions (keys, destinations, deleting projects) need POST /v1/auth/sudo
within the last 15 minutes.
Sending credentials
GET /v1/projects/my-site/analytics/overview?from=2026-10-01&to=2026-10-14 HTTP/1.1
Host: api.3x.example
Authorization: Bearer sk_3x_…- Projects can be addressed by id or slug (
my-site); slugs are unique per organization, andorg-slug/project-slugis always unambiguous. In a URL path, encode the slash:/v1/projects/acme%2Fmy-site/…. A bare slug that exists in several of your organizations answers 404 – use theorg-slug/project-slugform or the id. Credential-less feed URLs (Google Ads CSV) always use the project id. - No access to a project at all → 404 (3x does not reveal that it exists). Access without the
required scope → **403
insufficient_scope** withrequired. - A missing or malformed header → 401 with
WWW-Authenticate: Bearer.
All error shapes: Errors.