HANDWRITTNER · PUBLIC API V1

Public API documentation

Templates, handwriting settings and API requests: from your first key to a finished document.

Sign in and create an API key ↗
Bearer API keyJSON → PDF / PNG / SVGOpenAPI ↗
BASE URLhttps://handwrittner.com/api/v1

Getting started

  1. Sign in to API Dashboard and create a key.
  2. Open Templates and click Create template.
  3. Copy the templateId from the save dialog or Templates.
  4. Send text, wait for completed, then download the PDF with the same API key.

A key in the format hw_live_… is shown in full only once. Send it in the header Authorization: Bearer …. Keep the key on your server or in an environment variable.

Templates

The API template builder saves structure, handwriting, text size and color, spacing, margins, page size and background. Choose Simple text for one text or Structured template for dynamic fields and columns.

Simple text accepts templateId + text. Structured template accepts templateId + data; the template defines structure and styling. Fonts come from the system or your library. The Public API returns font metadata only, with no font files or download links.

GET /templatesYour templates: ID, name, mode and dates
GET /templates/{id}Builder model with styling and blocks; settings and fontIds for legacy templates
GET /fontsShared and personal fonts; premium requires an eligible subscription

Each job stores a settings snapshot. Template changes affect new jobs.

Simple Text

templateId + text

For plain text and paragraphs. No block structure is needed.

Structured Template

templateId + data

For headings, lists, images, tables and columns. Block order is stored in the template.

Handwrittner interface
Choose a mode when creating a template. It determines the fields in your API request.

API template builder

Add a block, select Static or Dynamic and assign a unique API key. For Dynamic, your app sends the value under that name in the data object.

Handwrittner interface
Add blocks on the left, arrange them in the center and configure the selected element on the right.
ElementValue in data
Text and heading"body": "Текст..."
List"topics": ["Пункт 1", "Пункт 2"]
Image"photo": { "assetId": "asset_…" }
Table"results": [{ "x": "1", "value": "2" }]

Columns control block placement. An image and text can sit side by side; each dynamic field is sent separately in data. Dividers, spacing and page breaks are defined in the template.

{
  "templateId": "tpl_…",
  "data": {
    "title": "Limits of functions",
    "body": "Today we studied limits…",
    "topics": [
      "Limit",
      "Continuity"
    ],
    "photo": {
      "assetId": "asset_…"
    },
    "results": [
      {
        "x": "1",
        "value": "2"
      },
      {
        "x": "2",
        "value": "4"
      }
    ]
  }
}

Check new values in Test Data, then update Preview using the button. History lets you undo and redo changes to the structure.

Text and Heading accept a string, List an array of strings, Image an assetId object, and Table an array of objects with template column keys. Optional fields may be omitted; invalid values return INVALID_TEMPLATE_DATA with the field name. Version 2 templates reject content and overrides. Legacy Document v1 templates retain their original contract, without automatic migration.

Text and page settings

In Builder, click Configure template or Handwriting and modifiers in /main. The selected font carries over to the full editor.

Handwrittner interface
/main saves the template formatting. The editor text is a sample used to adjust the settings.
What the template stores

Handwriting, text size and color, spacing, margins, first-line indentation, grid, paper and modifiers. Text in /main is a sample; each document receives its content through the API.

Use Create custom page size below the Page size field. Enter a name and dimensions in mm or px. The maximum size is A3 (297 × 420 mm), including landscape orientation.

Text modifiers

The same effect editor as on the regular site is used: words, letters, lines, paper and background. Choose a preset or adjust individual settings. Access to some features depends on your account permissions.

Handwrittner interface
Enable effects, choose presets and fine-tune parameters before saving the template.

Start with small variations in letter angle, size and position. Check Preview, then save the template: these settings apply to new API documents.

Your own font

Choose the default handwriting, a ready font from your library or create one in FontCreator. Creating and editing your own font requires an eligible subscription.

After your subscription ends

Your saved and built font stays available for the API. Editing it again requires a subscription. API sheets are paid for separately.

GET /fonts — Metadata for available fonts. Source font files are not returned by the API.

Simple text · PDF generation

POST /generations
{
  "templateId": "tpl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "text": "Hello from Handwrittner API"
}

Send text for Simple Text or data for Structured Template. Use only one content field per request. For v2 templates, change formatting in Builder, not through overrides.

POST /generations202 Accepted: ID, queued status and dates
GET /generations/{id}Status, pages, creditsSpent, downloadUrl after completion
GET /generations/{id}/downloadPDF; requires the owner's Bearer API key
POST /generations/{id}/cancelCancel queued/processing without charge

Statuses: queued → processing → completed, or failed / cancelled. Check status about every 10 seconds. Results are stored for 7 days; downloads then return 410.

A normal POST creates a new job. After a network error, do not retry blindly: check recent jobs in the dashboard.

Working examples

Enter your API key and templateId. Run the three steps in order to save handwrittner.pdf.

Handwrittner interface
After saving the template, Builder inserts its ID and data into ready-to-use code examples.

PDF generation is asynchronous. The first request creates a job and returns its ID. Then check the job status and, after completed, download the PDF using downloadUrl.

POST generation → queued / processing → GET status → completed → GET download → PDF

Bash requires jq to read JSON. Run all three steps in order in the same terminal.

Step 1. Create a generation

POST /api/v1/generations
# Requires jq.
command -v jq >/dev/null || { printf '%s\n' 'Install jq first.' >&2; exit 1; }
export HW_API_KEY='your_api_key'

JOB=$(curl --fail-with-body 'https://handwrittner.com/api/v1/generations' \
  -H "Authorization: Bearer $HW_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @- <<'HW_TEMPLATE_JSON'
{
  "templateId": "tpl_REPLACE_WITH_YOUR_TEMPLATE_ID",
  "text": "Hello from Handwrittner API"
}
HW_TEMPLATE_JSON
) || exit 1
BASE='https://handwrittner.com/api/v1'
printf '%s\n' "$JOB"
GENERATION_ID=$(printf '%s' "$JOB" | jq -er '.id') || exit 1

The server returns a generation ID and queued status: the job is in the queue. The POST response does not contain a PDF.

{
  "id": "gen_...",
  "status": "queued"
}

Step 2. Check generation status

GET /api/v1/generations/{generationId}
while true; do
  STATUS=$(printf '%s' "$JOB" | jq -er '.status') || exit 1
  case "$STATUS" in
    completed) break ;;
    queued|processing) sleep 10 ;;
    *) printf '%s\n' "$JOB" >&2; exit 1 ;;
  esac
  JOB=$(curl --fail-with-body \
    "$BASE/generations/$GENERATION_ID" \
    -H "Authorization: Bearer $HW_API_KEY") || exit 1
  printf '%s\n' "$JOB"
done

queued → processing → completed. Check the status every 10 seconds. If it is failed, display error and do not download the PDF. Example completed response:

{
  "id": "gen_...",
  "status": "completed",
  "pages": 2,
  "creditsSpent": 2,
  "downloadUrl": "/api/v1/generations/gen_.../download"
}

Step 3. Download the PDF

GET downloadUrl
[ "$(printf '%s' "$JOB" | jq -r '.status')" = completed ] || exit 1
DOWNLOAD_URL=$(printf '%s' "$JOB" | jq -er '.downloadUrl') || exit 1
case "$DOWNLOAD_URL" in
  https://*|http://*) ;;
  /*) AUTHORITY="${BASE#*://}"; DOWNLOAD_URL="${BASE%%://*}://${AUTHORITY%%/*}$DOWNLOAD_URL" ;;
  *) printf '%s\n' 'Invalid downloadUrl' >&2; exit 1 ;;
esac
curl --fail-with-body "$DOWNLOAD_URL" \
  -H "Authorization: Bearer $HW_API_KEY" \
  -o 'handwrittner.pdf' || exit 1

Download the PDF only after completed. Use downloadUrl from the response; relative paths are resolved against the API URL.

Render text

Render API is suitable for captions, messages, Telegram bots and text in apps and websites. POST /renders synchronously returns a ready SVG or PNG with HTTP 200. No polling is needed.

PDF generation via POST /generations remains the way to create complete printable documents, including multiple pages.

The style comes from templateId. The only allowed override is fontSize (6–96 pt). SVG contains letter outlines; PNG is rendered on the server. Original TTF/OTF/WOFF/WOFF2 files, font URLs, base64 fonts and source glyphs are not returned. HTML and WebFont are unsupported.

SVG

export HW_API_KEY='your_api_key'

curl --fail-with-body 'https://handwrittner.com/api/v1/renders' \
  -H "Authorization: Bearer $HW_API_KEY" \
  -H 'Content-Type: application/json' \
  --output 'handwriting.svg' \
  --data-binary @- <<'HW_TEMPLATE_JSON'
{
  "templateId": "tpl_REPLACE_WITH_YOUR_TEMPLATE_ID",
  "text": "Hello from Handwrittner",
  "format": "svg"
}
HW_TEMPLATE_JSON

PNG

export HW_API_KEY='your_api_key'

curl --fail-with-body 'https://handwrittner.com/api/v1/renders' \
  -H "Authorization: Bearer $HW_API_KEY" \
  -H 'Content-Type: application/json' \
  --output 'handwriting.png' \
  --data-binary @- <<'HW_TEMPLATE_JSON'
{
  "templateId": "tpl_REPLACE_WITH_YOUR_TEMPLATE_ID",
  "text": "Hello from Handwrittner",
  "format": "png"
}
HW_TEMPLATE_JSON

Content-Type: image/svg+xml; charset=utf-8 or image/png. The response includes Content-Length and one X-Request-ID.

Errors: 400 INVALID_REQUEST (including size and line limits), 401 INVALID_API_KEY / API_KEY_REVOKED, 403 RENDER_ACCESS_DENIED / FONT_ACCESS_DENIED, 404 TEMPLATE_NOT_FOUND / FONT_NOT_FOUND, 429 RATE_LIMIT_EXCEEDED, 500 RENDER_FAILED, 503 RENDER_DISABLED. Error bodies retain code, message and requestId.

Default limits: 3,000 UTF-16 characters, 20 lines including wrapping, one page, up to 2048 px per side and 4 MP at 144 DPI; output up to 8 MB, processing up to 15 seconds. Overflow is rejected without cropping. Extra text and graphic template overlays are unsupported.

Per account: one active heavy render, 5 accepted render requests per minute and 100 per UTC day, regardless of key count. Failures after admission also consume quota. The general key limit is 10 requests per minute. Server limits may differ.

Successful results are counted separately in renders, svgRenders and pngRenders in the response to GET /usage. In credits mode, output consumes API sheets by page size (A5 — 1, A4 — 1.5, A3 — 2); premium font and feature access is still checked. If the mode is disabled on the server, the response is 503 RENDER_DISABLED.

Preview

POST /previews accepts templateId and either text or data depending on the template mode. After completed, retrieve images using GET /generations/{id}/preview. Rendering stops after the first four pages; images contain a watermark. PDF is unavailable for previews.

The page/pages parameters and ranges are unsupported. Repeating an unchanged preview returns the existing job until the result expires. Up to 3 new previews per minute per account.

Usage and balance

A51 sheet from your balance per page
A41,5 sheets from your balance per page
A32 sheets from your balance per page

GET /usage?from=2026-09-01&to=2026-10-01. Dates are UTC, to is exclusive; maximum 366 days. Defaults to the current month through today.

Response: requests, generations, successfulGenerations, failedGenerations, cancelledGenerations, pages, creditsSpent, period and balance. API sheets per page: A5 — 1, A4 — 1.5, A3 — 2. Custom sizes use the smallest fitting standard, up to A3. API balance is separate; website subscriptions do not replace API sheet charges. The dashboard offers API Week, Basic, Pro and Premium, plus non-expiring packs of 50–1,000 sheets. Plan sheets are used first, then extra balance. Bought sheets remain when connecting or changing plans. Daily allowance resets at 00:00 UTC; plans last 7 or 30 days. balance.subscription contains plan limits and usage; balance.usage contains pagesToday and pagesThisPeriod. Test top-ups charge no money and test sheets do not transfer to live balance.

The server checks plan allowance and balance before work and at completion. PDF, final status and charge commit atomically. If there are not enough sheets for the entire document or an internal error occurs, no PDF is issued and nothing is charged. Previews are excluded from paid generations; requests with valid keys, including failed ones, are counted.

Limits

  • 10 requests per minute per key, including polling and downloads.
  • 2 unfinished jobs per account; up to 32 queued jobs per backend instance.
  • Heavy rendering shares the website PDF pool: by default, 2 concurrent tasks, at most one per user.
  • Text up to 100,000 characters; accounts without a subscription retain a 4,000-character limit.
  • Up to 200 pages; request up to 512 KiB; PDF up to 55 MB; rendering timeout 60 seconds by default.
  • Up to 100 templates and 10 active API keys.

429 responses include Retry-After: 60. Wait that long before retrying.

Errors and requestId

{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Your API plan allowance and extra balance are insufficient for this document. Buy sheets or choose a plan.",
    "requestId": "req_…"
  }
}

Every API request has an X-Request-ID header. Include it when contacting support.

HTTPCodes
400 / 413INVALID_REQUEST
401INVALID_API_KEY, API_KEY_REVOKED
402INSUFFICIENT_BALANCE
404TEMPLATE_NOT_FOUND, GENERATION_NOT_FOUND, ENDPOINT_NOT_FOUND
409 / 410GENERATION_NOT_READY, PREVIEW_ONLY, GENERATION_EXPIRED
429RATE_LIMIT_EXCEEDED
500INTERNAL_ERROR

GENERATION_FAILED and INSUFFICIENT_BALANCE may appear in a failed job's error field. Resources owned by others return 404, as if missing.

Document v1 compatibility

This section covers older Document v1 templates. For new structured Builder templates, use data as shown above.

Send content instead of text. Handwrittner Document Model v1 supports paragraph, heading (1–3), bulletList, orderedList, image, table, columns, pageBreak, divider and spacer. HTML and Tiptap JSON are not accepted.

{
  "templateId": "tpl_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "content": [
    {
      "type": "heading",
      "level": 1,
      "text": "Limits of functions"
    },
    {
      "type": "paragraph",
      "text": "Today we studied the main properties of limits."
    },
    {
      "type": "bulletList",
      "items": [
        "Limit of a function",
        "Continuity",
        "Asymptotes"
      ]
    },
    {
      "type": "columns",
      "columns": [
        {
          "width": 55,
          "content": [
            {
              "type": "paragraph",
              "text": "The explanation is on the left."
            },
            {
              "type": "table",
              "rows": [
                [
                  "x",
                  "f(x)"
                ],
                [
                  "1",
                  "2"
                ],
                [
                  "2",
                  "4"
                ]
              ]
            }
          ]
        },
        {
          "width": 45,
          "content": [
            {
              "type": "image",
              "assetId": "asset_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
              "width": "100%",
              "align": "center"
            },
            {
              "type": "paragraph",
              "text": "Figure 1."
            }
          ]
        }
      ]
    },
    {
      "type": "divider"
    },
    {
      "type": "spacer",
      "height": 12
    },
    {
      "type": "pageBreak"
    },
    {
      "type": "paragraph",
      "text": "Continued on a new page."
    }
  ]
}

First upload an image with a separate POST /assets request: raw PNG or JPEG, Content-Type image/png or image/jpeg. Example: curl -H 'Authorization: Bearer …' -H 'Content-Type: image/png' --data-binary @image.png …/api/v1/assets. The response includes id, type, width and height. Use the returned assetId.

Images are private. URLs, SVG and base64 are not accepted in content. Up to 5 MiB, 4096 px per side and 8 MP. Per account: 200 images / 100 MiB. WebP is not supported yet.

Columns: 2–6, widths totaling 100%; equal widths if omitted. Set columnGap in the template (12 pt by default). Tables wrap within columns. Nested columns and pageBreak inside a column are unsupported. Spacer: 1–500 pt.

Default limits: 200 blocks total, 10,000 characters per text block, 100 list items, 200 × 20 table cells, 4,000 cells total, 20 images. Overall text limits and billing by actual pages still apply. The server may set lower limits.

Need help?

For any questions, contact me on Telegram (@lsamf). Every API request has an X-Request-ID header. Include it when contacting support.