Kalpana Docs
Resources

Upload fonts directly

Send font files straight to storage with signed URLs, then record them.

Two requests record fonts without putting the bytes in a JSON body. Use this for large families (up to 40 MB per file) or when uploading up to 50 fonts at once.

Step 1: request upload URLs

POST /api/fonts/uploads

Requires assets:create.

FieldRequiredDescription
workspaceIdYesWorkspace id or slug.
filesYes1 to 50 items of { contentHash, filename, sizeBytes }. contentHash must be a lowercase SHA-256 hex digest, filename must end in .ttf, .otf, .woff or .woff2, and sizeBytes must be a positive whole number up to 40 MB.
curl --request POST "https://app.kalpana.one/api/fonts/uploads" \
  --header "x-api-key: $KALPANA_API_KEY" \
  --header "content-type: application/json" \
  --data '{
    "workspaceId": "acme",
    "files": [{ "contentHash": "HEX_SHA256", "filename": "Inter-Bold.ttf", "sizeBytes": 310200 }]
  }'
{
  "uploads": [
    {
      "contentHash": "HEX_SHA256",
      "contentType": "font/ttf",
      "uploadUrl": "https://storage.example/signed-put-url"
    }
  ]
}

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. A uploadUrl of null means the workspace already holds that file; skip its upload.

Step 2: record the fonts

POST /api/fonts/uploads/complete

Requires assets:create.

FieldRequiredDescription
workspaceIdYesWorkspace id or slug.
fontsYes1 to 50 items of { family, style, contentHash, filename, weight?, italic? }, with contentHash as sent in step 1. weight (400 by default) and italic (false by default) are fallbacks; the values read from the font file take precedence.
{
  "fonts": [
    {
      "id": "0192f8a4-6c1e-7c3a-9b0e-4d2f1a7e5c11",
      "family": "Inter",
      "style": "Bold",
      "weight": 700,
      "italic": false,
      "filename": "Inter-Bold.ttf"
    }
  ],
  "failed": []
}

fonts holds the recorded fonts in the shape of List fonts, without url. A font the workspace already holds with the same contentHash is returned as it is. A font that could not be recorded is reported in failed as { contentHash, message } while the rest still land, for example when the upload never reached storage, the file name has an unsupported extension, or the same family, style, weight and italic flag already exists from another file.

Errors

StatusCause
400In step 1, a file name has an unsupported extension or a file is over 40 MB. In either step, a contentHash is not a lowercase SHA-256 hex digest.
403Your role or key lacks assets:create.
Was this page helpful?

Updated 4 Nov 2026

On this page