Ir al contenido

Data API

Esta página aún no está disponible en español. Se muestra la versión en inglés.

The Data API is how other systems push data into Querri and read it back out. Use it to send rows from another app as they happen, run a nightly sync from a system of record, feed a read-only reporting tool, or build your own pipeline in any language.

This page covers everything a Data API integration needs. There are three ways to call it; pick whichever fits your stack.

PathWhen to use itWhat you get
Python SDKAnything written in PythonTyped sync and async clients, pagination, retries, environment-variable auth
querri CLIScheduled jobs, CI scripts, one-off commands, shell pipelinesOne command per operation, JSON output, browser sign-in or environment-variable auth
Raw HTTPAny other language, or a no-code tool’s webhook stepFull control, no extra dependency

The SDK and CLI ship in the same querri package on PyPI. If you’re in Python, use the SDK; every example below has an SDK form. If you’re not, the curl examples are what to translate.

Terminal window
pip install querri
export QUERRI_API_KEY=qk_your_key_here
export QUERRI_ORG_ID=your_org_id
from querri import Querri
client = Querri() # reads QUERRI_API_KEY and QUERRI_ORG_ID
source = client.sources.create_data_source(
name="Web Leads",
rows=[
{"name": "Alice", "email": "alice@example.com", "score": 85},
{"name": "Bob", "email": "bob@example.com", "score": 92},
],
)
print(source.id, source.row_count)

For async code, use AsyncQuerri and await the calls.

Terminal window
pip install "querri[cli]"
querri auth login # or set QUERRI_API_KEY and QUERRI_ORG_ID
echo '[{"name":"Alice","email":"a@example.com"}]' \
| querri source new --name "Web Leads"
querri source list
querri source data <source_id> --page-size 100
Terminal window
curl -X POST https://app.querri.com/api/v1/sources \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id" \
-H "Content-Type: application/json" \
-d '{
"name": "Web Leads",
"rows": [{"name": "Alice", "email": "alice@example.com", "score": 85}]
}'

All public API endpoints live under:

https://app.querri.com/api/v1

For server-to-server integrations, use an API key and send two headers on every request:

Authorization: Bearer qk_your_secret_here
X-Tenant-ID: your_org_id
Content-Type: application/json # for POST and PUT

The SDK and CLI send both when QUERRI_API_KEY and QUERRI_ORG_ID are set, or when you pass Querri(api_key=..., org_id=...).

The API also accepts a signed-in user’s token, an embed session, and browser cookies. See Authentication for all four, and API Keys for creating keys.

ScopeWhat it allows
data:readList sources, get a schema, read rows, run SQL, ask a question
data:writeCreate, append to, replace and delete sources

A key with only data:read can’t change sources. A key with only data:write can’t read data back, so give it both if your integration checks what it wrote. Listing and getting sources also accept admin:sources:read, and deleting accepts admin:sources:write.

Other parts of the API have their own scopes. See the API Reference.

60 requests a minute per key by default. You can set a key’s limit from 1 to 10,000 when you create it. Past the limit, the API returns 429 with a Retry-After header.

All paths are relative to https://app.querri.com/api/v1.

OperationMethodPathScope
List sourcesGET/sourcesdata:read
Get a source and its schemaGET/sources/{id}data:read
Read rowsGET/sources/{id}/datadata:read
Run SQLPOST/sources/{id}/querydata:read
Ask a questionPOST/sources/{id}/askdata:read
Create a sourcePOST/sourcesdata:write
Append rowsPOST/sources/{id}/rowsdata:write
Replace all rowsPUT/sources/{id}/datadata:write
Delete a sourceDELETE/sources/{id}data:write

Source IDs are UUIDs.

# Python SDK
for s in client.sources.list():
print(s["id"], s["name"], s.get("row_count"))
Terminal window
# CLI
querri source list
querri --json source list # machine-readable
Terminal window
# HTTP
curl "https://app.querri.com/api/v1/sources?limit=100" \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id"

The response is one page of sources. Each has id, name, service, connector_id, columns, row_count, materialized, status, workspace_id, access_controlled and updated_at.

{
"data": [
{
"id": "8f14e45f-ceea-4ba1-9f3c-5d2b8a0c1e77",
"name": "Web Leads",
"service": "api",
"columns": ["name", "email", "score"],
"row_count": 1000,
"materialized": true,
"status": "ready",
"access_controlled": false
}
],
"has_more": false,
"next_cursor": null
}

Pages hold up to 100 sources (limit, default 20). Pass next_cursor back as after for the next page. The SDK’s list() does this for you when you loop over it.

Only a materialized source can be queried. An Excel upload, for example, isn’t materialized until it’s analyzed.

source = client.sources.get(source_id)
Terminal window
querri source describe <source_id>
Terminal window
curl https://app.querri.com/api/v1/sources/{source_id} \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id"

Returns columns, column_types, per-column details such as counts and example values, row_count, materialized, and status, progress and error_message, so a poller can tell a source that’s still processing from one that failed.

page = client.sources.source_data(source_id, page=1, page_size=1000)
print(page.total_rows, len(page.data))
Terminal window
querri source data <source_id> --page 1 --page-size 1000
Terminal window
curl "https://app.querri.com/api/v1/sources/{source_id}/data?page=1&page_size=100" \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id"

Response:

{
"data": [
{"name": "Alice", "email": "alice@example.com", "score": 85},
{"name": "Bob", "email": "bob@example.com", "score": 92}
],
"total_rows": 2,
"page": 1,
"page_size": 100
}

page_size goes up to 10,000.

The source ID goes in the URL path. The body holds the query and paging.

result = client.sources.query(
sql="SELECT name, score FROM data WHERE score > 80",
source_id=source_id,
page=1,
page_size=100,
)
print(result.total_rows, result.data)
Terminal window
querri source query --source-id <source_id> --sql "SELECT name, score FROM data WHERE score > 80"
Terminal window
curl -X POST https://app.querri.com/api/v1/sources/{source_id}/query \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id" \
-H "Content-Type: application/json" \
-d '{
"sql": "SELECT name, score FROM data WHERE score > 80",
"page": 1,
"page_size": 100
}'

Queries run in DuckDB against a view named data. Only SELECT statements are allowed, up to 10,000 characters. Row-level security applies (see Who can read what).

The response adds ordering to data, total_rows, page and page_size. Each page reruns the query, so rows are sorted by every column you select to keep page boundaries stable.

Querri writes a SELECT for your question, checks it, and runs it the same way as a SQL query.

answer = client.sources.ask(source_id, question="Which five leads have the highest score?")
print(answer["generated_sql"])
print(answer["data"])
Terminal window
querri source ask <source_id> "Which five leads have the highest score?"
Terminal window
curl -X POST https://app.querri.com/api/v1/sources/{source_id}/ask \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id" \
-H "Content-Type: application/json" \
-d '{"question": "Which five leads have the highest score?"}'

The body takes question (up to 2,000 characters), plus optional page and page_size. The response has the rows, generated_sql and your question. If the generated SQL is rejected, the error is invalid_generated_sql and includes the SQL.

The SDK has two create methods on client.sources. Use create_data_source for rows of JSON. The API has no endpoint for connector-backed sources, so create isn’t useful here.

source = client.sources.create_data_source(
name="Web Leads",
rows=[
{"name": "Alice", "email": "alice@example.com", "score": 85},
{"name": "Bob", "email": "bob@example.com", "score": 92},
],
)
# source.id, source.name, source.columns, source.row_count, source.updated_at
Terminal window
echo '[{"name":"Alice","email":"a@example.com","score":85}]' \
| querri source new --name "Web Leads"
Terminal window
curl -X POST https://app.querri.com/api/v1/sources \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id" \
-H "Content-Type: application/json" \
-d '{
"name": "Web Leads",
"rows": [
{"name": "Alice", "email": "alice@example.com", "score": 85},
{"name": "Bob", "email": "bob@example.com", "score": 92}
]
}'

Response (201 Created):

{
"id": "8f14e45f-ceea-4ba1-9f3c-5d2b8a0c1e77",
"name": "Web Leads",
"columns": ["name", "email", "score"],
"row_count": 2,
"updated_at": "2026-03-08T15:30:00.000000"
}

Querri works out each column’s type from the values you send. The new source belongs to the key’s bound user, or to the person who created the key, and lands in that person’s private workspace.

Add rows to an existing source. Columns are matched by name: new columns are added, and missing ones are filled with nulls. Use this when rows arrive over time.

result = client.sources.append_rows(
source_id,
rows=[
{"name": "Charlie", "email": "charlie@example.com", "score": 78},
{"name": "Diana", "email": "diana@example.com", "score": 95},
],
)
Terminal window
curl -X POST https://app.querri.com/api/v1/sources/{source_id}/rows \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{"name": "Charlie", "email": "charlie@example.com", "score": 78}
]
}'

The source must already have data. If it doesn’t, the API returns no_data; use replace or create instead.

Swap a source’s contents for a new dataset. Use this for full nightly syncs.

result = client.sources.replace_data(source_id, rows=fresh_export)
Terminal window
curl -X PUT https://app.querri.com/api/v1/sources/{source_id}/data \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{"name": "Eve", "email": "eve@example.com", "score": 100}
]
}'
client.sources.delete(source_id)
Terminal window
querri source delete <source_id>
Terminal window
curl -X DELETE https://app.querri.com/api/v1/sources/{source_id} \
-H "Authorization: Bearer qk_your_key_here" \
-H "X-Tenant-ID: your_org_id"

The response is:

{ "id": "8f14e45f-ceea-4ba1-9f3c-5d2b8a0c1e77", "deleted": true }

A source created through the API or from an upload is removed along with its data. A source that came from a connector is marked deleted and its data files are removed, so the connector still knows what it synced before.

LimitValue
Rows per write request100,000
Request body50 MB
Rows per page (reads)10,000
SQL length10,000 characters
Question length2,000 characters
Rate limit60 a minute per key by default (up to 10,000)

Two checks apply to every request.

Item access. The key acts as its bound user, or, without one, as the person who created it. That person must be able to see the source. Reading needs view access, appending and replacing need edit access, and deleting needs owner access. Otherwise the API returns 403 with access_denied or insufficient_permission.

Row-level security applies to reads:

  • A key with a bound user is filtered by that user’s access policies.
  • A key without one is filtered by access policies that list the key itself.
  • With no applicable policy, the key reads every row, except in sources marked access-controlled (access_controlled: true), which return no rows until a policy matches.

The access_policy_ids field on a key has no effect, so don’t use it to limit rows.

Writes don’t apply row filters. The key needs edit or owner access to the source, as above.

A key can be limited to a list of sources, but only when it’s created through POST /keys with a source scope in explicit mode:

{ "mode": "explicit", "source_ids": ["8f14e45f-ceea-4ba1-9f3c-5d2b8a0c1e77"] }

That key lists only those sources and gets 403 with source_not_in_scope for any other. New sources it creates aren’t added to its list, so integrations that create sources need a key without a source scope.

A Source Scope chosen in Settings → API Keys doesn’t restrict the key. It can still read every source its creator can.

An admin creates keys at Settings → API Keys (/settings/api) with Create API Key. Choose Custom to pick data:read and data:write.

IntegrationNameScopesNotes
Create and append from another appCRM Syncdata:read, data:writeRead lets it check what it wrote
Append onlyLead Capturedata:writeFine if it never reads back
Nightly batch syncNightly Sync Botdata:read, data:writeRead to check, write to replace
Reporting toolReporting Read-Onlydata:readTo limit sources, create the key with POST /keys and an explicit source scope
  • Bound user: the identity reads are filtered as. Set it with bound_user_id through the API.
  • Source scope: enforced only when set through POST /keys in explicit mode.
  • Expiry: 90 days by default, a year at most. Replace keys before they expire.
  • Rate limit: 60 a minute by default; raise it for busy integrations.
  1. Use the fewest scopes that work. An integration that only sends rows needs data:write, not data:read.
  2. One key per integration. Don’t reuse a sync key in a reporting tool.
  3. Add an IP allowlist for server-to-server keys when the addresses are stable.
  4. Store the secret in a vault. The qk_ secret is shown once, when the key is created.

The full lifecycle: create, append, read, replace, delete.

from querri import Querri
client = Querri() # QUERRI_API_KEY and QUERRI_ORG_ID from the environment
# 1. Create
source = client.sources.create_data_source(
name="CRM Contacts",
rows=[{"name": "Alice", "email": "alice@corp.com", "deal_stage": "qualified"}],
)
# 2. Append
client.sources.append_rows(source.id, rows=[
{"name": "Bob", "email": "bob@corp.com", "deal_stage": "proposal"},
{"name": "Carol", "email": "carol@corp.com", "deal_stage": "closed_won"},
])
# 3. Read back
page = client.sources.source_data(source.id, page=1, page_size=100)
print(f"{page.total_rows} contacts loaded")
# 4. Nightly: replace with the full export
client.sources.replace_data(source.id, rows=fresh_crm_export)
# 5. Remove it when you're done
client.sources.delete(source.id)

For asyncio backends, use AsyncQuerri and await each call.

The CLI is the shortest path for scheduled jobs and operator scripts:

Terminal window
# 1. Authenticate once (browser sign-in, saved to ~/.querri/tokens.json)
querri auth login
# Or, in CI, set environment variables
export QUERRI_API_KEY=qk_...
export QUERRI_ORG_ID=your_org_id
# 2. Load a JSON export as a new source
cat fresh_export.json \
| querri source new --name "Nightly CRM Snapshot $(date +%F)"
# 3. Or query an existing source
querri --json source query --source-id <source_id> \
--sql "SELECT region, COUNT(*) FROM data GROUP BY region"

The CLI’s source commands are list, get, describe, data, query, ask, new, update, delete, sync and connectors. For appending or replacing rows, use the SDK or HTTP.

Webhook from another app: append on each new record

Section titled “Webhook from another app: append on each new record”

A common pattern: when a record appears in another app, append it to a Querri source. Any tool that can send an HTTP request works.

Once: create the source with the SDK or curl, and save the returned id.

For each record, send:

  • Method: POST
  • URL: https://app.querri.com/api/v1/sources/{source_id}/rows
  • Headers:
    Authorization: Bearer qk_your_key_here
    X-Tenant-ID: your_org_id
    Content-Type: application/json
  • Body:
    {
    "rows": [
    {
    "name": "{{name}}",
    "email": "{{email}}",
    "company": "{{company}}",
    "created_at": "{{created_date}}"
    }
    ]
    }

Test with one record before you turn it on.

const BASE = "https://app.querri.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.QUERRI_API_KEY}`,
"X-Tenant-ID": process.env.QUERRI_ORG_ID,
"Content-Type": "application/json",
};
const create = await fetch(`${BASE}/sources`, {
method: "POST",
headers,
body: JSON.stringify({
name: "CRM Contacts",
rows: [{ name: "Alice", email: "alice@corp.com" }],
}),
});
const { id } = await create.json();
await fetch(`${BASE}/sources/${id}/rows`, {
method: "POST",
headers,
body: JSON.stringify({
rows: [{ name: "Bob", email: "bob@corp.com" }],
}),
});
const read = await fetch(`${BASE}/sources/${id}/data?page=1&page_size=100`, { headers });
const { data, total_rows } = await read.json();

The same requests work from Go, Ruby, Java or anything else with an HTTP client.

Create a key with only data:read, then walk every source and page through its rows:

from querri import Querri
client = Querri() # a read-only key
for s in client.sources.list():
if not s.get("materialized"):
continue
rows = []
page_number = 1
while True:
page = client.sources.source_data(s["id"], page=page_number, page_size=1000)
rows.extend(page.data)
if not page.data or len(rows) >= (page.total_rows or 0):
break
page_number += 1
print(f"{s['name']}: loaded {len(rows)} rows")

Errors come back as {"detail": {"error": {"type", "code", "message"}}}.

CodeHTTPWhen it happens
source_not_found404No source with that ID
no_data400 or 404The source has no data (append needs existing data)
access_denied403The key’s identity can’t see the source
insufficient_permission403The identity can see the source but needs edit or owner access
source_not_in_scope403The key’s explicit source scope doesn’t include this source
insufficient_scope403The key is missing the scope the endpoint needs
too_many_rows400More than 100,000 rows in one request
empty_data400The rows have no columns
payload_too_large413The body is over 50 MB
invalid_sql400The SQL isn’t a single allowed SELECT
query_failed400The SQL was allowed but failed to run
invalid_generated_sql400For a question, the SQL Querri wrote was rejected
llm_error500For a question, Querri couldn’t write the SQL

The Python SDK raises these as typed exceptions: NotFoundError, PermissionError, ValidationError, RateLimitError and so on. See Error handling.

  • Python SDK: pip install querri, with sync and async clients.
  • Querri CLI: pip install "querri[cli]", then querri auth login.
  • Authentication: API keys, tokens, embed sessions and cookies.
  • API Keys: creating and revoking keys.
  • API Reference: every public endpoint, including projects, dashboards, files and policies.