CreateUpload
Reserves a short-lived slot for a file's bytes and returns a presigned URL bound to their checksum.
Send the bytes with uploadMethod to uploadUrl, with every header in requiredHeaders,
before expiresAt; then pass the upload's id to the operation that uses it. The SDKs'
uploads.create(file) does all of this.
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/uploads |
| TypeScript | caitlyn.uploads.reserve({ … }) |
| Python | caitlyn.uploads.reserve(…) |
| Go | client.Uploads.Reserve(ctx, &caitlyn.CreateUploadInput{…}) |
| Rust | client.uploads().reserve(CreateUploadInput { … }) |
| Stability | Generally available |
| Since | Caitlyn 5.115.0 |
| Retried by the SDKs | Yes, on 429, 503 and connection failures (Retries) |
The SDKs also have uploads.create(file) (Python uploads.create(file=…), Go Uploads.Create, Rust uploads().create), which computes the checksum, reserves the upload and sends the bytes to uploadUrl with requiredHeaders, returning the reserved upload.
Request
Section titled “Request”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 |
|---|---|---|---|
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. |
fileName (required) |
string | body | The file's name on the caller's machine. 1 to 1024 characters. |
contentType (required) |
string | body | The file's media type. 3 to 255 characters. |
byteSize (required) |
long | body | The file's size in bytes. |
sha256 (required) |
string | body | The base64-encoded SHA-256 of the file's bytes. The upload URL accepts only these bytes. 44 to 44 characters. Pattern ^[A-Za-z0-9+/]{43}=$. |
Response
Section titled “Response”Status 201.
| 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 upload, as /api/v3/uploads/{uploadId}. 1 to 2048 characters. |
id (required) |
string | body | The upload's identifier, passed to the operation that uses it. 1 to 128 characters. |
uploadUrl (required) |
string | body | Where to send the bytes. Expires at expiresAt. 1 to 4096 characters. |
uploadMethod (required) |
string | body | The HTTP method to send the bytes with, usually PUT. 3 to 16 characters. Pattern ^[A-Z]+$. |
requiredHeaders (required) |
map of string to string | body | Headers that must be sent with the bytes, exactly as given. |
expiresAt (required) |
timestamp (RFC 3339) | body | When the upload URL stops working. |
status (required) |
UploadStatus | body | Where the upload is in its lifecycle. |
Every result also keeps the JSON it was read from, unknown fields included, as raw, and the response metadata (HTTP status, request ID, ETag).
Permissions
Section titled “Permissions”- Permission:
files.create. - 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.
Errors
Section titled “Errors”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. |
| 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. |
| 413 | PayloadTooLargeError |
PAYLOAD_TOO_LARGE |
The body, a file or the send context is over its documented limit. |
| 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. |
Examples
Section titled “Examples”Reserve an upload for a PDF
Section titled “Reserve an upload for a PDF”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.uploads.reserve({ fileName: 'leave-policy.pdf', contentType: 'application/pdf', byteSize: 482113, sha256: 'n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=',}, { idempotencyKey: '9b1d4e2a-7c3f-4a8e-b5d6-0e1f2a3b4c5d' });console.log(result);import os
from caitlyn import Caitlynfrom caitlyn.auth import Token
caitlyn = Caitlyn(workspace="your-company.getcaitlyn.ai", auth=Token(os.environ["CAITLYN_TOKEN"]))
result = caitlyn.uploads.reserve( idempotency_key="9b1d4e2a-7c3f-4a8e-b5d6-0e1f2a3b4c5d", file_name="leave-policy.pdf", content_type="application/pdf", byte_size=482113, sha256="n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=",)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.Uploads.Reserve(ctx, &caitlyn.CreateUploadInput{ FileName: "leave-policy.pdf", ContentType: "application/pdf", ByteSize: 482113, SHA256: "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", }, caitlyn.WithIdempotencyKey("9b1d4e2a-7c3f-4a8e-b5d6-0e1f2a3b4c5d")) if err != nil { log.Fatal(err) } out, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(out))}use caitlyn_sdk::{Auth, Client, CreateUploadInput};
#[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 .uploads() .reserve(CreateUploadInput { file_name: "leave-policy.pdf".into(), content_type: "application/pdf".into(), byte_size: 482113, sha256: "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=".into(), }) .idempotency_key("9b1d4e2a-7c3f-4a8e-b5d6-0e1f2a3b4c5d") .await?; println!("{result:#?}"); Ok(())}The HTTP exchange:
POST /api/v3/uploads HTTP/1.1Host: your-company.getcaitlyn.aiAuthorization: Bearer <token>Idempotency-Key: 9b1d4e2a-7c3f-4a8e-b5d6-0e1f2a3b4c5dContent-Type: application/json
{ "fileName": "leave-policy.pdf", "contentType": "application/pdf", "byteSize": 482113, "sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="}HTTP/1.1 201Location: /api/v3/uploads/0193a6f2-1111-7a2b-8c3d-4e5f60718293Content-Type: application/json
{ "id": "0193a6f2-1111-7a2b-8c3d-4e5f60718293", "uploadUrl": "https://uploads.example.com/0193a6f2-1111-7a2b-8c3d-4e5f60718293?signature=abc", "uploadMethod": "PUT", "requiredHeaders": { "Content-Type": "application/pdf" }, "expiresAt": "2026-11-10T01:17:03.000Z", "status": "awaiting_bytes"}A file over the size limit
Section titled “A file over the size limit”import { Caitlyn, PayloadTooLargeError } 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.uploads.reserve({ fileName: 'archive.zip', contentType: 'application/zip', byteSize: 9000000000, sha256: 'n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=', }); console.log(result);} catch (err) { if (!(err instanceof PayloadTooLargeError)) throw err; console.error(err.status, err.code, err.detail, err.requestId);}import os
from caitlyn import Caitlyn, PayloadTooLargeErrorfrom caitlyn.auth import Token
caitlyn = Caitlyn(workspace="your-company.getcaitlyn.ai", auth=Token(os.environ["CAITLYN_TOKEN"]))
try: result = caitlyn.uploads.reserve( file_name="archive.zip", content_type="application/zip", byte_size=9000000000, sha256="n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", ) print(result)except PayloadTooLargeError 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.Uploads.Reserve(ctx, &caitlyn.CreateUploadInput{ FileName: "archive.zip", ContentType: "application/zip", ByteSize: 9000000000, SHA256: "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", }) if err != nil { var apiErr *caitlyn.PayloadTooLargeError 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, CreateUploadInput};
#[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 .uploads() .reserve(CreateUploadInput { file_name: "archive.zip".into(), content_type: "application/zip".into(), byte_size: 9000000000, sha256: "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=".into(), }) .await?; println!("{result:#?}"); Ok(()) } .await; match outcome { Err(caitlyn_sdk::Error::PayloadTooLarge(err)) => eprintln!("{} {} {:?} {:?}", err.status, err.code, err.detail, err.request_id), other => other?, } Ok(())}The HTTP exchange:
POST /api/v3/uploads HTTP/1.1Host: your-company.getcaitlyn.aiAuthorization: Bearer <token>Idempotency-Key: <generated by the SDK>Content-Type: application/json
{ "fileName": "archive.zip", "contentType": "application/zip", "byteSize": 9000000000, "sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="}HTTP/1.1 413Content-Type: application/problem+json
{ "type": "https://build.caitlyn.ai/errors/PAYLOAD_TOO_LARGE", "title": "Payload too large", "status": 413, "code": "PAYLOAD_TOO_LARGE", "detail": "Files can be at most 500 MB.", "requestId": "01JC9Q2W7R4M0H2T6N1S8V3B65"}
