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 Supabase JWT tokens issued during portal sign-in.
All endpoints are under /api/v1/portal/.
Authentication
Include your Supabase 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", "ingest"]
}'Response (201)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "production-key",
"key": "sk_live_abc123def456...",
"scopes": ["search", "ingest"],
"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", "ingest"],
"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", "ingest"],
"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",
"entities": {
"used": 12500,
"included": 100000,
"overage": 0,
"overage_cost": 0.0
},
"searches": {
"used": 8200,
"included": null,
"overage": 8200,
"overage_cost": 8.20
},
"ingestions": {
"used": 1500,
"included": null,
"overage": 1500,
"overage_cost": 12.00
},
"rate_limit": {
"requests_per_minute": 200,
"search_per_minute": 100,
"max_verticals": -1
},
"allows_overage": true,
"estimated_overage_total": 20.20
}| Field | Description |
|---|---|
entities.used | Total entities 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) |
ingestions.overage_cost | Estimated ingestion overage cost ($6/1K on Pro) |
estimated_overage_total | Sum of search + ingestion costs |
allows_overage | Whether the plan allows usage beyond free allowance |
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": "ingest_data",
"title": "Ingest sample data",
"completed": false,
"completed_at": null
},
{
"id": "first_search",
"title": "Run your first search",
"completed": false,
"completed_at": null
}
],
"all_completed": false
}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 |