Primeros pasos
- Entra en Panel de API y crea una clave.
- Abre Plantillas y pulsa «Crear plantilla».
- Copia el templateId del diálogo de guardado o de Plantillas.
- Envía el texto, espera a completed y descarga el PDF con la misma clave API.
La clave con formato hw_live_… se muestra completa una sola vez. Envíala en el encabezado Authorization: Bearer …. Guarda la clave en tu servidor o en una variable de entorno.
Plantillas
El editor de plantillas API guarda la estructura, escritura, tamaño y color del texto, espaciado, márgenes, tamaño de página y fondo. Elige Texto simple para un texto o Plantilla estructurada para campos dinámicos y columnas.
Texto simple acepta templateId + text. Plantilla estructurada acepta templateId + data; la plantilla define la estructura y el estilo. Las fuentes se eligen del sistema o tu biblioteca. La API pública devuelve solo metadatos, sin archivos ni enlaces de descarga.
GET /templates | Tus plantillas: ID, nombre, modo y fechas |
GET /templates/{id} | Modelo del editor con estilo y bloques; settings y fontIds para plantillas antiguas |
GET /fonts | Fuentes compartidas y propias; premium requiere una suscripción compatible |
Cada tarea guarda una copia de los ajustes. Los cambios en la plantilla afectan a tareas nuevas.
Simple Text
templateId + textPara texto y párrafos. No es necesario crear una estructura de bloques.
Structured Template
templateId + dataPara títulos, listas, imágenes, tablas y columnas. El orden de los bloques se guarda en la plantilla.
Editor de plantillas API
Añade un bloque, elige Static o Dynamic y asigna un API key único. Para Dynamic, la aplicación envía el valor con ese nombre en el objeto data.
| Elemento | Valor en data |
|---|---|
| Texto y título | "body": "Текст..." |
| Lista | "topics": ["Пункт 1", "Пункт 2"] |
| Imagen | "photo": { "assetId": "asset_…" } |
| Tabla | "results": [{ "x": "1", "value": "2" }] |
Las columnas controlan la posición de los bloques. Una imagen y su texto pueden colocarse juntos; cada campo dinámico se envía por separado en data. Los separadores, espacios y saltos de página se definen en la plantilla.
{
"templateId": "tpl_…",
"data": {
"title": "Límites de funciones",
"body": "Hoy estudiamos los límites…",
"topics": [
"Límite",
"Continuidad"
],
"photo": {
"assetId": "asset_…"
},
"results": [
{
"x": "1",
"value": "2"
},
{
"x": "2",
"value": "4"
}
]
}
}Comprueba los nuevos valores en Test Data y actualiza Preview con el botón. El historial permite deshacer y rehacer cambios de estructura.
Text y Heading aceptan una cadena; List, una lista de cadenas; Image, un objeto assetId; Table, una lista de objetos con las claves de columna. Los campos opcionales pueden omitirse; los valores no válidos devuelven INVALID_TEMPLATE_DATA con el nombre field. Las plantillas v2 no aceptan content ni overrides. Document v1 mantiene su contrato original sin migración automática.
Ajustes de texto y página
En Builder, pulsa Configurar plantilla o Escritura y modificadores en /main. La fuente seleccionada se mantiene al abrir el editor completo.
Escritura, tamaño y color del texto, espaciado, márgenes, sangría, cuadrícula, papel y modificadores. El texto de /main es una muestra; cada documento recibe sus datos mediante la API.
Usa Crear formato personalizado debajo de Formato. Introduce un nombre y las dimensiones en mm o px. El máximo es A3 (297 × 420 mm), también en orientación horizontal.
Modificadores de texto
Se usa el mismo editor de efectos que en el sitio: palabras, letras, líneas, papel y fondo. Elige un ajuste predefinido o modifica parámetros individuales. Algunas funciones dependen de los permisos de tu cuenta.
Empieza con pequeñas variaciones de inclinación, tamaño y posición de las letras. Revisa Preview y guarda la plantilla: los ajustes se aplicarán a nuevos documentos API.
Tu propia fuente
Elige la escritura predeterminada, una fuente de tu biblioteca o crea una en FontCreator. Crear y editar tu fuente requiere una suscripción adecuada.
Tu fuente guardada y compilada sigue disponible para la API. Para editarla de nuevo necesitas una suscripción. Las hojas API se pagan por separado.
GET /fonts — Metadatos de las fuentes disponibles. La API no devuelve archivos fuente de las tipografías.
Texto simple · generación de PDF
POST /generations
{
"templateId": "tpl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"text": "Hello from Handwrittner API"
}Envía text para Simple Text o data para Structured Template. Usa un solo campo de contenido por solicitud. En plantillas v2, el formato se cambia en Builder, sin overrides.
POST /generations | 202 Accepted: ID, estado queued y fechas |
GET /generations/{id} | Estado, pages, creditsSpent, downloadUrl al finalizar |
GET /generations/{id}/download | PDF; requiere la clave API Bearer del propietario |
POST /generations/{id}/cancel | Cancelar queued/processing sin coste |
Estados: queued → processing → completed, o failed / cancelled. Consulta el estado aproximadamente cada 10 segundos. Los resultados se guardan 7 días; después, download devuelve 410.
Un POST normal crea una tarea nueva. Ante un fallo de red, no lo repitas sin comprobar las tareas recientes en el panel.
Ejemplos de uso
Introduce tu clave API y templateId. Ejecuta los tres pasos en orden para guardar handwrittner.pdf.
La generación de PDF es asíncrona. La primera solicitud crea un trabajo y devuelve su ID. Después consulta su estado y, cuando sea completed, descarga el PDF mediante downloadUrl.
Bash necesita jq para leer JSON. Ejecuta los tres pasos en orden en la misma terminal.
Paso 1. Crear una generación
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 1El servidor devuelve un ID de generación y el estado queued: el trabajo está en cola. La respuesta POST no contiene un PDF.
{
"id": "gen_...",
"status": "queued"
}Paso 2. Consultar el estado
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"
donequeued → processing → completed. Consulta el estado cada 10 segundos. Si es failed, muestra error y no descargues el PDF. Ejemplo de respuesta completed:
{
"id": "gen_...",
"status": "completed",
"pages": 2,
"creditsSpent": 2,
"downloadUrl": "/api/v1/generations/gen_.../download"
}Paso 3. Descargar el 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 1Descarga el PDF solo después de completed. Usa downloadUrl de la respuesta; las rutas relativas se resuelven con respecto a la URL de la API.
Renderizar texto
API de renderizado sirve para subtítulos, mensajes, bots de Telegram y texto en aplicaciones y sitios web. POST /renders devuelve un SVG o PNG listo de forma síncrona con HTTP 200. No requiere consultas de estado.
Generación de PDF mediante POST /generations sigue siendo la forma de crear documentos completos para imprimir, incluidos los de varias páginas.
El estilo procede de templateId. Solo se permite sobrescribir fontSize (6–96 pt). SVG contiene contornos de letras y PNG se genera en el servidor. No se devuelven archivos originales TTF/OTF/WOFF/WOFF2, URL de fuentes, fuentes base64 ni glifos originales. HTML y WebFont no se admiten.
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_JSONPNG
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_JSONContent-Type: image/svg+xml; charset=utf-8 o image/png. La respuesta incluye Content-Length y un X-Request-ID.
Errores: 400 INVALID_REQUEST (incluidos límites de tamaño y líneas), 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. El cuerpo mantiene code, message y requestId.
Límites predeterminados: 3000 caracteres UTF-16, 20 líneas incluidos saltos, una página, hasta 2048 px por lado y 4 MP a 144 ppp; salida de hasta 8 MB y procesamiento de hasta 15 segundos. Se rechaza el desbordamiento sin recortar. Aún no se admiten capas adicionales de texto y gráficos.
Por cuenta: un renderizado pesado activo, 5 solicitudes de renderizado aceptadas por minuto y 100 por día UTC, sin importar el número de claves. Los errores tras la admisión también consumen cuota. El límite general por clave es de 10 solicitudes por minuto. El servidor puede configurar otros límites.
Los resultados correctos se contabilizan por separado en renders, svgRenders y pngRenders de la respuesta de GET /usage. En modo credits, el resultado consume hojas API según el tamaño de página (A5 — 1, A4 — 1,5, A3 — 2); se sigue comprobando el acceso a fuentes y funciones premium. Si el modo no está habilitado en el servidor, se devuelve 503 RENDER_DISABLED.
Vista previa
POST /previews acepta templateId y text o data según el modo de la plantilla. Tras completed, obtén las imágenes mediante GET /generations/{id}/preview. El renderizado se detiene tras las primeras cuatro páginas; las imágenes incluyen una marca de agua. No hay PDF para las vistas previas.
No se admiten los parámetros page/pages ni rangos. Repetir una vista previa sin cambios devuelve la tarea existente hasta que caduque el resultado. Hasta 3 vistas nuevas por minuto y cuenta.
Uso y saldo
GET /usage?from=2026-09-01&to=2026-10-01. Fechas en UTC; to no se incluye. Máximo 366 días. Por defecto, el mes actual hasta hoy incluido.
Respuesta: requests, generations, successfulGenerations, failedGenerations, cancelledGenerations, pages, creditsSpent, period y balance. Hojas API por página: A5 — 1, A4 — 1,5, A3 — 2. Los tamaños personalizados se cobran según el menor estándar que los contenga, hasta A3. El saldo API es independiente; la suscripción del sitio no sustituye el cobro de hojas API. El panel ofrece API Week, Basic, Pro y Premium, y paquetes sin caducidad de 50 a 1000 hojas. Primero se usa el límite del plan y después el saldo adicional. Las hojas compradas se conservan al activar o cambiar de plan. El límite diario se renueva a las 00:00 UTC; los planes duran 7 o 30 días. balance.subscription contiene límites y uso del plan; balance.usage contiene pagesToday y pagesThisPeriod. Las recargas de prueba no cobran dinero y las hojas de prueba no pasan al saldo real.
El servidor comprueba el límite y saldo antes del trabajo y al finalizar. El PDF, estado final y cobro se guardan de forma atómica. Si no hay hojas para todo el documento o hay un error interno, no se entrega el PDF ni se cobra. Las vistas previas no cuentan como generaciones de pago; sí las solicitudes con clave válida, incluidas las fallidas.
Límites
- 10 solicitudes por minuto y clave, incluidas consultas de estado y descargas.
- 2 tareas pendientes por cuenta; hasta 32 tareas en cola por instancia del servidor.
- El renderizado pesado comparte el grupo PDF del sitio: por defecto, 2 tareas simultáneas y una por usuario como máximo.
- Texto de hasta 100 000 caracteres; las cuentas sin suscripción mantienen el límite de 4000 caracteres.
- Hasta 200 páginas; solicitud de hasta 512 KiB; PDF de hasta 55 MB; tiempo de renderizado de 60 segundos por defecto.
- Hasta 100 plantillas y 10 claves API activas.
Las respuestas 429 incluyen Retry-After: 60. Espera ese tiempo antes de reintentar.
Errores y requestId
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "El límite del plan y el saldo adicional no bastan para este documento. Compra hojas o elige un plan.",
"requestId": "req_…"
}
}Cada solicitud API tiene un encabezado X-Request-ID. Inclúyelo al contactar con soporte.
| HTTP | Códigos |
|---|---|
| 400 / 413 | INVALID_REQUEST |
| 401 | INVALID_API_KEY, API_KEY_REVOKED |
| 402 | INSUFFICIENT_BALANCE |
| 404 | TEMPLATE_NOT_FOUND, GENERATION_NOT_FOUND, ENDPOINT_NOT_FOUND |
| 409 / 410 | GENERATION_NOT_READY, PREVIEW_ONLY, GENERATION_EXPIRED |
| 429 | RATE_LIMIT_EXCEEDED |
| 500 | INTERNAL_ERROR |
GENERATION_FAILED e INSUFFICIENT_BALANCE pueden aparecer en el campo error de una tarea failed. Los recursos ajenos devuelven 404, como si no existieran.
Compatibilidad con Document v1
Esta sección describe las antiguas plantillas Document v1. Para nuevas plantillas estructuradas de Builder, usa data como se muestra arriba.
Envía content en lugar de text. Handwrittner Document Model v1 admite paragraph, heading (1–3), bulletList, orderedList, image, table, columns, pageBreak, divider y spacer. No se aceptan HTML ni Tiptap JSON.
{
"templateId": "tpl_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"content": [
{
"type": "heading",
"level": 1,
"text": "Límites de funciones"
},
{
"type": "paragraph",
"text": "Hoy estudiamos las propiedades principales de los límites."
},
{
"type": "bulletList",
"items": [
"Límite de una función",
"Continuidad",
"Asíntotas"
]
},
{
"type": "columns",
"columns": [
{
"width": 55,
"content": [
{
"type": "paragraph",
"text": "La explicación está a la izquierda."
},
{
"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": "Figura 1."
}
]
}
]
},
{
"type": "divider"
},
{
"type": "spacer",
"height": 12
},
{
"type": "pageBreak"
},
{
"type": "paragraph",
"text": "Continuación en una página nueva."
}
]
}Primero sube la imagen con un POST /assets independiente: PNG o JPEG sin codificar, Content-Type image/png o image/jpeg. Ejemplo: curl -H 'Authorization: Bearer …' -H 'Content-Type: image/png' --data-binary @image.png …/api/v1/assets. La respuesta incluye id, type, width y height. Usa el assetId devuelto.
Las imágenes son privadas. No se aceptan URL, SVG ni base64 en content. Hasta 5 MiB, 4096 px por lado y 8 MP. Por cuenta: 200 imágenes / 100 MiB. WebP aún no se admite.
Columnas: 2–6, anchos que sumen 100 %; iguales si se omite width. Define columnGap en la plantilla (12 pt por defecto). Las tablas se distribuyen dentro de las columnas. No se admiten columnas anidadas ni pageBreak dentro de una columna. Spacer: 1–500 pt.
Límites predeterminados: 200 bloques, 10 000 caracteres por bloque de texto, 100 elementos por lista, tablas de 200 × 20 celdas, 4000 celdas en total y 20 imágenes. Se mantienen los límites de texto y el cobro por páginas reales. El servidor puede establecer límites inferiores.