Skip to content

Authentication

The Querri public API at https://app.querri.com/api/v1 accepts four kinds of credentials. Server-to-server integrations should use API keys, which most of this page covers. The other three are at the end.

MethodUse it whenHeader(s)
API key (qk_)Server-to-server integrations, scripts, scheduled jobsAuthorization: Bearer qk_... and X-Tenant-ID: <org_id>
JWT bearerA backend acting for a signed-in Querri userAuthorization: Bearer eyJ...
Embed session (es_)Calls made for an end user of your embedded QuerriX-Embed-Session: es_...
Cookie and CSRFCode running in a browser that’s already signed in to Querriaccess_token cookie, plus X-CSRF-Token on changes

When a request carries more than one, the API checks them in this order: X-Embed-Session, then a Bearer qk_ key, then a JWT (from the header or the cookie).

Every API-key request needs two headers: the key as a bearer token, and your organization ID.

Terminal window
curl https://app.querri.com/api/v1/projects \
-H "Authorization: Bearer qk_live_abc123..." \
-H "X-Tenant-ID: your_org_id"
HeaderValueDescription
AuthorizationBearer qk_...Your API key
X-Tenant-IDYour organization IDWhich organization the key belongs to

Leave out X-Tenant-ID and the API returns 400 with the code missing_tenant_id. Leave out the key and it returns 401 with missing_api_key. A wrong, expired or revoked key returns 401 with invalid_api_key.

Organization admins create keys in the Querri web app:

  1. Go to Settings → API Keys and click Create API Key.
  2. Choose a key type: Data Access, Full Access, Embed, or Custom to pick scopes yourself.
  3. Give the key a Key Name, and set Expires In: 30, 60, 90 (the default) or 180 days, or 1 year.
  4. Optionally change Rate Limit (per min) (default 60) and add an IP Allowlist.
  5. Click Create Key, then copy the secret. It’s shown only once.

If you lose the secret, revoke the key and create a new one.

The key dialog also has a Source Scope field. It doesn’t restrict the key today, so don’t rely on it to limit which sources a key can read. See Source scope.

You can also manage keys through the /keys endpoints, with the admin:keys:manage scope.

Your organization ID is shown as Org ID on Settings → My Account, with a Copy button, and again in the details after you create a key.

A key can only call endpoints its scopes allow. Give each integration only the scopes it needs.

ScopeAccess
data:readList sources, read data, run SQL, ask questions of a source, read views
data:writeCreate, append to, replace and delete data sources; create and change views
embed:session:createCreate, refresh, list and revoke embed sessions
admin:users:readList and get users
admin:users:writeCreate, update and delete users
admin:projects:readList and get projects, steps, step data and chats
admin:projects:writeCreate, update, delete and run projects; send chat messages
admin:dashboards:readList and get dashboards, check refresh status
admin:dashboards:writeUpdate and refresh dashboards
admin:policies:readList and get access policies, resolve access, list columns
admin:policies:writeCreate, update and delete policies; assign users
admin:sources:readList connectors and sources
admin:sources:writeUpdate and delete sources
admin:files:readList and get files
admin:files:uploadUpload files
admin:files:deleteDelete files
admin:permissions:readList shares
admin:permissions:writeShare and unshare projects, dashboards, steps and sources
admin:keys:manageCreate, list, get and revoke API keys
admin:usage:readRead usage
admin:audit:readQuery the audit log
admin:skills:readList and get skills
admin:skills:writeCreate, update and delete skills
admin:library:readRead the Library
admin:library:writeChange the Library
*Every scope

The only wildcard is * on its own. Patterns such as admin:users:* or admin:* aren’t valid scopes, and creating a key with one fails.

A few endpoints accept either of two scopes. Listing and getting sources take data:read or admin:sources:read, and deleting a source takes data:write or admin:sources:write.

The key types in Settings are presets. Data Access is data:read. Embed is embed:session:create and admin:dashboards:read. Full Access grants every scope the dialog lists.

OptionWhat it does
IP allowlistOnly requests from these IP addresses or CIDR ranges are accepted. Anything else gets 403 with ip_not_allowed. Empty means any IP.
Rate limitRequests per minute for this key. Default 60, from 1 to 10,000.
ExpiryEvery key expires. The API accepts 1 to 365 days (default 90).
Bound user (bound_user_id)API only. Reads are row-filtered as this user, and access to individual projects, dashboards and sources follows this user.
Source scope (source_scope)Restricts the key to listed sources, but only when set through the API. See below.

Without a bound user, access to individual items follows the person who created the key. Scopes decide which endpoints a key can call; they don’t grant access to items that person can’t see.

The API also accepts access_policy_ids when you create a key. It’s stored but has no effect. A key’s row filters come from access policies that list the key.

A key is limited to specific sources only when it’s created with POST /keys and a source scope in explicit mode:

{
"name": "Partner feed",
"scopes": ["data:read"],
"source_scope": { "mode": "explicit", "source_ids": ["<source_uuid>"] }
}

Such a key sees only those sources when listing, and gets 403 with source_not_in_scope for any other source. A Source Scope chosen in Settings → API Keys is saved in a different mode, which the API doesn’t enforce, so that key can still read every source its creator can.

Responses carry rate-limit headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1708300800

Past the limit, the API returns 429 with the code rate_limit_exceeded and a Retry-After: 60 header. Wait, then retry.

If your backend has a Querri access token for a signed-in user, forward it to call the API as that user:

Terminal window
curl https://app.querri.com/api/v1/projects \
-H "Authorization: Bearer eyJhbGc..."

The organization comes from the token, so X-Tenant-ID isn’t needed. What the token can call depends on the user’s role, not on key scopes. Admins get every scope. Everyone else gets a fixed set covering projects, dashboards, sources, files, sharing, the Library, data and embed sessions, but not users, keys, policies, skills, usage or audit. Per-item access still applies. Tokens are limited to 600 requests a minute per user.

An invalid or expired token returns 401 with invalid_jwt.

The Python SDK accepts a token too: pass access_token=, set QUERRI_ACCESS_TOKEN, or sign in once with querri auth login.

An embed session token (es_...) comes from POST /embed/sessions. Send it in the X-Embed-Session header to call the API as that session’s user:

Terminal window
curl https://app.querri.com/api/v1/projects \
-H "X-Embed-Session: es_..."

A session gets the same scopes as a non-admin user’s token, minus two: it can’t create embed sessions, and it can’t change dashboards. It’s limited to 600 requests a minute per session. An invalid session returns 401 with invalid_embed_session.

In the Python SDK, client.as_user(session) builds a client that sends this header for you.

Code running in a browser that’s signed in to Querri is authenticated by the access_token cookie. Changes (POST, PUT, PATCH, DELETE) also need an X-CSRF-Token header whose value matches the csrf_token cookie.

const csrfToken = document.cookie
.split("; ")
.find((c) => c.startsWith("csrf_token="))
?.split("=")[1];
fetch("https://app.querri.com/api/v1/projects/<project_id>", {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({ name: "Renamed from the browser" }),
});

A missing or mismatched token returns 403 with csrf_token_invalid. Most integrations shouldn’t use this method. It’s meant for code that already runs inside a Querri browser session.

Errors come back wrapped in detail:

{
"detail": {
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The API key provided is invalid, expired, or revoked.",
"doc_url": "https://docs.querri.com/api/errors#invalid_api_key"
}
}
}

Handle errors by code. Every response also carries an X-Request-Id header to quote to support.

For what each code means and what to do about it, see API errors.

HTTPCodeWhen
400missing_tenant_idAn API key was sent without X-Tenant-ID
401missing_api_keyNo credentials at all
401invalid_api_keyThe key is wrong, expired or revoked
401invalid_jwtThe token is invalid or expired
401invalid_embed_sessionThe embed session is invalid or expired
403ip_not_allowedThe request came from an IP outside the key’s allowlist
403insufficient_scopeThe key lacks the scope the endpoint needs
403access_deniedThe key’s identity can’t see this item
403insufficient_permissionThe identity can see the item but needs more access, such as editor or owner
403csrf_token_invalidA cookie-authenticated change without a matching CSRF token
429rate_limit_exceededToo many requests this minute
  1. Keep keys on the server. API keys are secrets. For browsers, create an embed session instead.
  2. Grant the fewest scopes that work.
  3. Set an IP allowlist when your integration runs from known addresses.
  4. Rotate keys. They expire anyway; replace them before they do.
  5. Store keys in environment variables such as QUERRI_API_KEY, never in source code.
  6. Watch the audit log. Key use appears in Settings → Security → Audit Log, and through GET /audit/events.