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.
Choosing a method
Section titled “Choosing a method”| Method | Use it when | Header(s) |
|---|---|---|
API key (qk_) | Server-to-server integrations, scripts, scheduled jobs | Authorization: Bearer qk_... and X-Tenant-ID: <org_id> |
| JWT bearer | A backend acting for a signed-in Querri user | Authorization: Bearer eyJ... |
Embed session (es_) | Calls made for an end user of your embedded Querri | X-Embed-Session: es_... |
| Cookie and CSRF | Code running in a browser that’s already signed in to Querri | access_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).
API keys
Section titled “API keys”Every API-key request needs two headers: the key as a bearer token, and your organization ID.
Required headers
Section titled “Required headers”curl https://app.querri.com/api/v1/projects \ -H "Authorization: Bearer qk_live_abc123..." \ -H "X-Tenant-ID: your_org_id"| Header | Value | Description |
|---|---|---|
Authorization | Bearer qk_... | Your API key |
X-Tenant-ID | Your organization ID | Which 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.
Creating API keys
Section titled “Creating API keys”Organization admins create keys in the Querri web app:
- Go to Settings → API Keys and click Create API Key.
- Choose a key type: Data Access, Full Access, Embed, or Custom to pick scopes yourself.
- Give the key a Key Name, and set Expires In: 30, 60, 90 (the default) or 180 days, or 1 year.
- Optionally change Rate Limit (per min) (default 60) and add an IP Allowlist.
- 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.
Finding your organization ID
Section titled “Finding your organization ID”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.
Scopes
Section titled “Scopes”A key can only call endpoints its scopes allow. Give each integration only the scopes it needs.
| Scope | Access |
|---|---|
data:read | List sources, read data, run SQL, ask questions of a source, read views |
data:write | Create, append to, replace and delete data sources; create and change views |
embed:session:create | Create, refresh, list and revoke embed sessions |
admin:users:read | List and get users |
admin:users:write | Create, update and delete users |
admin:projects:read | List and get projects, steps, step data and chats |
admin:projects:write | Create, update, delete and run projects; send chat messages |
admin:dashboards:read | List and get dashboards, check refresh status |
admin:dashboards:write | Update and refresh dashboards |
admin:policies:read | List and get access policies, resolve access, list columns |
admin:policies:write | Create, update and delete policies; assign users |
admin:sources:read | List connectors and sources |
admin:sources:write | Update and delete sources |
admin:files:read | List and get files |
admin:files:upload | Upload files |
admin:files:delete | Delete files |
admin:permissions:read | List shares |
admin:permissions:write | Share and unshare projects, dashboards, steps and sources |
admin:keys:manage | Create, list, get and revoke API keys |
admin:usage:read | Read usage |
admin:audit:read | Query the audit log |
admin:skills:read | List and get skills |
admin:skills:write | Create, update and delete skills |
admin:library:read | Read the Library |
admin:library:write | Change 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.
API key options
Section titled “API key options”| Option | What it does |
|---|---|
| IP allowlist | Only requests from these IP addresses or CIDR ranges are accepted. Anything else gets 403 with ip_not_allowed. Empty means any IP. |
| Rate limit | Requests per minute for this key. Default 60, from 1 to 10,000. |
| Expiry | Every 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.
Source scope
Section titled “Source scope”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.
Rate limiting
Section titled “Rate limiting”Responses carry rate-limit headers:
X-RateLimit-Limit: 60X-RateLimit-Remaining: 58X-RateLimit-Reset: 1708300800Past the limit, the API returns 429 with the code rate_limit_exceeded and a Retry-After: 60 header. Wait, then retry.
JWT bearer tokens
Section titled “JWT bearer tokens”If your backend has a Querri access token for a signed-in user, forward it to call the API as that user:
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.
Embed sessions
Section titled “Embed sessions”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:
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.
Cookie and CSRF
Section titled “Cookie and CSRF”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.
Error responses
Section titled “Error responses”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.
| HTTP | Code | When |
|---|---|---|
| 400 | missing_tenant_id | An API key was sent without X-Tenant-ID |
| 401 | missing_api_key | No credentials at all |
| 401 | invalid_api_key | The key is wrong, expired or revoked |
| 401 | invalid_jwt | The token is invalid or expired |
| 401 | invalid_embed_session | The embed session is invalid or expired |
| 403 | ip_not_allowed | The request came from an IP outside the key’s allowlist |
| 403 | insufficient_scope | The key lacks the scope the endpoint needs |
| 403 | access_denied | The key’s identity can’t see this item |
| 403 | insufficient_permission | The identity can see the item but needs more access, such as editor or owner |
| 403 | csrf_token_invalid | A cookie-authenticated change without a matching CSRF token |
| 429 | rate_limit_exceeded | Too many requests this minute |
Security best practices
Section titled “Security best practices”- Keep keys on the server. API keys are secrets. For browsers, create an embed session instead.
- Grant the fewest scopes that work.
- Set an IP allowlist when your integration runs from known addresses.
- Rotate keys. They expire anyway; replace them before they do.
- Store keys in environment variables such as
QUERRI_API_KEY, never in source code. - Watch the audit log. Key use appears in Settings → Security → Audit Log, and through
GET /audit/events.