Personal Fonts
Upload fonts to My Fonts and use them in image creation, editing, and Creative Director.
My Fonts is an account-owned library shared by the website, REST, SDK, and MCP. Upload OTF, TTF, or WOFF2 files (one file per request, up to 5 MiB). Matching styles are grouped into families.
These interfaces require a backend and client version exposing personal fonts. If your MCP tool inventory does not include the font tools below, update that client/server before using this workflow.
My fonts and Font collection
My fonts contains your private uploads. Font collection contains curated, ready-to-use families available to every authenticated account. Collection entries are read-only; upload, rename, and delete apply only to your own fonts.
Filter the same paginated list with source=custom (default), source=global, or source=all. For example:
// MCP
list_fonts({ source: 'global', search: 'Noto', limit: 20 });
// Server-side SDK
await fonts.list({ source: 'global', search: 'Noto', limit: 20 });
// REST: GET /api/v1/fonts?source=global&search=Noto&limit=20Results include source: "custom" | "global". Inspect a family for its available styles, supported scripts, and collection license information. The initial collection includes Inter, Montserrat, Lora, Playfair Display, Noto Sans, Noto Serif, Heebo, Assistant, Rubik, Frank Ruhl Libre, Noto Sans Hebrew, and Noto Sans Arabic. Language and style coverage varies by family; do not assume every weight or language is available.
Select up to three families across both sources using the same font_ids. No upload or separate specimen workflow is needed for collection fonts. Curated font files retain their Open Font License notices. Collection availability depends on the deployed catalog.
Upload with REST
Use your account authentication with the dedicated endpoint—not media upload:
curl --request POST "$KOLBO_API_ORIGIN/api/v1/fonts" \
--header "X-API-Key: $KOLBO_API_KEY" \
--form "[email protected]"The multipart body accepts exactly one file in the field file (the filename must end in .otf, .ttf or .woff2, and the bytes must carry a real OTF/TTF/WOFF2 signature) plus an optional language field (a 2–16 character language tag such as he; anything else falls back to en) used when rendering the style preview.
The request returns 202 Accepted with data: { id, status }, where id is the upload ID and status is processing, ready or failed. Read GET /api/v1/fonts/uploads/:id until ready (it then also returns font_id) or failed (it then returns error). Use the returned font_id for generation, not the upload ID. Uploading a file identical to one already in your library, or to one still processing, returns that existing upload instead of a new one. Do not retry a failed security check through media upload.
| Operation | Route | Notes |
|---|---|---|
| List/search | GET /api/v1/fonts?source=custom&search=Birzia&limit=20 | Query: source (custom default, global, all), search (case-insensitive name match, up to 120 characters), limit (1–50, default 30), cursor |
| Inspect family/styles/scripts | GET /api/v1/fonts/:id | Works for your fonts and collection fonts |
| Style preview (PNG) | GET /api/v1/fonts/:id/styles/:styleId/preview | Authenticated; the preview_url on each style |
| Style font file | GET /api/v1/fonts/:id/styles/:styleId/file | Authenticated; returns the font bytes for browser rendering |
| Rename | PATCH /api/v1/fonts/:id with {"name":"My Font"} | Your fonts only; 1–120 characters |
| Delete | DELETE /api/v1/fonts/:id | Your fonts only; returns data: { id, deleted: true } |
| Upload | POST /api/v1/fonts (multipart) | Returns 202 |
| Upload status | GET /api/v1/fonts/uploads/:id | processing, ready or failed |
| Issue upload ticket | POST /api/v1/fonts/upload-tickets | Returns 201 with data: { ticket, upload_url, expires_at } |
A family object contains id, name, status (ready), source (custom or global), license and license_url (collection fonts only), created_at, updated_at, and styles[] with id, name, weight, italic, version, scripts and preview_url.
Lists return { data, next_cursor }; send next_cursor as cursor for the next page (null means last page). Previews and font files require account authentication. Deletion prevents future use; it does not delete completed generated images. Collection fonts cannot be renamed or deleted (404 FONT_NOT_FOUND). Write operations need an API key with write permission.
Limits and errors
| Limit | Value |
|---|---|
| File size | 5 MiB, one file per request |
| Upload attempts | 10 per minute per account |
| Accepted uploads | 20 per hour; 3 processing at once per account |
| Library size | 30 families, 60 styles in total, 20 styles per family |
| Outstanding upload tickets | 3 unexpired tickets per account |
Errors return { "success": false, "code": "...", "error": "..." }. Common codes: INVALID_FONT_FILE (400), INVALID_QUERY (400), INVALID_FONT_NAME (400), INVALID_FONT_IDS (400), FONT_NOT_FOUND (404), FONT_UPLOAD_NOT_FOUND (404), INVALID_FONT_TICKET (401), FONT_UPLOAD_LIMIT (429), FONT_WORKER_BUSY (429, retry shortly), FONT_UPLOAD_UNAVAILABLE (503), and 413 when the file exceeds the size limit. Reaching a library limit while an upload is processing marks that upload failed.
MCP uploads
- Local stdio:
upload_font({file_path: "C:/fonts/BIRZIA-BOLD.OTF"}). - Local stdio with a preview language:
upload_font({file_path: "C:/fonts/BIRZIA-BOLD.OTF", language: "he"}). - Remote with shell access: call
create_font_upload_ticket, then POST multipart fieldfileto its returnedupload_urlexactly as given usingAuthorization: Bearer <ticket>. No API key is sent with this request; the ticket authorizes exactly one font upload. It is single-use and expires after ten minutes. Never follow redirects or publish the ticket. - Browser-only: call
font_upload_widgetand choose the file in its dedicated card.
Poll get_font_upload_status({upload_id}) until ready or failed, with a delay between checks. Retrieve the family with get_font or find it using list_fonts({search:"Birzia"}). Library management also exposes rename_font and delete_font. These uploads appear in the same My Fonts library as website uploads.
SDK
Use the account client only on a trusted server. apiUrl is the API origin, without /api.
import { createFontClient } from '@kolbo/app-sdk';
import { readFile } from 'node:fs/promises';
const fonts = createFontClient({
apiUrl: process.env.KOLBO_API_ORIGIN,
apiKey: process.env.KOLBO_API_KEY,
});
const uploaded = await fonts.upload(
new Blob([await readFile('./BIRZIA-BOLD.OTF')]),
'BIRZIA-BOLD.OTF',
);
const status = await fonts.status(uploaded.data.id);
// If still processing, check again later; generate only once ready.
const library = await fonts.list({ search: 'Birzia' });Methods: list, get, upload, status, rename, delete, createUploadTicket, and grantToApp. createFontClient throws in a browser and requires an HTTPS apiUrl (plain HTTP only for localhost or 127.0.0.1). Never embed an account API key in browser or App Builder code. App end-user credentials do not grant access to an owner's personal library; grant selected families explicitly with grantToApp(appId, fontIds), which calls PUT /api/v1/apps/:appId/fonts with {"font_ids": [...]} (up to three family IDs; an empty array clears the grant) and returns data: { app_id, font_ids }.
Generate using fonts
Check model discovery (GET /api/v1/models) for supports_custom_fonts: true. GPT Image 2 and the GPT Image 2.5 models currently report it, while other GPT Image versions may not; capability discovery, not the model's name, determines support. If you pass an explicit model that lacks the capability, the request fails with 400 CUSTOM_FONTS_UNSUPPORTED. If you omit model, Smart Select only considers models that support custom fonts.
Add optional font_ids (an array of up to three 24-character family IDs; duplicates are ignored) to POST /api/v1/generate/image, POST /api/v1/generate/image-edit, or the image path of POST /api/v1/generate/creative-director. POST /api/v1/edit/image also accepts font_ids, but only with operation: "magic_edit"; it is then routed through image editing with image_url as the source image. The MCP tools are generate_image, generate_image_edit, and generate_creative_director (image workflows only). SDK image creation/editing use the same field. Batch prompts inherit the selected families. A deleted or inaccessible family fails with 404 FONT_NOT_FOUND; a malformed list fails with 400 INVALID_FONT_IDS.
{
"prompt": "Create a Hebrew webinar ad. Exact heading: בונים את העתיד. Use a bold heading and light supporting text.",
"font_ids": ["<ready-family-id>"]
}Language and style choices come from the prompt; no language picker is required. Explicit copy is preserved, and selecting a font alone does not request new text. For edits, specify only the typography to change and preserve unrelated text. Uploaded styles and character coverage constrain what can be rendered.
The backend prepares internal specimens and combines them with ordinary references and Visual DNA within the resolved model's reference limit; if they cannot all fit, the request fails with CUSTOM_FONT_REFERENCE_LIMIT. Do not render specimens yourself or send font files through reference_images. Fonts and specimens do not become normal media-library items. Public generation metadata contains font selections, not internal specimen attachments: a completed image or image-edit status result includes selected_fonts[] with each family's id, name and styles[] (id, name, version, weight, italic).
Uploads and preparation have no separate credit charge. Image generation uses normal billing; there is no automatic paid typography-regeneration loop.