Documentación de la API
EasyRender es una API REST para renderizar plantillas Handlebars a HTML, PDF, PNG y JPEG.
Todas las respuestas son application/json salvo los endpoints de render,
que devuelven el documento directamente.
https://tu-dominio.com
--insecure en curl o acepta el certificado en el navegador.
Autenticación
Todos los endpoints excepto POST /v1/signup y POST /v1/webhooks/stripe
requieren una API key válida en la cabecera Authorization.
Authorization: Bearer er_5d77432280424e8cafeee6d52ef00ad1
Formato de la key
Las keys tienen el prefijo er_ seguido de 32 caracteres hexadecimales.
Se almacena únicamente su hash SHA-256; nunca en claro.
Créditos
Cada render consume créditos según el formato de salida. Los créditos se descuentan antes de iniciar el render para evitar trabajo no cobrado. Si el servidor falla (error 5xx), los créditos se reembolsan automáticamente.
Las cuentas nuevas reciben 25 créditos de bienvenida — suficiente para probar los cuatro formatos sin tarjeta de crédito.
Planes
El plan de tu cuenta determina los límites de uso. Todos los planes usan el mismo sistema de créditos para los renders; las diferencias son en número de plantillas, tamaño máximo de los datos y funcionalidades disponibles.
| Límite | Starter | Pro | Scale |
|---|---|---|---|
| Plantillas guardadas | 10 | 100 | ilimitadas |
| Cuerpo HTTP máximo | 512 KB | 5 MB | 10 MB* |
Campo data máximo |
512 KB | 5 MB | 10 MB* |
| API keys activas | 1 | 1 | ilimitadas |
| Webhook por plantilla | ✕ | ✓ | ✓ |
data y cuerpo HTTP pueden ajustarse
por cuenta en el plan Scale. Contacta con soporte para configurar límites personalizados.
Cuando superas el límite de plantillas de tu plan, la API devuelve 403
con un mensaje indicando el máximo y sugiriendo actualizar el plan.
Los límites de tamaño se comprueban antes de procesar la petición y devuelven 413.
Rate Limiting
El límite se aplica por API key usando una ventana deslizante en memoria. La ruta de signup tiene un límite más agresivo por IP para frenar altas masivas.
| Ruta | Límite por defecto | Ventana |
|---|---|---|
| Todas las rutas autenticadas | 60 peticiones | 60 s |
POST /v1/signup | 5 peticiones | 1 hora |
Todas las respuestas incluyen estas cabeceras:
x-ratelimit-limit: 60 # límite de la ventana x-ratelimit-remaining: 58 # peticiones restantes x-ratelimit-reset: 1716120600 # Unix timestamp (UTC) del reset
Errores
Todos los errores devuelven JSON con el campo error y el código HTTP correspondiente.
{ "error": "descripción del error" }
| 400 | Cuerpo JSON inválido o campos obligatorios ausentes. |
| 401 | API key ausente, inválida o revocada. |
| 402 | Créditos insuficientes. El campo needed indica cuántos faltan. |
| 404 | Recurso no encontrado o no pertenece a la cuenta. |
| 409 | Conflicto: p. ej. email ya registrado. |
| 403 | Límite del plan superado (p. ej. máximo de plantillas o webhook no disponible en tu plan). |
| 410 | Cupón expirado o agotado. |
| 413 | Cuerpo o data demasiado grande para tu plan (Starter: 512 KB · Pro: 5 MB · Scale: 10 MB). |
| 422 | Plantilla Handlebars inválida. Los créditos no se reembolsan (error del usuario). |
| 429 | Rate limit superado. |
| 500 | Error del servidor (p. ej. Puppeteer falla). Los créditos se reembolsan. El campo credits_refunded indica cuántos. |
/v1/signup
Crea una cuenta nueva y devuelve la API key. No requiere autenticación. La key se muestra una única vez en la respuesta; guárdala antes de cerrar.
Cuerpo de la petición
| Campo | Tipo | Descripción | |
|---|---|---|---|
| string | requerido | Dirección de correo. Debe ser única en el sistema. |
Ejemplo
POST /v1/signup content-type: application/json { "email": "tú@ejemplo.com" }
{
"account_id": "58d3914b-d535-4c3a-ac77-1acf61751e52",
"email": "tú@ejemplo.com",
"api_key": "er_5d77432280424e8cafeee6d52ef00ad1", // ⚠ solo aquí
"credits": 25,
"message": "Guarda tu api_key, no se mostrará de nuevo."
}
Códigos de respuesta
| 201 | Cuenta creada. La respuesta incluye la API key. |
| 400 | Email ausente o con formato inválido. |
| 409 | Ya existe una cuenta con ese email. |
| 429 | Demasiados registros desde esta IP (5 por hora). |
/v1/templates
Devuelve todas las plantillas de la cuenta ordenadas por fecha de actualización descendente.
Ejemplo
{
"data": [
{
"id": "010b6365-bdab-49ad-8b81-954a205ff578",
"name": "Factura",
"source": "<h1>Hola, {{nombre}}</h1>...",
"webhook_url": "https://mi-app.com/hooks/render", // null si no configurado
"updated_at": "2026-05-19 10:30:00",
"created_at": "2026-05-18 09:00:00"
}
]
}
test_data no se incluye en el listado para mantener las respuestas ligeras.
Usa GET /v1/templates/:id para obtenerlo.
/v1/templates
Crea una nueva plantilla. EasyRender compila el source con Handlebars antes de guardarlo; si tiene errores de sintaxis devuelve 422 sin guardar nada.
Cuerpo de la petición
| Campo | Tipo | Descripción | |
|---|---|---|---|
| name | string | requerido | Nombre descriptivo de la plantilla. |
| source | string | requerido | Código fuente Handlebars. Puede contener HTML completo o un fragmento. |
| test_data | string | opcional | JSON de prueba para usar en el editor visual. Se devuelve en GET por id. |
| webhook_url | string | opcional | URL a la que se notificará cada vez que esta plantilla sea renderizada. Requiere plan Pro o Scale. Debe ser http:// o https://. |
Ejemplo
POST /v1/templates authorization: Bearer er_•••••••••••••••• { "name": "Factura", "source": "<h1>Factura {{numero}}</h1><p>{{cliente}}</p>", "test_data": "{\"numero\":\"2026-001\",\"cliente\":\"Acme S.L.\"}", "webhook_url": "https://mi-app.com/hooks/render" }
{
"id": "010b6365-bdab-49ad-8b81-954a205ff578",
"name": "Factura",
"source": "<h1>Factura {{numero}}</h1><p>{{cliente}}</p>",
"test_data": "{\"numero\":\"2026-001\",\"cliente\":\"Acme S.L.\"}",
"webhook_url": "https://mi-app.com/hooks/render"
}
Códigos de respuesta
| 201 | Plantilla creada. Devuelve el objeto completo con su id. |
| 400 | name o source ausentes, o webhook_url con formato inválido. |
| 403 | Límite de plantillas del plan alcanzado, o webhook_url no disponible en tu plan. |
| 422 | El source contiene errores de sintaxis Handlebars. |
/v1/templates/:id
Devuelve una plantilla por su UUID, incluyendo test_data.
Ejemplo
{
"id": "010b6365-bdab-49ad-8b81-954a205ff578",
"name": "Factura",
"source": "<h1>Factura {{numero}}</h1>...",
"test_data": "{\"numero\":\"2026-001\",\"cliente\":\"Acme S.L.\"}",
"webhook_url": "https://mi-app.com/hooks/render",
"updated_at": "2026-05-19 10:30:00",
"created_at": "2026-05-18 09:00:00"
}
Códigos de respuesta
| 200 | Plantilla encontrada. |
| 404 | Plantilla no encontrada o no pertenece a la cuenta. |
/v1/templates/:id
Actualiza una plantilla existente. Todos los campos son opcionales; los campos no enviados conservan su valor anterior.
Cuerpo de la petición
| Campo | Tipo | Descripción | |
|---|---|---|---|
| name | string | opcional | Nuevo nombre. |
| source | string | opcional | Nuevo source Handlebars. Se revalida antes de guardar. |
| test_data | string | opcional | Nuevos datos de prueba en formato JSON. |
| webhook_url | string | null | opcional | Nueva URL de webhook. Envía null para eliminarla. Requiere plan Pro o Scale. |
Códigos de respuesta
| 200 | Plantilla actualizada. Devuelve el objeto completo. |
| 400 | webhook_url con formato inválido. |
| 403 | webhook_url no disponible en tu plan. |
| 404 | Plantilla no encontrada. |
| 422 | El source actualizado contiene errores Handlebars. |
/v1/templates/:id
Elimina una plantilla. La operación es irreversible.
{ "deleted": true }
Códigos de respuesta
| 200 | Plantilla eliminada. |
| 404 | Plantilla no encontrada. |
/v1/render
Renderiza una plantilla Handlebars con los datos proporcionados y devuelve el documento. La respuesta es el fichero directamente (HTML, PDF, PNG o JPEG), no JSON.
Parámetros base
| Campo | Tipo | Descripción | |
|---|---|---|---|
| template_id | string | uno de los dos | UUID de una plantilla guardada. Exclusivo con source. |
| source | string | uno de los dos | Source Handlebars inline. No se persiste. Límite según plan: 512 KB (Starter) · 5 MB (Pro) · 10 MB (Scale). |
| data | object | opcional | Variables que se inyectan en la plantilla. |
| format | string | por defecto: "html" | "html" · "pdf" · "png" · "jpeg" |
| object | opcional | Opciones de PDF. Solo aplica cuando format = "pdf". Ver Opciones PDF. |
|
| image | object | opcional | Opciones de imagen. Solo aplica cuando format = "png" o "jpeg". Ver Opciones imagen. |
| waitUntil | string | por defecto: "networkidle0" | "networkidle0" espera a que no haya actividad de red (más lento, más fiel). "load" o "domcontentloaded" son ~3× más rápidos si no hay recursos externos. |
Cabeceras de respuesta
content-type: application/pdf # o text/html, image/png, image/jpeg x-credits-charged: 3 # créditos descontados en esta llamada x-credits-remaining: 22 # saldo tras el descuento x-ratelimit-remaining: 57
x-credits-refunded: 3 x-credits-remaining: 25 # saldo tras el reembolso { "error": "...", "credits_refunded": 3 }
Ejemplo completo
POST /v1/render authorization: Bearer er_•••••••••••••••• content-type: application/json { "template_id": "010b6365-bdab-49ad-8b81-954a205ff578", "data": { "cliente": "Acme S.L.", "total": "1.240,00 €" }, "format": "pdf", "pdf": { "paper": "A4", "landscape": false, "margin": { "top": "2cm", "right": "1.5cm", "bottom": "2cm", "left": "1.5cm" }, "footerTemplate": "<div style='font-size:9px;text-align:right;width:100%;padding-right:1cm'><span class='pageNumber'></span>/<span class='totalPages'></span></div>" }, "waitUntil": "load" } # ← 200 OK · Content-Type: application/pdf · x-credits-charged: 3
Códigos de respuesta
| 200 | El cuerpo de la respuesta es el documento renderizado. |
| 400 | Cuerpo inválido, format desconocido, o ni template_id ni source presentes. |
| 402 | Créditos insuficientes. |
| 404 | template_id no encontrado. |
| 413 | Cuerpo o source demasiado grande. |
| 422 | Error en la plantilla (sintaxis Handlebars, datos incorrectos). No se reembolsan créditos. |
| 500 | Fallo de Puppeteer u otro error de servidor. Los créditos se reembolsan. |
pdf { … }
— solo cuando format = "pdf"
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
| paper | string | "A4" | Tamaño del papel. Valores: A4 A3 A5 A6 Letter Legal Tabloid. |
| landscape | boolean | false | Orientación apaisada. |
| margin | object | sin margen | Márgenes CSS. Campos: top right bottom left. Valores: "1cm", "10mm", "0.5in", "20px". |
| scale | number | 1 | Factor de zoom del contenido renderizado. Rango 0.1–2 (se clampa automáticamente). Útil para ajustar páginas web al tamaño del papel. |
| headerTemplate | string | — | HTML para la cabecera de página. Si se proporciona, activa automáticamente la cabecera. Clases especiales: .pageNumber .totalPages .title .date. |
| footerTemplate | string | — | HTML para el pie de página. Mismas clases especiales que headerTemplate. |
pt, mm).
El tamaño de fuente debe ser explícito (p. ej. font-size:9px).
Ejemplo de footer con número de página
"<div style='font-size:9px;font-family:sans-serif;color:#666; width:100%;text-align:right;padding-right:1cm'> Pág. <span class='pageNumber'></span> de <span class='totalPages'></span> </div>"
image { … }
— solo cuando format = "png" o "jpeg"
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
| width | number | 1200 | Ancho del viewport en píxeles. Rango 100–3840. |
| height | number | 630 | Alto del viewport en píxeles. Rango 100–3840. Con fullPage: true el documento puede crecer verticalmente más allá de este valor. |
| deviceScaleFactor | number | 1 | Factor de densidad de píxeles. Rango 1–3. Con valor 2 se genera una imagen HiDPI (el doble de resolución). Útil para pantallas retina y redes sociales. |
| omitBackground | boolean | false | Solo PNG. Fondo transparente. Requiere que la plantilla no tenga background-color propio. Ignorado en JPEG (JPEG no soporta transparencia). |
| quality | number | 90 | Solo JPEG. Calidad de compresión. Rango 0–100. Un valor de 60–75 ofrece buen equilibrio tamaño/calidad para contenido fotográfico. |
| fullPage | boolean | true | Captura el documento completo independientemente del alto del viewport. Con false solo captura el área visible. |
Ejemplo: Open Graph image (1200×630 @2x)
{
"template_id": "…",
"data": { "titulo": "Mi artículo" },
"format": "png",
"image": {
"width": 1200,
"height": 630,
"deviceScaleFactor": 2, // → imagen de 2400×1260 px
"fullPage": false
},
"waitUntil": "load"
}
Ejemplo: logo con fondo transparente
{
"source": "<div style='padding:20px'><h1 style='color:#f0a830'>{{marca}}</h1></div>",
"data": { "marca": "EasyRender" },
"format": "png",
"image": { "width": 400, "height": 100, "omitBackground": true, "fullPage": false }
}
/v1/webhooks/stripe
Endpoint para recibir eventos de Stripe. No requiere API key; la autenticidad
se verifica mediante la firma del webhook (Stripe-Signature).
Es idempotente: un mismo evento procesado dos veces no carga créditos dobles.
https://tu-dominio.com/v1/webhooks/stripe y copia el
Webhook Secret (whsec_…) a la variable de entorno
STRIPE_WEBHOOK_SECRET.
Evento soportado
| Evento | Acción |
|---|---|
| checkout.session.completed | Suma créditos a la cuenta asociada al price_id del producto comprado, según el mapa configurado en STRIPE_CREDIT_PACKS. |
Variables de entorno requeridas
STRIPE_SECRET_KEY=sk_live_… STRIPE_WEBHOOK_SECRET=whsec_… STRIPE_CREDIT_PACKS={"price_1Abc":1000,"price_1Def":5000} # price_id → créditos que otorga
Códigos de respuesta
| 200 | Evento procesado (o ya procesado anteriormente — idempotente). |
| 400 | Firma inválida o cuerpo malformado. |
/v1/coupons/redeem
Canjea un código promocional para añadir créditos a la cuenta. Cada código solo puede canjearse una vez por cuenta. Los códigos pueden tener límite de usos totales y fecha de caducidad.
Cuerpo de la petición
| Campo | Tipo | Descripción | |
|---|---|---|---|
| code | string | requerido | Código del cupón. No distingue mayúsculas/minúsculas. |
Ejemplo
POST /v1/coupons/redeem authorization: Bearer er_•••••••••••••••• content-type: application/json { "code": "PROMO25" }
{
"success": true,
"credits_granted": 25
}
Códigos de respuesta
| 200 | Cupón canjeado. Los créditos se añaden inmediatamente al saldo. |
| 400 | code ausente. |
| 404 | Código inválido o no existe. |
| 409 | Ya has canjeado este código anteriormente. |
| 410 | Código expirado o agotado (usos máximos alcanzados). |
/v1/api-keys
Devuelve todas las API keys de la cuenta (activas y revocadas). El valor de la key en sí nunca se devuelve; solo los metadatos.
{
"data": [
{
"id": "f3a1c2d4-…",
"label": "producción", // null si sin etiqueta
"revoked": 0, // 0 = activa · 1 = revocada
"created_at": "2026-05-19 10:00:00"
}
]
}
/v1/api-keys
Crea una nueva API key. El valor de la key se devuelve una sola vez; guárdala antes de cerrar. El plan Starter y Pro solo permiten 1 key activa; el plan Scale permite múltiples.
Cuerpo de la petición
| Campo | Tipo | Descripción | |
|---|---|---|---|
| label | string | opcional | Etiqueta descriptiva para identificar el uso de la key (p. ej. "producción", "staging"). |
Ejemplo
POST /v1/api-keys authorization: Bearer er_•••••••••••••••• content-type: application/json { "label": "staging" }
{
"id": "f3a1c2d4-…",
"label": "staging",
"api_key": "er_9f3c1a…" // ⚠ solo aquí
}
Códigos de respuesta
| 201 | Key creada. Incluye el valor de la key en claro. |
| 403 | Tu plan no permite más keys activas. Actualiza a Scale. |
/v1/api-keys/:id
Revoca una API key por su id. Las peticiones posteriores que usen esa key
recibirán 401. No se puede revocar la última key activa de la cuenta.
{ "revoked": true }
Códigos de respuesta
| 200 | Key revocada. |
| 400 | No puedes revocar tu única API key activa. |
| 404 | Key no encontrada o ya estaba revocada. |