Query API
The Query API lets you take the logic and structure you define in Qluent and use it outside the product. Ask questions in plain English and receive structured data responses—perfect for building AI experiences.
Getting started
Prerequisites
- A Qluent project with connected data sources
- Project owner permissions (required to manage API keys)
- Your project UUID (found in Project Settings → General)
Creating an API key
- Navigate to Project Settings → API Settings
- Click Create API Key
- Enter a descriptive name (e.g., "Slack Integration", "Query agent")
- Copy the key immediately—it will only be shown once
We store only a one-way hash of your key. If you lose it, you'll need to create a new one.
Project scope
Each API key is scoped to a single project. The project UUID in the URL path must match the project the key was created under — otherwise the request is rejected with 403 PROJECT_MISMATCH. To query a different project, create a new key in that project's API Settings.
Authentication
All requests require your key in the X-API-Key header and the project UUID in the URL path:
curl -X POST "https://api.qluent.io/api/v1/project/your-project-uuid/query/" \
-H "X-API-Key: qk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"question": "What were total sales last month?", "user_email": "analyst@company.com"}'
Response headers
Responses carry headers for debugging and rate limit monitoring:
| Header | When sent | Description |
|---|---|---|
X-Request-ID | Every response | Unique trace ID for the request. Use this to reference specific requests when reporting issues. |
X-RateLimit-Limit | Authenticated responses only | Maximum requests allowed per rate limit window |
X-RateLimit-Remaining | Authenticated responses only | Requests remaining in the current window |
X-RateLimit-Reset | Authenticated responses only | Seconds until the rate limit resets |
Retry-After | 429 responses | Seconds to wait before retrying after a rate-limit rejection |
Rate-limit headers are set after the auth/rate-limit middleware runs, so validation errors (e.g. missing X-API-Key) come back with X-Request-ID only.
Making requests
Endpoint
POST https://api.qluent.io/api/v1/project/{project_uuid}/query/
Request body
| Field | Type | Required | Description |
|---|---|---|---|
question | string | Yes | Natural language question to answer |
user_email | string | Yes | Email of the user making the request (for audit) |
thread_id | string | Thread ID for follow-up questions | |
callback_url | string | HTTPS URL for async webhook delivery |
Example request
curl -X POST "https://api.qluent.io/api/v1/project/your-project-uuid/query/" \
-H "X-API-Key: qk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"question": "What were total sales last month?",
"user_email": "analyst@company.com"
}'
Example response
{
"success": true,
"thread_id": "bef625ba-036f-4b56-a941-57d717e4c06f",
"message_id": "0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b",
"question": "What were total sales last month?",
"sql": "SELECT SUM(amount) FROM sales WHERE date >= '2024-01-01'",
"explanation": "Total sales last month were $125,000 across 847 transactions.",
"data": [{"total_sales": 125000, "transaction_count": 847}],
"columns": ["total_sales", "transaction_count"],
"row_count": 1,
"download_url": "https://api.qluent.io/api/query/results/0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b",
"google_sheets_url": "https://app.qluent.com/project/your-project-uuid/google_sheets/8d2f1e90-4c3b-4a7d-b6e5-9f0a1b2c3d4e?utm_source=api&post_uuid=0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b"
}
Response fields
| Field | Type | Description |
|---|---|---|
data | object[] | Result rows, capped at the first 1,000. Use download_url for the full set when row_count > 1000. |
columns | string[] | Column names. |
row_count | integer | Total rows in the full dataset, not the length of data. |
download_url | string, nullable | CSV download URL. Returned whenever results can be exported; resolved on demand if no stored file exists. |
google_sheets_url | string, nullable | Google Sheets URL for the full dataset. Returned alongside download_url regardless of size. |
The download endpoint (GET https://api.qluent.io/api/query/results/{id} — note: not under /api/v1/) accepts a stored CSV UUID, a message UUID, or a legacy file_download_uuid, so clients can build the URL from a cached message_id. The Google Sheets URL lives on the app domain (https://app.qluent.com/project/{project_uuid}/google_sheets/{export_id}) and carries post_uuid as a query parameter.
Asynchronous queries (webhooks)
If your integration can’t wait for a response, include callback_url in the request. Qluent returns a 202 immediately and posts the result to your webhook.
Example async request
curl -X POST "https://api.qluent.io/api/v1/project/your-project-uuid/query/" \
-H "X-API-Key: qk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"question": "What were total sales last month?",
"user_email": "analyst@company.com",
"callback_url": "https://your-server.com/webhook"
}'
Immediate response (202)
{
"success": true,
"status": "processing",
"request_id": "a3f9c0d2",
"message": "Query is being processed. Results will be sent to your callback URL."
}
Webhook payload
{
"request_id": "a3f9c0d2",
"success": true,
"thread_id": "bef625ba-036f-4b56-a941-57d717e4c06f",
"message_id": "0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b",
"question": "What were total sales last month?",
"explanation": "Total sales last month were $125,000 across 847 transactions.",
"data": [{"total_sales": 125000, "transaction_count": 847}],
"columns": ["total_sales", "transaction_count"],
"row_count": 1,
"download_url": "https://api.qluent.io/api/query/results/0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b",
"google_sheets_url": "https://app.qluent.com/project/your-project-uuid/google_sheets/8d2f1e90-4c3b-4a7d-b6e5-9f0a1b2c3d4e?utm_source=api&post_uuid=0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b"
}
Webhook signature verification
Webhooks include the X-Qluent-Signature header. Verify it using HMAC‑SHA256 with your project’s webhook_secret:
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, webhook_secret: str) -> bool:
expected = hmac.new(webhook_secret.encode(), body, hashlib.sha256).hexdigest()
provided = signature.replace("sha256=", "")
return hmac.compare_digest(expected, provided)
Callback URL requirements:
- Must use HTTPS
- Must be publicly accessible
- Must respond within 30 seconds
Deliveries are attempted once; failures are not retried. Your endpoint must therefore be reachable and respond 2xx on the first attempt.
Follow-up questions
Continue a conversation by including the thread_id from the previous response:
curl -X POST "https://api.qluent.io/api/v1/project/your-project-uuid/query/" \
-H "X-API-Key: qk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"question": "Break that down by region",
"thread_id": "bef625ba-036f-4b56-a941-57d717e4c06f",
"user_email": "analyst@company.com"
}'
Clarification responses
When a question is too vague or ambiguous, the API returns a 422 with error_code: "CLARIFICATION_NEEDED". The response includes a clarification object with structured follow-up questions your application can render to the user:
{
"success": false,
"thread_id": "bef625ba-036f-4b56-a941-57d717e4c06f",
"message_id": "0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b",
"question": "orders",
"clarification": {
"message": "I need some clarification:",
"questions": [
"Are you looking for order counts, order values, or order details?",
"What time period are you interested in?"
]
},
"error_code": "CLARIFICATION_NEEDED",
"error": null
}
| Field | Type | Description |
|---|---|---|
clarification.message | string | A summary of why clarification is needed |
clarification.questions | string[] | Specific follow-up questions the user should answer (always non-empty) |
Use the thread_id from the response to send the user's clarification as a follow-up question.
Unanswerable questions
If the question is well-formed but can't be answered from the available data, the API returns 422 with error_code: "CANNOT_ANSWER". There is no clarification object and no result fields — sending a follow-up won't help; the data isn't there.
{
"success": false,
"thread_id": "bef625ba-036f-4b56-a941-57d717e4c06f",
"message_id": "0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b",
"question": "What's the weather in Paris?",
"sql": null,
"explanation": null,
"data": null,
"columns": null,
"row_count": null,
"clarification": null,
"error_code": "CANNOT_ANSWER",
"error": "This question can't be answered from the connected data sources."
}
Streaming responses (SSE)
For real-time progress updates during query execution, use the streaming endpoint:
POST https://api.qluent.io/api/v1/project/{project_uuid}/query/stream
The request body is the same as the standard endpoint. The response is a Server-Sent Events stream with these event types:
| Event | Description |
|---|---|
status | Processing status updates |
sql | The generated SQL query |
result | Final query result. Same payload as the standard response, including download_url and google_sheets_url. |
clarification | Question requires clarification |
error | Error occurred during processing |
Feedback endpoint
Submit feedback on query responses to help improve result quality.
Endpoint
POST https://api.qluent.io/api/v1/project/{project_uuid}/feedback/
Request body
| Field | Type | Required | Description |
|---|---|---|---|
message_id | string | Yes | The message_id from a query response |
positive | boolean | Yes | true for positive feedback, false for negative |
comment | string | No | Optional feedback text (max 10,000 chars) |
user_email | string | Yes | Email of user submitting feedback (must have project access) |
Example request
curl -X POST "https://api.qluent.io/api/v1/project/your-project-uuid/feedback/" \
-H "X-API-Key: qk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"message_id": "0ee4fa4c-ca84-4ffd-b167-cc8d7a06db4b",
"positive": true,
"comment": "Exactly what I needed",
"user_email": "analyst@company.com"
}'
Example response
{
"success": true,
"feedback_id": 123,
"message": "Feedback submitted successfully"
}
Error codes
| Error Code | HTTP Status | Description |
|---|---|---|
INVALID_REQUEST | 400 | Malformed request body |
INVALID_API_KEY | 401 | API key is invalid or revoked |
| (FastAPI validation error) | 422 | X-API-Key header is missing |
USER_NOT_AUTHORIZED | 403 | User does not have access to this project |
PROJECT_MISMATCH | 403 | API key does not belong to the project UUID in the URL |
MESSAGE_NOT_FOUND | 404 | Invalid message_id or not in project (the message_id must be a UUID returned by a prior query) |
Error handling
| Error Code | HTTP Status | Description |
|---|---|---|
INVALID_REQUEST | 400 | Malformed request body |
INVALID_API_KEY | 401 | API key is invalid or revoked |
| (FastAPI validation error) | 422 | X-API-Key header is missing entirely. Returned as the standard FastAPI validation envelope, not as a flat error_code payload. |
USER_NOT_AUTHORIZED | 403 | User does not have access to this project |
PROJECT_MISMATCH | 403 | API key does not belong to the project UUID in the URL |
PROJECT_NOT_FOUND | 404 | Project not found |
THREAD_NOT_FOUND | 404 | Thread not found or expired |
CLARIFICATION_NEEDED | 422 | Question requires clarification — see Clarification responses |
CANNOT_ANSWER | 422 | Question cannot be answered with available data — see Unanswerable questions |
RATE_LIMIT_EXCEEDED | 429 | Too many requests |
EXECUTION_ERROR | 500 | Query failed during execution. Covers downstream data-source errors, timeouts, and other unexpected failures — the API does not currently differentiate these into separate codes. |
Rate limits
| Limit | Value |
|---|---|
| Requests per minute | 100 per project |
| Concurrent requests | 10 per project |
| Max query execution time | 5 minutes |
When rate limited, implement exponential backoff using the Retry-After response header (seconds to wait).
Security FAQ
How are API keys generated and stored?
Your API key is generated using cryptographically secure randomness. The full key (qk_...) is shown only once at creation—we never store or display it again. We store only a one-way hash, so even if our database were compromised, your actual key cannot be recovered.
How does authentication work?
Every API request requires your key in the X-API-Key header and the target project UUID in the URL path. We hash the incoming key and compare it against stored hashes—your raw key never touches our database. Keys are scoped to a single project: if the project UUID in the URL doesn't match the key's project, the request is rejected with 403 PROJECT_MISMATCH.
How is streaming secured?
The SSE streaming endpoint uses the same API key authentication as the standard query endpoint. All data is transmitted over HTTPS with TLS encryption. Each event in the stream is authenticated as part of the same request that provided the API key.
Can I encrypt API responses?
Yes. You can configure PGP encryption per API key:
- Upload your PGP public key in API Settings
- All responses are encrypted with your public key before transmission
- Only you can decrypt with your private key
Even Qluent cannot read the encrypted payload. Encryption can be enabled, updated, or removed at any time from Project Settings → API Settings.
Who can manage API keys?
Only project owners can create, configure, or revoke API keys. Keys can be instantly revoked if compromised. We track last_used_at so you can audit key activity.
What about rate limiting?
Rate limits are applied per project (not per API key). Authenticated query responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers so you can monitor your usage—see Response headers for details.
Managing keys
- View keys: Project Settings → API Settings
- Revoke keys: Click Revoke next to any key (takes effect immediately)
- Configure encryption: Click Configure to upload your PGP public key
Revoked keys stop working instantly. Applications using the key will receive 401 errors.