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/uploadsRequires assets:create.
| Field | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace id or slug. |
files | Yes | 1 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/completeRequires assets:create.
| Field | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace id or slug. |
fonts | Yes | 1 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
| Status | Cause |
|---|---|
400 | In 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. |
403 | Your role or key lacks assets:create. |
Updated 4 Nov 2026