Keldura Developer API
The Keldura Developer API gives you programmatic access to your folders, prompts, documents, and folder chat. It is authenticated with API keys you create and manage from your account.
Authentication
Every request must include a valid API key in the Authorization header using the Bearer scheme:
Authorization: Bearer YOUR_API_KEYGenerate keys from the API Keys page in settings. Each key is shown in full only once at creation time — store it somewhere safe. Keys can be deactivated at any time.
A missing or invalid key returns 401 Unauthorized with a WWW-Authenticate: Bearer challenge header.
Base URL & versioning
All endpoints below are relative to:
https://keldura.ai The canonical developer endpoints live under /api/dev/v1. The unversioned /api/dev paths still work as deprecated aliases and respond with a Warning: 299 header — migrate to /api/dev/v1.
Pagination
List endpoints are paginated. Use the page (1-based, default 1) and per_page (default 50, max 200) query parameters. Responses wrap the items in a data array alongside a pagination object:
{
"data": [ ... ],
"pagination": {
"count": 50,
"total": 137,
"per_page": 50,
"total_pages": 3,
"next_page": 2,
"previous_page": null
}
}next_page and previous_page are null when there is no adjacent page.
Rate limits
Requests are rate limited per API-key owner. Every response carries the current window state:
x-ratelimit-limit— requests allowed per window.x-ratelimit-remaining— requests left in the window.x-ratelimit-reset— Unix epoch second when the window resets.
When the limit is exceeded the API responds with 429 Too Many Requests and a Retry-After header (seconds to wait).
OpenAPI spec
A machine-readable OpenAPI 3 contract is published (unauthenticated) so you can generate clients or load it into API tooling:
GET /api/dev/v1/openapi.jsonExample
curl https://keldura.ai/api/dev/v1/openapi.jsonFolders
List folders
Returns a page of folders owned by the authenticated user.
GET /api/dev/v1/folders?page=1&per_page=50curl -H "Authorization: Bearer YOUR_API_KEY" \
https://keldura.ai/api/dev/v1/foldersCreate a folder
parentId is optional; the hierarchy is two levels only (a root folder plus direct subfolders), so it must reference a root folder.
POST /api/dev/v1/folders{
"name": "My Folder",
"description": "Optional description",
"parentId": "<optional-root-folder-id>"
}curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"My Folder"}' \
https://keldura.ai/api/dev/v1/foldersGet a folder
GET /api/dev/v1/folders/{folderId}Update a folder
PUT /api/dev/v1/folders/{folderId}{
"name": "Renamed Folder",
"description": "Updated description"
}Delete a folder
Permanently deletes the folder and any associated vector stores.
DELETE /api/dev/v1/folders/{folderId}Prompts
List prompts
Returns a page of saved prompts belonging to the authenticated user.
GET /api/dev/v1/prompts?page=1&per_page=50curl -H "Authorization: Bearer YOUR_API_KEY" \
https://keldura.ai/api/dev/v1/promptsDocuments
Documents are folder-scoped. Each list item carries a status (syncing, synced, or sync_failed) reflecting indexing progress, plus a content_url to fetch the full document.
List documents in a folder
folder_id is required.
GET /api/dev/v1/documents?folder_id={folderId}&page=1&per_page=50Create a document from text
Creates a text document and attaches it to the folder; indexing then proceeds asynchronously (poll the document's status). The Location response header points at the new document.
POST /api/dev/v1/documents{
"folder_id": "<folder-id>",
"name": "Optional title",
"content": "The document text."
}curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"folder_id":"<folder-id>","content":"Hello"}' \
https://keldura.ai/api/dev/v1/documentsCreate a document from a file
Multipart upload. Text is extracted server-side (PDF, DOCX, PPT, images, or plain text), stored as a document, attached to the folder, and indexed asynchronously. Pass overwrite=true to replace an existing same-name document instead of receiving 409 Conflict.
POST /api/dev/v1/documents/file?folder_id={folderId}&overwrite=falsecurl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@notes.pdf" \
"https://keldura.ai/api/dev/v1/documents/file?folder_id=<folder-id>"Get a document
Returns the document including its inline content.
GET /api/dev/v1/documents/{documentId}Remove a document from a folder
Soft-deletes the document's link to the given folder. folder_id is required.
DELETE /api/dev/v1/documents/{documentId}?folder_id={folderId}Chat sessions
A chat session is scoped to one or more folders and an optional set of saved prompts. The response includes the session id you use to send and poll messages.
List chat sessions
GET /api/dev/v1/chats?page=1&per_page=50Create a chat session
All fields are optional; omitted arrays default to empty.
POST /api/dev/v1/chats{
"name": "My Chat",
"usedFolders": ["<folder-id>"],
"usedPrompts": ["<prompt-id>"]
}curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"My Chat","usedFolders":["<folder-id>"],"usedPrompts":[]}' \
https://keldura.ai/api/dev/v1/chatsGet a chat session
GET /api/dev/v1/chats/{sessionId}Update a chat session
All fields optional; provided fields replace the current values.
PUT /api/dev/v1/chats/{sessionId}{
"name": "Renamed Chat",
"usedFolders": ["<folder-id>"],
"usedPrompts": ["<prompt-id>"]
}Delete a chat session
Soft-deletes (reversible) the chat session.
DELETE /api/dev/v1/chats/{sessionId}Messages
Send a chat message
Submits a user message to an existing chat session and starts an asynchronous answer. Supply an optional requestId (a UUID) to correlate the result; otherwise the response returns the server-generated one. Poll the next endpoint to read the assistant answer once it is ready.
POST /api/dev/v1/chats/{sessionId}/messages{
"text": "What is this folder about?",
"requestId": "<optional-client-generated-uuid>"
}curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"What is inside this folder?"}' \
https://keldura.ai/api/dev/v1/chats/<session-id>/messagesPoll for a chat answer
Returns the current status and (when complete) the assistant answer with citations.
GET /api/dev/v1/chats/{sessionId}/messages/{requestId}Response shape
200 OKwith{ "status": "PENDING" | "PROCESSING", "requestId": "..." }— still running, poll again shortly.200 OKwith{ "status": "COMPLETED", "requestId": "...", "result": { ... } }— payload includes the answer and citations.500 Internal Server Errorwith a plain-text body containing the error message — the request failed.
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://keldura.ai/api/dev/v1/chats/<session-id>/messages/<request-id>Webhooks
Register a webhook to be told when long-running work ends instead of polling. Keldura sends an HTTP POST to your URL for two events:
transcription.completed— a transcription run completed.import.finished— an import run was processed to its end, ascompleted,completed_with_errorsorfailed.
Not every ending produces a delivery. For example, import.finished is not sent for an import that is cancelled or that consists of files uploaded from your device, and transcription.completed is not sent for a run that fails. Poll the run when you need its final state.
Register a webhook
event_type must be one of the two events above, and url must be an absolute http or https URL on a public host; anything else answers 400 Bad Request. localhost and private or internal addresses are refused. If Keldura gets no answer about the URL's host within five seconds, the answer is 503 Service Unavailable: nothing is wrong with the URL, so try again. An account can have a limited number of webhooks: 10 by default, counted across both events, and only one for the same event and URL. A registration beyond either answers 400 Bad Request; for the limit, the message names the limit in force. Deleting a webhook frees a slot. The response includes the signing secret. It is returned only this once, so store it.
POST /api/dev/v1/webhooks{
"event_type": "transcription.completed",
"url": "https://example.com/keldura-hook"
}List your webhooks
Returns your webhooks as an array. Secrets are not included.
GET /api/dev/v1/webhooksDelete a webhook
DELETE /api/dev/v1/webhooks/{webhookId}Deliveries
The body is JSON. For transcription.completed:
{
"event": "transcription.completed",
"processing_run_id": "<run-id>",
"result_history_id": "<history-id>",
"status": "completed"
}result_history_id can be null.
For import.finished:
{
"event": "import.finished",
"import_run_id": "<run-id>",
"status": "completed",
"completed_item_count": 3,
"succeeded_item_count": 3,
"failed_item_count": 0
}Each delivery carries two headers:
X-Keldura-Event— the event type, for exampleimport.finished.X-Keldura-Signature—sha256=followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret. Use the secret as text, exactly as it was returned.
Verify the signature before you trust a delivery: compute the same value over the exact bytes you received and compare it with the header.
expected = "sha256=" + hex(hmac_sha256(secret, raw_body)) A delivery that does not get a 2xx response is retried up to two more times, then dropped. Redirects are not followed: a 3xx response counts as a failed attempt. Before every attempt Keldura resolves the URL's host again. If it resolves to an address that is not public, the delivery is skipped; if it does not resolve, or the lookup gets no answer, that counts as a failed attempt. Deliveries are best effort, so keep polling as a fallback for work you cannot afford to miss.
Deprecated header names
Each delivery also carries the same two values under the header names used before the product was renamed: X-TubeScribes-Event and X-TubeScribes-Signature. These names are deprecated. Read the X-Keldura-* headers instead.
Errors
400 Bad Request— malformed request (e.g. a blank folder name or invalid body).401 Unauthorized— missing or invalid API key (returns aWWW-Authenticate: Bearerheader).403 Forbidden— the authenticated user is not allowed to access the resource, or a plan/quota limit was reached.404 Not Found— the requested folder, document, chat session, or message could not be found.409 Conflict— a document with the same file name already exists (passoverwrite=true).410 Gone— the chat session has expired (soft-deleted).429 Too Many Requests— rate limit exceeded; see theRetry-Afterheader.500 Internal Server Error— unexpected server-side failure; the response body contains the error message.503 Service Unavailable— a webhook URL's host could not be checked in time; try again.
Need help?
Questions, bug reports, or feature requests are welcome at hello@keldura.ai.
© Yuri Zelikov (sole proprietor), trading as WavelyLabs. All rights reserved.