Portal API
JWT-authenticated endpoints for the developer portal.
Portal API
The Portal API powers the QANATIX developer dashboard. Unlike the main API (which uses API keys), the Portal API authenticates with JWT tokens issued during portal sign-in.
All endpoints are under /api/v1/portal/.
Authentication
Include your JWT in the Authorization header:
curl https://api.qanatix.com/api/v1/portal/account \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."The JWT is issued when you sign in via the portal. It contains your user ID and tenant ID — no X-Tenant-Id header is needed.
API key management
POST /portal/keys
Create a new API key for your tenant.
curl -X POST https://api.qanatix.com/api/v1/portal/keys \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "production-key",
"scopes": ["search", "upload"]
}'Response (201)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "production-key",
"key": "sk_live_abc123def456...",
"scopes": ["search", "upload"],
"created_at": "2026-03-08T12:00:00Z",
"message": "Store this key securely — it cannot be retrieved again."
}The raw key is returned once. Store it immediately.
GET /portal/keys
List all API keys for your tenant.
curl https://api.qanatix.com/api/v1/portal/keys \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"keys": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "production-key",
"scopes": ["search", "upload"],
"created_at": "2026-03-08T12:00:00Z",
"last_used_at": "2026-03-08T14:30:00Z",
"expires_at": null
}
]
}Key values are never returned in list responses.
DELETE /portal/keys/{key_id}
Revoke an API key. The key stops working immediately.
curl -X DELETE https://api.qanatix.com/api/v1/portal/keys/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (204)
No content. The key is permanently revoked.
POST /portal/keys/{key_id}/rotate
Rotate an API key. Generates a new key value with the same name, scopes, and expiration. The old key is invalidated immediately.
curl -X POST https://api.qanatix.com/api/v1/portal/keys/550e8400-e29b-41d4-a716-446655440000/rotate \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "production-key",
"key": "sk_live_new789ghi012...",
"scopes": ["search", "upload"],
"created_at": "2026-03-08T12:00:00Z",
"message": "Store this key securely — it cannot be retrieved again."
}Account
GET /portal/account
Get account information for the authenticated user.
curl https://api.qanatix.com/api/v1/portal/account \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "developer@example.com",
"tenant_id": "t_abc123",
"display_name": "Jane Developer",
"company": "Acme Corp",
"created_at": "2026-03-01T10:00:00Z"
}PATCH /portal/account
Update your profile information.
curl -X PATCH https://api.qanatix.com/api/v1/portal/account \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"display_name": "Jane D.",
"company": "Acme Industries"
}'Updatable fields
| Field | Type | Description |
|---|---|---|
display_name | string | Your display name |
company | string | Company or organization name |
Response (200)
{
"user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "developer@example.com",
"tenant_id": "t_abc123",
"display_name": "Jane D.",
"company": "Acme Industries",
"created_at": "2026-03-01T10:00:00Z"
}Usage
GET /portal/usage
Get usage and cost summary for your tenant.
curl https://api.qanatix.com/api/v1/portal/usage \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"plan": "pro",
"period": "2026-03",
"records": {
"used": 12500,
"included": 100000,
"overage": 0,
"overage_cost": 0.0
},
"searches": {
"used": 8200,
"included": null,
"overage": 8200,
"overage_cost": 8.20
},
"rate_limit": {
"requests_per_minute": 200,
"search_per_minute": 100,
"max_collections": -1
},
"allows_overage": true,
"estimated_overage_total": 8.20
}| Field | Description |
|---|---|
records.used | Total active records stored (capped on Free/Pro, unlimited on Scale+) |
searches.used | Search queries this period |
searches.overage_cost | Estimated search overage cost (€1.50/1K on Pro) |
estimated_overage_total | Total estimated search overage cost |
allows_overage | Whether the plan allows usage beyond free allowance |
Analytics
GET /portal/analytics
Get provider analytics — how AI agents are querying your public data.
curl https://api.qanatix.com/api/v1/portal/analytics \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"period": "2026-03",
"total_open_searches": 47200,
"collections": [
{
"collection": "manufacturing",
"record_count": 2450,
"public_count": 2100,
"open_searches": 31500,
"last_updated": "2026-03-15T08:30:00Z"
},
{
"collection": "hotels",
"record_count": 1200,
"public_count": 1200,
"open_searches": 15700,
"last_updated": "2026-03-14T22:00:00Z"
}
]
}| Field | Description |
|---|---|
total_open_searches | Total times your public data was queried via QANATIX Open this month |
collections[].open_searches | Per-collection search count (when per-collection tracking is available) |
collections[].public_count | Number of records with visibility=public |
collections[].last_updated | Most recent record update in this collection |
Quickstart
GET /portal/quickstart
Get the quickstart checklist status for your tenant. Each step is marked as completed or pending based on your actual usage.
curl https://api.qanatix.com/api/v1/portal/quickstart \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"steps": [
{
"id": "create_api_key",
"title": "Create an API key",
"completed": true,
"completed_at": "2026-03-08T12:00:00Z"
},
{
"id": "define_schema",
"title": "Define a schema",
"completed": true,
"completed_at": "2026-03-08T12:05:00Z"
},
{
"id": "upload_data",
"title": "Upload sample data",
"completed": false,
"completed_at": null
},
{
"id": "first_search",
"title": "Run your first search",
"completed": false,
"completed_at": null
}
],
"all_completed": false
}Data Discovery
These endpoints power the dashboard's data browser. They mirror the main API endpoints but use JWT auth instead of API keys.
GET /portal/collections
List all collections for your tenant with record counts, indexing stats, and replace status.
curl https://api.qanatix.com/api/v1/portal/collections \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"collections": [
{
"name": "manufacturing",
"record_types": ["product"],
"total_records": 2500,
"indexed": 2500,
"pending": 0,
"processing": 0,
"failed": 0,
"has_pending_replace": false
}
],
"plan": "pro",
"record_limit": 100000,
"total_records": 2500
}GET /portal/records
List records in a collection with pagination.
curl "https://api.qanatix.com/api/v1/portal/records?collection=manufacturing&limit=20&offset=0" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
collection | string | required | Collection to list records from |
record_type | string | — | Filter by record type |
limit | integer | 20 | Max results (1–100) |
offset | integer | 0 | Pagination offset |
Response (200)
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Linear Bearing LM16UU",
"description": "16mm linear motion bearing",
"collection": "manufacturing",
"record_type": "product",
"indexing_status": "indexed",
"collection_data": { "sku": "LM16UU", "price_eur": 12.50 },
"created_at": "2026-03-07T10:00:00Z",
"updated_at": "2026-03-07T10:05:00Z"
}
],
"total": 2500
}POST /portal/search/{collection}
Search within a collection from the dashboard. Same behavior as POST /search/{collection} but uses JWT auth.
curl -X POST https://api.qanatix.com/api/v1/portal/search/manufacturing \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{ "query": "linear bearing", "limit": 10 }'POST /portal/upload/{collection}/{record_type}/upload
Upload a file from the dashboard. Same behavior as POST /upload/{collection}/{record_type}/upload but uses JWT auth.
curl -X POST https://api.qanatix.com/api/v1/portal/upload/manufacturing/product/upload \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-F "file=@catalog.csv"Error responses
Portal API errors follow the same format as the main API:
{
"detail": "Invalid or expired token"
}| Status | Meaning |
|---|---|
401 | Missing or invalid JWT token |
403 | Token valid but insufficient permissions |
404 | Resource not found |
422 | Validation error (invalid request body) |
429 | Rate limit exceeded |