API Reference
All API endpoints use JSON format. Authentication uses Bearer aqn_ API key authentication. See Authentication for details.
Base URL: https://your-instance/api/
Authentication
Include your API key in all requests:
Authorization: Bearer aqn_your_api_key
API keys are created and managed from the API Keys page (/keys/). Full-access keys see all collections; scoped keys see only granted collections.
OpenAPI Schema
The full OpenAPI schema is available at /api/v1/schema/ in all environments (including production). Use it to integrate with any OpenAPI-compatible tool:
- SDK generation (openapi-generator, kiota, etc.)
- ChatGPT Custom GPT Actions
- Postman / Insomnia import
- Any OpenAPI-compatible tool or workflow
GET /api/v1/schema/
Returns a valid OpenAPI 3.0 document as JSON. No authentication required. Swagger UI (/api/v1/docs/) and ReDoc (/api/v1/redoc/) are available in debug/development environments only.
Collections
List Collections
GET /api/v1/collections/
Returns collections visible to the authenticated user (owned + subscribed public).
Response: 200 OK
[
{
"uuid": "a1b2c3...",
"name": "Project Docs",
"slug": "project-docs",
"description": "Technical documentation",
"document_count": 15,
"total_chunks": 342,
"is_public": false,
"is_subscribed": false,
"created_at": "2026-03-01T10:00:00Z"
}
]
Create Collection
POST /api/v1/collections/
Create a new collection owned by the authenticated user.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Collection name (must be non-empty) |
description |
string | No | Optional description |
Response: 201 Created — Full collection object
{
"uuid": "a1b2c3...",
"name": "Project Docs",
"slug": "project-docs",
"description": "Technical documentation",
"document_count": 0,
"total_chunks": 0,
"is_public": false,
"is_subscribed": false,
"created_at": "2026-03-23T10:00:00Z"
}
Errors:
400— Name is missing or empty
Example:
curl -X POST https://your-instance/api/v1/collections/ \
-H "Authorization: Bearer aqn_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Project Docs", "description": "Technical documentation"}'
Get Collection
GET /api/v1/collections/{uuid}/
Response: 200 OK — Collection object
Update Collection
PATCH /api/v1/collections/{uuid}/
Update a collection's name and/or description. Owner only.
Request Body: Any subset of the following fields:
| Field | Type | Description |
|---|---|---|
name |
string | New collection name |
description |
string | New description |
Response: 200 OK — Updated collection object
Errors:
400— Validation error (e.g. empty name)403— Not the collection owner404— Collection not found
Example:
curl -X PATCH https://your-instance/api/v1/collections/{uuid}/ \
-H "Authorization: Bearer aqn_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Renamed Docs"}'
Delete Collection
DELETE /api/v1/collections/{uuid}/
Soft-delete a collection and all its documents. Owner only. Search index entries are removed immediately; blobs are retained for a 7-day grace period before permanent deletion.
Response: 204 No Content
Errors:
403— Not the collection owner404— Collection not found
Example:
curl -X DELETE https://your-instance/api/v1/collections/{uuid}/ \
-H "Authorization: Bearer aqn_your_api_key" \
-w "\nHTTP %{http_code}\n"
Documents
List Documents
GET /api/v1/documents/
Returns documents across all visible collections.
Query Parameters:
| Param | Type | Description |
|---|---|---|
tag |
string | Filter by tag (case-insensitive contains) |
collection |
string | Filter by collection slug |
Response: 200 OK
[
{
"uuid": "d4e5f6...",
"title": "Annual Report 2025",
"source_type": "pdf",
"status": "indexed",
"tags": ["finance", "annual"],
"chunks_count": 24,
"original_filename": "report-2025.pdf",
"file_size": 102400,
"collection_name": "Project Docs",
"collection_slug": "project-docs",
"created_at": "2026-03-01T10:00:00Z"
}
]
Upload Documents
POST /api/v1/collections/{uuid}/documents/
Upload one or more documents to a collection.
Auth: Requires write permission — full-access keys or scoped keys with a write grant on the collection.
Request: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | One of file/files | Single file upload |
files |
file[] | One of file/files | Batch upload (multiple files) |
title |
string | No | Document title (defaults to filename) |
tags |
string | No | Comma-separated list of tags |
source_type |
string | No | File type override (auto-detected from extension if omitted) |
Single file response: 201 Created
{
"uuid": "d4e5f6...",
"title": "Annual Report 2025",
"source_type": "pdf",
"status": "pending",
"original_filename": "report-2025.pdf",
"file_size": 102400,
"tags": ["finance", "annual"],
"collection_name": "Project Docs",
"created_at": "2026-03-22T10:00:00Z"
}
Batch upload response: 201 Created
{
"uploaded": [
{
"uuid": "d4e5f6...",
"title": "Annual Report 2025",
"source_type": "pdf",
"status": "pending",
"original_filename": "report-2025.pdf",
"file_size": 102400,
"tags": [],
"collection_name": "Project Docs",
"created_at": "2026-03-22T10:00:00Z"
}
],
"skipped": [
{
"filename": "image.png",
"reason": "Unsupported file type."
}
],
"total_uploaded": 1,
"total_skipped": 1
}
Errors:
400— No files provided, unsupported file type, or file too large403— Write access required404— Collection not found
Example — single file:
curl -X POST https://your-instance/api/v1/collections/{uuid}/documents/ \
-H "Authorization: Bearer aqn_your_api_key" \
-F "file=@report-2025.pdf" \
-F "title=Annual Report 2025" \
-F "tags=finance,annual"
Example — batch upload:
curl -X POST https://your-instance/api/v1/collections/{uuid}/documents/ \
-H "Authorization: Bearer aqn_your_api_key" \
-F "files=@report-2025.pdf" \
-F "files=@summary.docx" \
-F "tags=finance"
Replace Document
POST /api/v1/documents/{uuid}/replace/
Replace a document's file and re-process it. The document UUID is preserved; old search entries are removed and the new file is processed from scratch.
Auth: Requires write permission — full-access keys or scoped keys with a write grant on the collection.
Request: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | Replacement file |
title |
string | No | Override document title (keeps existing title if omitted) |
tags |
string | No | Comma-separated list of tags (replaces existing tags if provided) |
source_type |
string | No | File type override (auto-detected from extension if omitted) |
Response: 200 OK — Document object with status: "pending"
{
"uuid": "d4e5f6...",
"title": "Annual Report 2026",
"source_type": "pdf",
"status": "pending",
"original_filename": "report-2026.pdf",
"file_size": 110592,
"tags": ["finance", "annual"],
"collection_name": "Project Docs",
"created_at": "2026-03-01T10:00:00Z"
}
Errors:
400— No file provided or unsupported file type403— Write access required404— Document not found
Example:
curl -X POST https://your-instance/api/v1/documents/{uuid}/replace/ \
-H "Authorization: Bearer aqn_your_api_key" \
-F "file=@report-2026.pdf" \
-F "title=Annual Report 2026"
Download Document
GET /api/v1/documents/{uuid}/download/
Download the original uploaded file. Returns a 302 redirect to a time-limited signed URL (valid for 1 hour).
Auth: Any valid API key or session auth that can see the document — owned collections or subscribed public collections.
Response: 302 Found — Redirect to Azure Blob Storage SAS URL
Errors:
404— Document not found or no file available (text-only or URL documents have no stored file)
Example:
curl -L -H "Authorization: Bearer aqn_..." \
https://aqoon.ai/api/v1/documents/{uuid}/download/ \
-o file.pdf
Use the -L flag to follow the redirect automatically. The signed URL expires after 1 hour.
Get Document Detail
GET /api/v1/documents/{uuid}/
Response: 200 OK — Includes content_text, error_message, and chunks array.
{
"uuid": "d4e5f6...",
"title": "Annual Report 2025",
"source_type": "pdf",
"status": "indexed",
"tags": ["finance", "annual"],
"chunks_count": 24,
"original_filename": "report-2025.pdf",
"file_size": 102400,
"content_text": "Full extracted text...",
"error_message": "",
"collection_name": "Project Docs",
"collection_slug": "project-docs",
"created_at": "2026-03-01T10:00:00Z",
"chunks": [
{
"id": "uuid",
"content": "Chunk text content...",
"chunk_index": 0,
"embedding_model": "text-embedding-3-small"
}
]
}
Search
Hybrid Search
POST /api/v1/search/
Request Body:
{
"query": "payment terms",
"collection": "project-docs",
"limit": 5
}
| Field | Type | Required | Default |
|---|---|---|---|
query |
string | Yes | — |
collection |
slug | No | All visible collections |
limit |
int | No | 5 (max: 50) |
Response: 200 OK
{
"query": "payment terms",
"count": 3,
"results": [
{
"content": "Matching chunk text...",
"title": "Contract Document",
"document_id": "uuid",
"collection_name": "Legal",
"tags": ["contract"],
"score": 0.89
}
]
}
Collection Subscriptions
Subscribe to a Public Collection
POST /api/v1/collections/{slug}/subscribe/
Response: 200 OK
{
"status": "subscribed",
"collection": "project-docs"
}
Errors:
404— Collection not found or not public
Unsubscribe from a Collection
DELETE /api/v1/collections/{slug}/subscribe/
Response: 200 OK
{
"status": "unsubscribed",
"collection": "project-docs"
}
Errors:
404— Subscription not found
API Key Management (KaaS)
KaaS API keys allow external developers to search your collections. Managed via the API Keys page in the web UI or via the REST API.
List / Create API Keys
GET /api/v1/keys/
POST /api/v1/keys/
Create Request Body:
{
"name": "Acme Corp Key"
}
Create Response: 201 Created
{
"uuid": "key-uuid",
"name": "Acme Corp Key",
"prefix": "aqn_abc123...",
"key": "aqn_full_key_here",
"is_active": true,
"created_at": "2026-03-01T10:00:00Z"
}
The key field is only returned at creation time.
Rotate API Key
POST /api/v1/keys/{uuid}/rotate/
Revokes the existing API key and creates a new one with the same name and collection access grants. The old key is immediately deactivated.
Response: 201 Created
{
"uuid": "new-key-uuid",
"name": "Acme Corp Key",
"prefix": "aqn_abc123...",
"key": "aqn_new_full_key_here",
"is_active": true,
"is_full_access": false,
"created_at": "2026-03-13T10:00:00Z"
}
The key field is only returned at rotation time. Store it securely — it cannot be retrieved again.
Errors:
404— API key not found or not owned by the authenticated user
Grant / Revoke Collection Access
POST /api/v1/keys/{uuid}/collections/
DELETE /api/v1/keys/{uuid}/collections/{collection-slug}/
Grant Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
collection |
slug | Yes | Collection slug to grant access to |
permission |
string | No | "read" (default) or "write" |
{
"collection": "my-collection",
"permission": "write"
}
If a grant already exists for this key and collection, the permission level is updated.
List Grants Response: GET /api/v1/keys/{uuid}/collections/ — 200 OK
[
{
"collection": "my-collection",
"permission": "write"
},
{
"collection": "public-docs",
"permission": "read"
}
]
Usage Stats
GET /api/v1/keys/usage/?days=30
GET /api/v1/keys/{uuid}/usage/
Webhooks
Webhooks let you receive real-time notifications when documents finish processing. Configure one webhook URL per organization to receive document.indexed and document.failed events. Payloads are signed with HMAC-SHA256 and delivered with exponential backoff retries on failure.
Get Webhook Config
GET /api/v1/webhooks/
Returns the current webhook configuration for the organization.
Response: 200 OK
{
"configured": true,
"url": "https://your-app.example.com/hooks/aqoon",
"is_active": true
}
The secret is never returned after initial creation.
curl https://your-instance/api/v1/webhooks/ \
-H "Authorization: Bearer aqn_your_api_key"
Create or Update Webhook
POST /api/v1/webhooks/
Creates a new webhook or updates the URL on an existing one.
Request Body:
{
"url": "https://your-app.example.com/hooks/aqoon"
}
On create: 201 Created — secret returned once, store it securely.
{
"configured": true,
"url": "https://your-app.example.com/hooks/aqoon",
"is_active": true,
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
On update (webhook already exists): 200 OK — secret is not returned.
{
"configured": true,
"url": "https://your-app.example.com/hooks/aqoon-updated",
"is_active": true
}
# Create
curl -X POST https://your-instance/api/v1/webhooks/ \
-H "Authorization: Bearer aqn_your_api_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.example.com/hooks/aqoon"}'
# Update URL
curl -X POST https://your-instance/api/v1/webhooks/ \
-H "Authorization: Bearer aqn_your_api_key" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.example.com/hooks/aqoon-v2"}'
Delete Webhook
DELETE /api/v1/webhooks/
Removes the webhook configuration. No more events will be delivered.
Response: 200 OK
curl -X DELETE https://your-instance/api/v1/webhooks/ \
-H "Authorization: Bearer aqn_your_api_key"
Send Test Event
POST /api/v1/webhooks/test/
Queues a test event to be delivered to the configured webhook URL immediately. Returns 404 if no webhook is configured.
Response: 200 OK
curl -X POST https://your-instance/api/v1/webhooks/test/ \
-H "Authorization: Bearer aqn_your_api_key"
Payload Format
Each event is delivered as a JSON POST to your webhook URL:
{
"event": "document.indexed",
"timestamp": "2026-03-23T10:00:00Z",
"data": {
"uuid": "a1b2c3...",
"status": "indexed",
"title": "Q1 Report",
"source_type": "pdf",
"collection_uuid": "d4e5f6...",
"collection_name": "Finance Docs",
"chunks_count": 42,
"tags": "finance,quarterly",
"error_message": null,
"original_filename": "q1-report.pdf",
"file_size": 204800
}
}
The event field is either document.indexed (processing succeeded) or document.failed (processing failed). On failure, error_message contains a description.
Signature Verification
Every webhook request includes two headers for security:
X-Aqoon-Signature: sha256=<hex-digest>X-Aqoon-Timestamp: <unix-timestamp>
The signature is an HMAC-SHA256 of {timestamp}.{raw-body} using your webhook secret. Always verify the signature before processing the payload.
import hashlib
import hmac
def verify_signature(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
message = f"{timestamp}.".encode() + body
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
import { createHmac, timingSafeEqual } from 'crypto';
function verifySignature(secret: string, timestamp: string, body: Buffer, signature: string): boolean {
const message = Buffer.concat([Buffer.from(`${timestamp}.`), body]);
const expected = 'sha256=' + createHmac('sha256', secret).update(message).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
To guard against replay attacks, reject events where X-Aqoon-Timestamp is more than 5 minutes in the past.
MCP Endpoint
The MCP server is accessible at /mcp using Streamable HTTP transport.
Auth: Authorization: Bearer aqn_<key>
Tools Available:
| Tool | Description |
|---|---|
search_documents |
Hybrid text + vector search with optional collection filter and result limit |
list_collections |
List all collections with document counts and descriptions |
get_document_info |
Get full document details and content chunks by UUID |
search_by_tag |
Find all documents with a specific tag |
Error Handling
HTTP Status Codes
| Code | Meaning |
|---|---|
200 |
Success |
201 |
Created |
400 |
Bad request (validation error) |
401 |
Unauthorized (missing or invalid token/API key) |
404 |
Resource not found |
429 |
Rate limited |
500 |
Server error |
Error Response Format
All errors use a consistent envelope with an error field and a machine-readable code field:
{
"error": "Collection not found.",
"code": "COLLECTION_NOT_FOUND"
}
Validation errors include a fields object with per-field details:
{
"error": "Validation failed.",
"code": "VALIDATION_FAILED",
"fields": {
"name": ["This field is required."],
"collection": ["Collection not found."]
}
}
Use code for programmatic error handling (e.g. switch / if logic in your client). Use error for display to end users.
Error Codes
401 — Authentication
| Code | Description |
|---|---|
INVALID_API_KEY |
API key not found or hash mismatch |
API_KEY_EXPIRED |
API key has passed its expiration date |
USER_ACCOUNT_DISABLED |
The key's user account is inactive |
AUTHENTICATION_REQUIRED |
No valid credentials provided |
403 — Authorization
| Code | Description |
|---|---|
WRITE_ACCESS_REQUIRED |
Scoped key needs write permission for this collection |
NOT_COLLECTION_OWNER |
Only the collection owner can update or delete |
SCOPED_KEY_FORBIDDEN |
Scoped keys cannot perform this action |
404 — Not Found
| Code | Description |
|---|---|
COLLECTION_NOT_FOUND |
Collection does not exist or is not visible to this key |
DOCUMENT_NOT_FOUND |
Document does not exist or is not accessible |
DOCUMENT_FILE_NOT_FOUND |
Document exists but the original file is missing from storage |
API_KEY_NOT_FOUND |
API key record not found |
GRANT_NOT_FOUND |
Collection access grant not found |
SUBSCRIPTION_NOT_FOUND |
Collection subscription not found |
WEBHOOK_NOT_CONFIGURED |
No webhook has been configured for this account |
400 — Validation
| Code | Description |
|---|---|
VALIDATION_FAILED |
One or more fields failed validation (see fields object) |
NO_FILES_PROVIDED |
Batch upload request included no files |
NO_FILE_PROVIDED |
Single upload request included no file |
UNSUPPORTED_FILE_TYPE |
File extension or MIME type is not supported |
FILE_TOO_LARGE |
File exceeds the 50 MB size limit |
INVALID_FILE_CONTENT |
File content does not match the declared type (magic byte check) |
URL_REQUIRED |
A URL source type was specified but no URL was provided |
NAME_REQUIRED |
A required name field was empty or missing |
429 — Rate Limiting
| Code | Description |
|---|---|
RATE_LIMIT_EXCEEDED |
Request quota exceeded; check X-RateLimit-* headers for reset times |
500 — Server Errors
| Code | Description |
|---|---|
SEARCH_UNAVAILABLE |
Azure AI Search is temporarily unavailable |
UPLOAD_FAILED |
File could not be written to blob storage |
WEBHOOK_TEST_FAILED |
Test event delivery to the configured webhook URL failed |