Docs

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.

CredentialPrefixBelongs toWhere it is used
Publishable keypk_3x_one projectBrowser SDK (data-key, ?k=), POST /v1/e, POST /v1/cb, GET /v1/c/{pk} – public by design
Secret keysk_3x_an organization, optionally narrowed to projectsYour server, CI, Claude Code (Authorization: Bearer sk_3x_…)
Proxy keysk_3x_ with scope ingest:proxyone projectThe first-party proxy (x-3x-proxy-key)
Dashboard sessionx3s_a userThe 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-keys

Rolling 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"}'
FieldMeaning
scopesWhat the key may do – see Scopes and roles. Ask for the least you need.
projectIdsOptional narrowing to some projects. A narrowed key never holds org:admin.
expiresAtOptional expiry.
noteFree 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, and org-slug/project-slug is 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 the org-slug/project-slug form 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** with required.
  • A missing or malformed header → 401 with WWW-Authenticate: Bearer.

All error shapes: Errors.