Kalpana Docs
Resources

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/uploads

Requires assets:create.

FieldRequiredDescription
workspaceIdYesWorkspace id or slug.
filesYes1 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/complete

Requires assets:create.

FieldRequiredDescription
workspaceIdYesWorkspace id or slug.
filesYes1 to 50 items of { contentHash, filename? }. Without filename a new image is named asset-<contentHash>.
collectionIdsNoImage 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.

Was this page helpful?

Updated 4 Nov 2026

On this page