Upload assets directly
Send images straight to storage with signed URLs, then record them.
Two requests record images without putting the bytes in a JSON body. Use this for up to 50 images at once, or for files near the size limit, where base64 in Upload an asset adds a third to the request.
Step 1: request upload URLs
POST /api/assets/uploadsRequires assets:create.
| Field | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace id or slug. |
files | Yes | 1 to 50 items of { contentHash, contentType, sizeBytes }. contentHash must be a lowercase SHA-256 hex digest of the file. contentType is image/png, image/jpeg, image/jpg, image/webp, image/gif or image/bmp. sizeBytes is the file's size in bytes, a whole number up to 5 MB. |
curl --request POST "https://app.kalpana.one/api/assets/uploads" \
--header "x-api-key: $KALPANA_API_KEY" \
--header "content-type: application/json" \
--data '{
"workspaceId": "acme",
"files": [{ "contentHash": "HEX_SHA256", "contentType": "image/png", "sizeBytes": 20480 }]
}'{
"uploads": [
{
"contentHash": "HEX_SHA256",
"contentType": "image/png",
"uploadUrl": "https://storage.example/signed-put-url"
}
]
}contentType comes back in lower case. PUT the file to each uploadUrl with the content-type header set to the returned contentType and a body of exactly sizeBytes bytes. The link is valid for one hour. An uploadUrl of null means the workspace already holds that image; skip its upload.
AVIF is not accepted here, because it has to be converted to PNG on the server. Send AVIF files through Upload an asset. A type outside the list fails the whole request with 400 and Unsupported image content type: <type>. A size over 5 MB fails it with 400 and Image exceeds the 5 MB limit.
Step 2: record the images
POST /api/assets/uploads/completeRequires assets:create.
| Field | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace id or slug. |
files | Yes | 1 to 50 items of { contentHash, filename? }. Without filename a new image is named asset-<contentHash>. |
collectionIds | No | Image collections to add every recorded image to. Each must be a UUID. Ids that are not in the workspace are skipped. |
curl --request POST "https://app.kalpana.one/api/assets/uploads/complete" \
--header "x-api-key: $KALPANA_API_KEY" \
--header "content-type: application/json" \
--data '{
"workspaceId": "acme",
"files": [{ "contentHash": "HEX_SHA256", "filename": "logo.png" }]
}'{
"assets": [
{
"id": "0b0f5a3e-5b1c-4d65-9a5e-3f7d2f2f6c11",
"filename": "logo.png",
"contentType": "image/png",
"sizeBytes": 20480,
"contentHash": "HEX_SHA256",
"url": "https://cdn.example/temporary-url"
}
],
"failed": []
}assets holds the recorded images, each with a temporary url and the fields shown in List assets except collectionIds. Use each id as an image variable's asset_id. An image the workspace already held is returned as it is, renamed when you send a new filename. An image that could not be recorded is reported in failed as { contentHash, message } while the rest still land, for example The upload did not reach storage or Image exceeds the 5 MB limit.
Updated 4 Nov 2026