Skip to content

CreateDocument

Creates a document from an upload; ingestion continues after the response.

Returns 202 with the document in its initial state and the Operation that tracks ingestion, and Location names the Operation. Needs the files permission, read access to the knowledge base and access to the folder; it does not need update access to the knowledge base, which is for the knowledge base's own configuration.

Not yet available. The SDKs have this operation, but no Caitlyn release serves it yet: a call gets 404. This page describes how it will work. GetAgentVersion is the one v3 operation served today.

HTTP POST /api/v3/knowledge-bases/{knowledgeBaseId}/documents
TypeScript caitlyn.documents.create({ … })
Python caitlyn.documents.create(…)
Go client.Documents.Create(ctx, &caitlyn.CreateDocumentInput{…})
Rust client.documents().create(CreateDocumentInput { … })
Stability Generally available
Since Caitlyn 5.115.0
Retried by the SDKs Yes, on 429, 503 and connection failures (Retries)

The SDKs also have documents.upload(file, …) (Python documents.upload(file=…, …), Go Documents.Upload, Rust documents().upload), which uploads the file first and then calls this operation with uploadId set to the upload's ID.

Member names are shown as they are in TypeScript and on the wire. Python and Rust use snake case (knowledge_base_id) and Go uses exported names (KnowledgeBaseID). idempotencyKey is optional: the SDKs generate one per call and reuse it on every retry. Pass your own as a call option (TypeScript { idempotencyKey }, Python idempotency_key=, Go caitlyn.WithIdempotencyKey, Rust .idempotency_key(…)) to make a retry of your own safe.

Member Type Sent in Description
knowledgeBaseId (required) string path The knowledge base to add the document to. 1 to 128 characters.
idempotencyKey string header Idempotency-Key Makes the request safe to retry. The SDKs generate one per call when the caller does not supply one. 1 to 255 characters.
uploadId (required) string body The upload holding the document's bytes. Each upload can be used once. 1 to 128 characters.
folder (required) string body The folder to put the document in. 1 to 1024 characters. Pattern `^(/
name string body The document's name. Absent means the uploaded file's name. 1 to 1024 characters.

Status 202.

Member Type Sent in Description
requestId string header Caitlyn-Request-Id The request's identifier. The same value is in every error body; quote it to support. 1 to 128 characters.
apiVersion string header Caitlyn-Api-Version The server's model version, for example 5.120.0. The SDKs keep the latest value they have seen and refuse to call operations or send members newer than it. 5 to 32 characters. Pattern ^[0-9]+\.[0-9]+\.[0-9]+$.
location string header Location The Operation that tracks ingestion, as /api/v3/operations/{operationId}. 1 to 2048 characters.
document (required) Document body The document in its initial state.
operation (required) Operation body The Operation that tracks ingestion.

Every result also keeps the JSON it was read from, unknown fields included, as raw, and the response metadata (HTTP status, request ID, ETag).

  • Permission: files.create.
  • Checked on: the knowledge bas named by knowledgeBaseId, which needs the read grant. The folder in folder must be one the caller can reach.
  • Who can call it: people (signed in, or a personal access token), API clients (machine sign-in).

The checks run in this order: who is calling, the workspace and the permission (each refused with 403), then the record (404, which is also what a record the caller may not see returns). Errors explains why.

The SDKs raise the error class for the response's HTTP status, whether or not the operation lists it (Errors). The statuses this operation returns:

Status Error Codes When
400 ValidationError VALIDATION_FAILED, UNKNOWN_FIELD, UNKNOWN_FILTER, UNKNOWN_SORT, INVALID_CURSOR The request is malformed: a member is missing, the wrong type or out of range; it has a member, filter or sort field the operation does not define; or its cursor is not valid for this list.
401 UnauthenticatedError UNAUTHENTICATED No credential was sent, or it is expired or not valid.
403 PermissionDeniedError PERMISSION_DENIED, NOT_AVAILABLE_TO_CREDENTIALS, PRIMARY_WORKSPACE_REQUIRED The caller may not call this operation: it lacks the permission, its credential type may not call it, or the operation is served only on the organisation's primary workspace. Decided before any record is read. A record the caller may not see is 404 instead.
404 NotFoundError NOT_FOUND The record does not exist, or the caller may not see it. The two are deliberately the same, so identifiers cannot be probed.
409 ConflictError CONFLICT, ALREADY_EXISTS, IDEMPOTENCY_KEY_IN_USE, OPERATION_NOT_CANCELLABLE The resource's state does not allow the request, a unique name is taken, or a request with the same idempotency key is still in progress.
422 UnprocessableContentError IDEMPOTENCY_KEY_REUSED The idempotency key was already used with a different request body.
429 TooManyRequestsError RATE_LIMITED Too many requests; wait for Retry-After and try again. Retried by the SDKs.
500 InternalError INTERNAL An unexpected server failure. Not retried by the SDKs.
503 ServiceUnavailableError SERVICE_UNAVAILABLE A dependency is briefly unavailable, or the request's audit event could not be recorded; wait for Retry-After and try again. Retried by the SDKs.
import { Caitlyn } from '@caitlyn-ai/sdk';
const caitlyn = new Caitlyn({ workspace: 'your-company.getcaitlyn.ai', auth: { mode: 'token', token: process.env.CAITLYN_TOKEN! } });
const result = await caitlyn.documents.create({
knowledgeBaseId: 'KB4T7YQ2WL',
uploadId: '0193a6f2-1111-7a2b-8c3d-4e5f60718293',
folder: 'hr/',
name: 'Leave policy',
}, { idempotencyKey: '3f0c9a1e-5d2b-4f7a-9c1e-2b8d4a6f0e13' });
console.log(result);
import os
from caitlyn import Caitlyn
from caitlyn.auth import Token
caitlyn = Caitlyn(workspace="your-company.getcaitlyn.ai", auth=Token(os.environ["CAITLYN_TOKEN"]))
result = caitlyn.documents.create(
idempotency_key="3f0c9a1e-5d2b-4f7a-9c1e-2b8d4a6f0e13",
knowledge_base_id="KB4T7YQ2WL",
upload_id="0193a6f2-1111-7a2b-8c3d-4e5f60718293",
folder="hr/",
name="Leave policy",
)
print(result)
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
caitlyn "github.com/caitlyn-ai/caitlyn-go"
)
func main() {
ctx := context.Background()
client, err := caitlyn.NewClient("your-company.getcaitlyn.ai", caitlyn.Token(os.Getenv("CAITLYN_TOKEN")))
if err != nil {
log.Fatal(err)
}
result, err := client.Documents.Create(ctx, &caitlyn.CreateDocumentInput{
KnowledgeBaseID: "KB4T7YQ2WL",
UploadID: "0193a6f2-1111-7a2b-8c3d-4e5f60718293",
Folder: "hr/",
Name: caitlyn.Ptr("Leave policy"),
}, caitlyn.WithIdempotencyKey("3f0c9a1e-5d2b-4f7a-9c1e-2b8d4a6f0e13"))
if err != nil {
log.Fatal(err)
}
out, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(out))
}
use caitlyn_sdk::{Auth, Client, CreateDocumentInput};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new("your-company.getcaitlyn.ai", Auth::token(std::env::var("CAITLYN_TOKEN")?))?;
let result = client
.documents()
.create(CreateDocumentInput {
knowledge_base_id: "KB4T7YQ2WL".into(),
upload_id: "0193a6f2-1111-7a2b-8c3d-4e5f60718293".into(),
folder: "hr/".into(),
name: Some("Leave policy".into()),
})
.idempotency_key("3f0c9a1e-5d2b-4f7a-9c1e-2b8d4a6f0e13")
.await?;
println!("{result:#?}");
Ok(())
}

The HTTP exchange:

POST /api/v3/knowledge-bases/KB4T7YQ2WL/documents HTTP/1.1
Host: your-company.getcaitlyn.ai
Authorization: Bearer <token>
Idempotency-Key: 3f0c9a1e-5d2b-4f7a-9c1e-2b8d4a6f0e13
Content-Type: application/json
{
"uploadId": "0193a6f2-1111-7a2b-8c3d-4e5f60718293",
"folder": "hr/",
"name": "Leave policy"
}
HTTP/1.1 202
Location: /api/v3/operations/0193a6f2-2222-7b3c-9d4e-5f6071829304
Content-Type: application/json
{
"document": {
"id": "doc-3b7f9e21",
"knowledgeBaseId": "KB4T7YQ2WL",
"name": "Leave policy",
"folder": "hr/",
"status": "pending",
"createdAt": "2026-11-10T01:02:03.000Z",
"updatedAt": "2026-11-10T01:02:03.000Z"
},
"operation": {
"id": "0193a6f2-2222-7b3c-9d4e-5f6071829304",
"kind": "document_ingestion",
"status": "queued",
"createdAt": "2026-11-10T01:02:03.000Z"
}
}

The upload was already used, or has expired

Section titled “The upload was already used, or has expired”
import { Caitlyn, ConflictError } from '@caitlyn-ai/sdk';
const caitlyn = new Caitlyn({ workspace: 'your-company.getcaitlyn.ai', auth: { mode: 'token', token: process.env.CAITLYN_TOKEN! } });
try {
const result = await caitlyn.documents.create({
knowledgeBaseId: 'KB4T7YQ2WL',
uploadId: '0193a6f2-1111-7a2b-8c3d-4e5f60718293',
folder: 'hr/',
});
console.log(result);
} catch (err) {
if (!(err instanceof ConflictError)) throw err;
console.error(err.status, err.code, err.detail, err.requestId);
}
import os
from caitlyn import Caitlyn, ConflictError
from caitlyn.auth import Token
caitlyn = Caitlyn(workspace="your-company.getcaitlyn.ai", auth=Token(os.environ["CAITLYN_TOKEN"]))
try:
result = caitlyn.documents.create(
knowledge_base_id="KB4T7YQ2WL",
upload_id="0193a6f2-1111-7a2b-8c3d-4e5f60718293",
folder="hr/",
)
print(result)
except ConflictError as err:
print(err.status, err.code, err.detail, err.request_id)
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
caitlyn "github.com/caitlyn-ai/caitlyn-go"
)
func main() {
ctx := context.Background()
client, err := caitlyn.NewClient("your-company.getcaitlyn.ai", caitlyn.Token(os.Getenv("CAITLYN_TOKEN")))
if err != nil {
log.Fatal(err)
}
result, err := client.Documents.Create(ctx, &caitlyn.CreateDocumentInput{
KnowledgeBaseID: "KB4T7YQ2WL",
UploadID: "0193a6f2-1111-7a2b-8c3d-4e5f60718293",
Folder: "hr/",
})
if err != nil {
var apiErr *caitlyn.ConflictError
if errors.As(err, &apiErr) {
fmt.Println(apiErr.Status, apiErr.Code, apiErr.Detail, apiErr.RequestID)
return
}
log.Fatal(err)
}
out, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(out))
}
use caitlyn_sdk::{Auth, Client, CreateDocumentInput};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new("your-company.getcaitlyn.ai", Auth::token(std::env::var("CAITLYN_TOKEN")?))?;
let outcome: Result<(), caitlyn_sdk::Error> = async {
let result = client
.documents()
.create(CreateDocumentInput {
knowledge_base_id: "KB4T7YQ2WL".into(),
upload_id: "0193a6f2-1111-7a2b-8c3d-4e5f60718293".into(),
folder: "hr/".into(),
..Default::default()
})
.await?;
println!("{result:#?}");
Ok(())
}
.await;
match outcome {
Err(caitlyn_sdk::Error::Conflict(err)) => eprintln!("{} {} {:?} {:?}", err.status, err.code, err.detail, err.request_id),
other => other?,
}
Ok(())
}

The HTTP exchange:

POST /api/v3/knowledge-bases/KB4T7YQ2WL/documents HTTP/1.1
Host: your-company.getcaitlyn.ai
Authorization: Bearer <token>
Idempotency-Key: <generated by the SDK>
Content-Type: application/json
{
"uploadId": "0193a6f2-1111-7a2b-8c3d-4e5f60718293",
"folder": "hr/"
}
HTTP/1.1 409
Content-Type: application/problem+json
{
"type": "https://build.caitlyn.ai/errors/CONFLICT",
"title": "Conflict",
"status": 409,
"code": "CONFLICT",
"detail": "Upload 0193a6f2-1111-7a2b-8c3d-4e5f60718293 has already been used. Reserve a new upload.",
"requestId": "01JC9Q2W7R4M0H2T6N1S8V3B64"
}