Skip to content
HeyMadeFonts

JSON API reference

Version 1 of the JSON API lives at https://heymadefonts.com/api/v1. It needs no key and sets no cookies. Responses allow any origin (Access-Control-Allow-Origin: *).

Response envelope

Every successful response wraps its payload in data and adds meta:

JSON
{
  "data": { … },
  "meta": { "requestId": "k3x9…", "version": "v1" }
}

Errors use one shape, with a stable machine-readable code, a message written for people, and whether retrying the same request may succeed:

JSON
{
  "error": {
    "code": "MIGRATE_NOTHING_FOUND",
    "message": "No font stylesheet URL was found. …",
    "retryable": false
  }
}

Validation errors add details, an object of field names and messages. Every response carries an X-Request-Id header; quote it when you report a problem.

Status codes

StatusMeaning
200Success.
201, 202A scan was created or accepted for processing.
302Redirect to a file on the font CDN (downloads).
400The request is malformed or a required field is missing (VALIDATION_FAILED, MIGRATE_INPUT_REQUIRED).
404No published font or scan with that identifier (NOT_FOUND).
413Input too large (MIGRATE_INPUT_TOO_LARGE).
422The request is well-formed but cannot be processed (MIGRATE_NOTHING_FOUND).
429Rate limit reached (RATE_LIMITED, retryable: true). Wait and retry.
500, 503Our fault (INTERNAL_ERROR, UNAVAILABLE, CATALOG_UNAVAILABLE). Retry later.

Fonts

GET /api/v1/fonts

Published families, paginated. Pagination details are in meta.pagination.

JSON, one item
{
  "id": 42,
  "slug": "lato",
  "family": "Lato",
  "category": "sans-serif",
  "designer": "Łukasz Dziedzic",
  "license": {
    "type": "OFL",
    "spdx": "OFL-1.1",
    "url": "https://heymadefonts.com/fonts/lato/license",
    "attribution": "Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic …"
  },
  "styles": [
    { "weightMin": 400, "weightMax": 400, "italic": false, "variableAxes": {}, "subsets": ["latin", "latin-ext", "…"] }
  ],
  "isVariable": false,
  "supportsScandinavian": true,
  "cssUrl": "https://fonts.heymadefonts.com/css2?family=Lato"
}

Illustrative values; the live response is the source of truth.

GET /api/v1/fonts/{slug}

One family with the same fields as a list item. Unknown or unpublished slugs return 404.

GET /api/v1/fonts/{slug}/download

Redirects (302) to an immutable ZIP on the font CDN containing the font files, the licence text and a README with attribution. The website’s download link, https://heymadefonts.com/fonts/lato/download, does the same.

GET /api/v1/catalog/version

The live catalogue version and when it was published. Use it to decide when to refresh a cached copy of the font list.

Migration

POST /api/v1/migrate

Converts pasted font-loading code (for example a fonts.googleapis.com link) into snippets for the closest HeyMadeFonts families. The text is parsed only: nothing in it is fetched, executed or stored. Send JSON or a form field named input, up to 20 KB and 20 URLs.

Shell
curl -s https://heymadefonts.com/api/v1/migrate \
  -H 'Content-Type: application/json' \
  -d '{"input": "<link href=\"https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap\" rel=\"stylesheet\">"}'

The data object contains:

status
supported (everything found), partial or unsupported (nothing usable).
summary
Counts: references, families, supported, partial, unsupported.
result
Combined url, css2Query, preconnect, link and import snippets covering every supported style, or null.
references[]
One entry per URL found: source, kind (link, import, wordpress, url), line, api, status, issues[], a drop-in replacement in the same syntax, and families[] with name, catalogName, slug, requested, status, reason, missing[] (style and reason), fontUrl and downloadUrl.
notes[]
Things the snippet does not fix by itself, such as preconnect hints to the old font host.

Errors: MIGRATE_INPUT_REQUIRED (400), MIGRATE_INPUT_TOO_LARGE (413, over 20 KB or more than 20 URLs), MIGRATE_NOTHING_FOUND (422), CATALOG_UNAVAILABLE (503).

Website scans

The scanner opens one public page and reports the font requests it observes. Ownership of a scan is proven with the X-Scan-Owner-Token header returned when the scan is created; there are no accounts or cookies.

POST /api/v1/scans
Create a scan for a public https:// or http:// URL. Subject to per-client, per-domain and global rate limits.
GET /api/v1/scans/{id}
Status and, when finished, the findings with evidence.
POST /api/v1/scans/{id}/share
Turn a time-limited public share link on or off (owner token required).
GET /api/v1/scans/{id}/report.pdf
The report as a PDF, with the same access rules as the report page.

See how the scanner works for what a scan can and cannot show.

HeyMadeIt integration

GET /api/v1/integrations/heymadeit/fonts

A feed of published families for HeyMadeIt, our sister design and print product, including each family’s licence and its print-embedding review status. It is public and read-only; GET /api/v1/integrations/heymadeit/fonts/{slug} returns one family.

Rate limits

Read endpoints allow 240 requests per minute per client, migration 30 per minute. Clients are identified by a daily-rotating hash of the IP address used only for rate limiting. Exceeding a limit returns 429 with retryable: true.