> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.webflow.com/_mcp/server. # Custom fonts > How to use the API to upload, update, and delete custom fonts on a Webflow site. Custom fonts API endpoints let you upload and manage custom fonts on a Webflow site. It uses eight OAuth v2 endpoints under `/v2/sites/{site_id}/custom_fonts`. The upload flow uses the same presigned-S3 handshake as the [Assets API](/data/docs/working-with-assets): Webflow returns signed S3 fields, and you POST the font binary directly to S3. The Webflow API server never receives the raw font bytes. ## Supported formats The API accepts the following font formats: * WOFF2 (`.woff2`) * WOFF (`.woff`) * TrueType (`.ttf`) * OpenType (`.otf`) * Embedded OpenType (`.eot`) For security reasons, SVG fonts are not supported. Sites that have pre-existing SVG fonts continue to return `format: "svg"` in read responses, but you cannot upload new SVG fonts through the API. ## Upload handshake Uploading a font is a two-step process. #### Register the font with Webflow Send a POST request to `/v2/sites/{site_id}/custom_fonts` with the font metadata and the MD5 hash of the binary, as in this example: ```javascript const { createHash } = require('node:crypto'); const { readFile } = require('node:fs/promises'); const buf = await readFile('./AcmeSans-Regular.woff2'); const fileHash = createHash('md5').update(buf).digest('hex'); const res = await fetch( `https://api.webflow.com/v2/sites/${siteId}/custom_fonts`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ fileName: 'AcmeSans-Regular.woff2', fileHash, fontFamily: 'Acme Sans', weight: 400, italic: false, fontDisplay: 'swap', }), }, ); const { customFont, upload } = await res.json(); // res.status === 202 // upload.expiresAt gives the URL expiry time (~15 minutes) ``` The API returns `202 Accepted` with a `customFont` object and an `upload` object. The `upload` object contains a presigned `url` and a `fields` map to use in the next step. #### Upload the binary to S3 Send a POST request that includes the font binary as `multipart/form-data` to `upload.url`. Include every key from `upload.fields` as a form field, then append the binary in a field named `file`. > **Warning** > > The `file` field must be the last field in the multipart form. > Any fields after `file` are ignored. ```javascript const form = new FormData(); for (const [k, v] of Object.entries(upload.fields)) { form.append(k, v); } // 'file' must be last form.append('file', new Blob([buf]), 'AcmeSans-Regular.woff2'); const s3Res = await fetch(upload.url, { method: 'POST', body: form }); // s3Res.status === 201 ``` The API returns `201 Created` on success. The font is immediately available in the site's typography picker and in published-site rendering. ## Batch registration When you need to register a whole font family at once (for example, Regular, Bold, Italic, and Bold Italic), send a POST request to `/v2/sites/{site_id}/custom_fonts/batchCreate` with up to 25 fonts in a single `items` array. This collapses the registration step into one rate-limited request instead of one request per style. Registration is batched, but the binary uploads are not: the response returns one `upload` object per font, and you still POST each font binary to its own presigned S3 URL exactly as in the [upload handshake](#upload-handshake) above. ```javascript const res = await fetch( `https://api.webflow.com/v2/sites/${siteId}/custom_fonts/batchCreate`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ items: [ { fileName: 'AcmeSans-Regular.woff2', fileHash: regularHash, fontFamily: 'Acme Sans', weight: 400, italic: false, fontDisplay: 'swap' }, { fileName: 'AcmeSans-Bold.woff2', fileHash: boldHash, fontFamily: 'Acme Sans', weight: 700, italic: false, fontDisplay: 'swap' }, ], }), }, ); const { created, failed } = await res.json(); // res.status === 200 // Upload each created[i].upload the same way as a single-font upload. ``` The API returns `200 OK` with a per-item manifest. Successfully registered fonts appear in `created`, each with its own `customFont` and `upload`. If the site's font limit is reached partway through the batch, the fonts that still fit are registered and the rest appear in `failed` with `name: "FontLimitReached"` — valid fonts are never discarded because a later font in the batch could not be registered. Each presigned URL expires roughly 15 minutes after issuance, so upload the binaries promptly. ## Examples ### Foundry or font-platform integration This example shows how a foundry or font-platform app provisions a licensed font into a user's Webflow site. ```javascript import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; async function uploadFontToSite({ siteId, accessToken, filePath, metadata }) { const buf = await readFile(filePath); const fileHash = createHash('md5').update(buf).digest('hex'); // Step 1 — register with Webflow const registerRes = await fetch( `https://api.webflow.com/v2/sites/${siteId}/custom_fonts`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ fileName: metadata.fileName, fileHash, fontFamily: metadata.fontFamily, weight: metadata.weight, italic: metadata.italic, fontDisplay: metadata.fontDisplay ?? 'swap', axes: metadata.axes ?? [], }), }, ); if (!registerRes.ok) { const err = await registerRes.json(); throw new Error(`[${err.name}] ${err.msg}`); } const { customFont, upload } = await registerRes.json(); // Step 2 — POST the binary directly to S3 const form = new FormData(); for (const [k, v] of Object.entries(upload.fields)) form.append(k, v); form.append('file', new Blob([buf]), metadata.fileName); // 'file' must be last const s3Res = await fetch(upload.url, { method: 'POST', body: form }); if (s3Res.status !== 201) throw new Error(`S3 upload failed: ${s3Res.status}`); return customFont; } ``` ### Multi-site brand font sync This example shows how an agency dashboard keeps the same brand fonts in sync across every site it manages. ```javascript async function syncBrandFonts({ targetSiteId, accessToken, brandKit }) { const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }; // List the target site's existing fonts const listRes = await fetch( `https://api.webflow.com/v2/sites/${targetSiteId}/custom_fonts`, { headers }, ); const { customFonts: existing } = await listRes.json(); // Identify fonts on the target that are not in the brand kit const brandFamilies = new Set(brandKit.map((f) => f.fontFamily)); const toRemove = existing .filter((f) => !brandFamilies.has(f.fontFamily)) .map((f) => ({ id: f.id })); // Bulk-delete the non-brand fonts if (toRemove.length > 0) { const delRes = await fetch( `https://api.webflow.com/v2/sites/${targetSiteId}/custom_fonts/batchDelete`, { method: 'POST', headers, body: JSON.stringify({ items: toRemove }) }, ); const { deleted, failed } = await delRes.json(); console.log(`Removed ${deleted.length}; ${failed.length} could not be removed.`); } // Upload missing brand fonts const existingFamilies = new Set(existing.map((f) => f.fontFamily)); for (const font of brandKit) { if (existingFamilies.has(font.fontFamily)) continue; await uploadFontToSite({ siteId: targetSiteId, accessToken, filePath: font.localPath, metadata: font, }); } } ``` ### AI or MCP agent typography setup This example shows how an AI agent uploads a brand font and verifies it is accessible before proceeding to dependent steps. ```javascript async function agentSetupTypography({ siteId, accessToken, brandFontPath, brandFontMeta }) { // 1. Upload the brand font const customFont = await uploadFontToSite({ siteId, accessToken, filePath: brandFontPath, metadata: brandFontMeta, }); // 2. Verify that the font is reachable through the API before downstream steps const verifyRes = await fetch( `https://api.webflow.com/v2/sites/${siteId}/custom_fonts/${customFont.id}`, { headers: { Authorization: `Bearer ${accessToken}` } }, ); if (!verifyRes.ok) { throw new Error('Font upload completed but resource is not yet readable.'); } return customFont; } ``` ## Limitations * **Supported formats:** WOFF2, WOFF, TTF, OTF, EOT. SVG fonts are not accepted. * **File size:** Uploads exceeding 4 MB are rejected by S3 with an `EntityTooLarge` error. * **Per-site limit:** Each site has an internal cap on total custom fonts. Exceeding the cap returns `409 Conflict`. * **Variable font axes:** Each font supports a maximum of 16 variable axes. * **Batch create:** A single batch create request supports 1–25 fonts. Binary uploads remain per-font. * **Batch delete:** A single batch delete request supports 1–100 font IDs. > Upload and manage custom fonts on a Webflow site programmatically. ## API Docs - Custom fonts [List custom fonts](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/list.md) - Custom fonts [Get custom font](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/get.md) - Custom fonts [Create custom font](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/create.md) - Custom fonts [Update custom font](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/update.md) - Custom fonts [Replace custom font file](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/replace-file.md) - Custom fonts [Delete custom font](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/delete.md) - Custom fonts [Batch create custom fonts](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/batch-create.md) - Custom fonts [Batch delete custom fonts](https://developers.webflow.com/data/v2.0.0/reference/custom-fonts/custom-fonts/batch-delete.md) ## OpenAPI Specification The raw OpenAPI 3.1 specification for this API is available at: - [OpenAPI JSON](https://developers.webflow.com/openapi.json) - [OpenAPI YAML](https://developers.webflow.com/openapi.yaml)