For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Resources
Get started
ReferenceGuidesExamplesChangelog
ReferenceGuidesExamplesChangelog
  • Data API
    • Introduction
  • Token
    • GETGet Authorization User Info
    • GETGet Authorization Info
  • Sites
    • GETList Sites
    • GETGet Site
    • GETGet Custom Domains
    • POSTPublish Site
  • Pages and Components
  • CMS
  • Forms
  • Custom Code
  • Assets
  • Custom fonts
  • Comments
    • GETList Comment Threads
    • GETGet Comment Thread
    • GETList Comment Replies
  • Ecommerce
  • Webhooks
    • GETList Webhooks
    • GETGet Webhook
    • POSTCreate Webhook
    • DELRemove Webhook
  • Site Configuration
  • Enterprise
LogoLogo
Resources
Get started
On this page
  • Supported formats
  • Upload handshake
  • Examples
  • Foundry or font-platform integration
  • Multi-site brand font sync
  • AI or MCP agent typography setup
  • Limitations

Custom fonts

Upload and manage custom fonts on a Webflow site programmatically.
Was this page helpful?
Previous

List custom fonts

Next
Built with

Custom fonts API endpoints let you upload and manage custom fonts on a Webflow site. It uses seven OAuth v2 endpoints under /v2/sites/{site_id}/custom_fonts.

The upload flow uses the same presigned-S3 handshake as the Assets API: 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.

1

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:

1const { createHash } = require('node:crypto');
2const { readFile } = require('node:fs/promises');
3
4const buf = await readFile('./AcmeSans-Regular.woff2');
5const fileHash = createHash('md5').update(buf).digest('hex');
6
7const res = await fetch(
8 `https://api.webflow.com/v2/sites/${siteId}/custom_fonts`,
9 {
10 method: 'POST',
11 headers: {
12 Authorization: `Bearer ${accessToken}`,
13 'Content-Type': 'application/json',
14 },
15 body: JSON.stringify({
16 fileName: 'AcmeSans-Regular.woff2',
17 fileHash,
18 fontFamily: 'Acme Sans',
19 weight: 400,
20 italic: false,
21 fontDisplay: 'swap',
22 }),
23 },
24);
25
26const { customFont, upload } = await res.json();
27// res.status === 202
28// 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.

2

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.

The file field must be the last field in the multipart form. Any fields after file are ignored.

1const form = new FormData();
2for (const [k, v] of Object.entries(upload.fields)) {
3 form.append(k, v);
4}
5// 'file' must be last
6form.append('file', new Blob([buf]), 'AcmeSans-Regular.woff2');
7
8const s3Res = await fetch(upload.url, { method: 'POST', body: form });
9// 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.

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.

1import { createHash } from 'node:crypto';
2import { readFile } from 'node:fs/promises';
3
4async function uploadFontToSite({ siteId, accessToken, filePath, metadata }) {
5 const buf = await readFile(filePath);
6 const fileHash = createHash('md5').update(buf).digest('hex');
7
8 // Step 1 — register with Webflow
9 const registerRes = await fetch(
10 `https://api.webflow.com/v2/sites/${siteId}/custom_fonts`,
11 {
12 method: 'POST',
13 headers: {
14 Authorization: `Bearer ${accessToken}`,
15 'Content-Type': 'application/json',
16 },
17 body: JSON.stringify({
18 fileName: metadata.fileName,
19 fileHash,
20 fontFamily: metadata.fontFamily,
21 weight: metadata.weight,
22 italic: metadata.italic,
23 fontDisplay: metadata.fontDisplay ?? 'swap',
24 axes: metadata.axes ?? [],
25 }),
26 },
27 );
28
29 if (!registerRes.ok) {
30 const err = await registerRes.json();
31 throw new Error(`[${err.name}] ${err.msg}`);
32 }
33
34 const { customFont, upload } = await registerRes.json();
35
36 // Step 2 — POST the binary directly to S3
37 const form = new FormData();
38 for (const [k, v] of Object.entries(upload.fields)) form.append(k, v);
39 form.append('file', new Blob([buf]), metadata.fileName); // 'file' must be last
40
41 const s3Res = await fetch(upload.url, { method: 'POST', body: form });
42 if (s3Res.status !== 201) throw new Error(`S3 upload failed: ${s3Res.status}`);
43
44 return customFont;
45}

Multi-site brand font sync

This example shows how an agency dashboard keeps the same brand fonts in sync across every site it manages.

1async function syncBrandFonts({ targetSiteId, accessToken, brandKit }) {
2 const headers = {
3 Authorization: `Bearer ${accessToken}`,
4 'Content-Type': 'application/json',
5 };
6
7 // List the target site's existing fonts
8 const listRes = await fetch(
9 `https://api.webflow.com/v2/sites/${targetSiteId}/custom_fonts`,
10 { headers },
11 );
12 const { customFonts: existing } = await listRes.json();
13
14 // Identify fonts on the target that are not in the brand kit
15 const brandFamilies = new Set(brandKit.map((f) => f.fontFamily));
16 const toRemove = existing
17 .filter((f) => !brandFamilies.has(f.fontFamily))
18 .map((f) => ({ id: f.id }));
19
20 // Bulk-delete the non-brand fonts
21 if (toRemove.length > 0) {
22 const delRes = await fetch(
23 `https://api.webflow.com/v2/sites/${targetSiteId}/custom_fonts/batchDelete`,
24 { method: 'POST', headers, body: JSON.stringify({ items: toRemove }) },
25 );
26 const { deleted, failed } = await delRes.json();
27 console.log(`Removed ${deleted.length}; ${failed.length} could not be removed.`);
28 }
29
30 // Upload missing brand fonts
31 const existingFamilies = new Set(existing.map((f) => f.fontFamily));
32 for (const font of brandKit) {
33 if (existingFamilies.has(font.fontFamily)) continue;
34 await uploadFontToSite({
35 siteId: targetSiteId,
36 accessToken,
37 filePath: font.localPath,
38 metadata: font,
39 });
40 }
41}

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.

1async function agentSetupTypography({ siteId, accessToken, brandFontPath, brandFontMeta }) {
2 // 1. Upload the brand font
3 const customFont = await uploadFontToSite({
4 siteId,
5 accessToken,
6 filePath: brandFontPath,
7 metadata: brandFontMeta,
8 });
9
10 // 2. Verify that the font is reachable through the API before downstream steps
11 const verifyRes = await fetch(
12 `https://api.webflow.com/v2/sites/${siteId}/custom_fonts/${customFont.id}`,
13 { headers: { Authorization: `Bearer ${accessToken}` } },
14 );
15 if (!verifyRes.ok) {
16 throw new Error('Font upload completed but resource is not yet readable.');
17 }
18
19 return customFont;
20}

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 delete: A single batch delete request supports 1–100 font IDs.