/
docs

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.

Base URL
https://tu-dominio.com
Nota sobre HTTPS: en desarrollo local Caddy genera un certificado autofirmado. Usa --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.

Cabecera requerida
Authorization: Bearer er_5d77432280424e8cafeee6d52ef00ad1
Importante: las API keys se muestran una sola vez al crear la cuenta. Si la pierdes tendrás que contactar soporte; no existe endpoint de recuperación.

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.

HTML1 crédito
PDF3 créditos
PNG3 créditos
JPEG3 créditos

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
* Scale configurable: los límites de 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.

RutaLímite por defectoVentana
Todas las rutas autenticadas60 peticiones60 s
POST /v1/signup5 peticiones1 hora

Todas las respuestas incluyen estas cabeceras:

Cabeceras de respuesta
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.

Formato de error
{ "error": "descripción del error" }
400Cuerpo JSON inválido o campos obligatorios ausentes.
401API key ausente, inválida o revocada.
402Créditos insuficientes. El campo needed indica cuántos faltan.
404Recurso no encontrado o no pertenece a la cuenta.
409Conflicto: p. ej. email ya registrado.
403Límite del plan superado (p. ej. máximo de plantillas o webhook no disponible en tu plan).
410Cupón expirado o agotado.
413Cuerpo o data demasiado grande para tu plan (Starter: 512 KB · Pro: 5 MB · Scale: 10 MB).
422Plantilla Handlebars inválida. Los créditos no se reembolsan (error del usuario).
429Rate limit superado.
500Error del servidor (p. ej. Puppeteer falla). Los créditos se reembolsan. El campo credits_refunded indica cuántos.
POST /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

CampoTipoDescripción
email string requerido Dirección de correo. Debe ser única en el sistema.

Ejemplo

Request
POST /v1/signup
content-type: application/json

{ "email": "tú@ejemplo.com" }
Response · 201 Created
{
  "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

201Cuenta creada. La respuesta incluye la API key.
400Email ausente o con formato inválido.
409Ya existe una cuenta con ese email.
429Demasiados registros desde esta IP (5 por hora).
GET /v1/templates

Devuelve todas las plantillas de la cuenta ordenadas por fecha de actualización descendente.

Ejemplo

Response · 200 OK
{
  "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"
    }
  ]
}
El campo test_data no se incluye en el listado para mantener las respuestas ligeras. Usa GET /v1/templates/:id para obtenerlo.
POST /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

CampoTipoDescripción
namestringrequerido Nombre descriptivo de la plantilla.
sourcestringrequerido Código fuente Handlebars. Puede contener HTML completo o un fragmento.
test_datastringopcional JSON de prueba para usar en el editor visual. Se devuelve en GET por id.
webhook_urlstringopcional URL a la que se notificará cada vez que esta plantilla sea renderizada. Requiere plan Pro o Scale. Debe ser http:// o https://.

Ejemplo

Request
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"
}
Response · 201 Created
{
  "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

201Plantilla creada. Devuelve el objeto completo con su id.
400name o source ausentes, o webhook_url con formato inválido.
403Límite de plantillas del plan alcanzado, o webhook_url no disponible en tu plan.
422El source contiene errores de sintaxis Handlebars.
GET /v1/templates/:id

Devuelve una plantilla por su UUID, incluyendo test_data.

Ejemplo

Response · 200 OK
{
  "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

200Plantilla encontrada.
404Plantilla no encontrada o no pertenece a la cuenta.
PUT /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

CampoTipoDescripción
namestringopcionalNuevo nombre.
sourcestringopcionalNuevo source Handlebars. Se revalida antes de guardar.
test_datastringopcionalNuevos datos de prueba en formato JSON.
webhook_urlstring | nullopcionalNueva URL de webhook. Envía null para eliminarla. Requiere plan Pro o Scale.

Códigos de respuesta

200Plantilla actualizada. Devuelve el objeto completo.
400webhook_url con formato inválido.
403webhook_url no disponible en tu plan.
404Plantilla no encontrada.
422El source actualizado contiene errores Handlebars.
DELETE /v1/templates/:id

Elimina una plantilla. La operación es irreversible.

Response · 200 OK
{ "deleted": true }

Códigos de respuesta

200Plantilla eliminada.
404Plantilla no encontrada.
POST /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

CampoTipoDescripción
template_idstringuno de los dos UUID de una plantilla guardada. Exclusivo con source.
sourcestringuno de los dos Source Handlebars inline. No se persiste. Límite según plan: 512 KB (Starter) · 5 MB (Pro) · 10 MB (Scale).
dataobjectopcional Variables que se inyectan en la plantilla.
formatstringpor defecto: "html" "html" · "pdf" · "png" · "jpeg"
pdfobjectopcional Opciones de PDF. Solo aplica cuando format = "pdf". Ver Opciones PDF.
imageobjectopcional Opciones de imagen. Solo aplica cuando format = "png" o "jpeg". Ver Opciones imagen.
waitUntilstringpor 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

Respuesta exitosa
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
Error de servidor (500) — créditos reembolsados
x-credits-refunded: 3
x-credits-remaining: 25              # saldo tras el reembolso
{ "error": "...", "credits_refunded": 3 }

Ejemplo completo

Request
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

200El cuerpo de la respuesta es el documento renderizado.
400Cuerpo inválido, format desconocido, o ni template_id ni source presentes.
402Créditos insuficientes.
404template_id no encontrado.
413Cuerpo o source demasiado grande.
422Error en la plantilla (sintaxis Handlebars, datos incorrectos). No se reembolsan créditos.
500Fallo de Puppeteer u otro error de servidor. Los créditos se reembolsan.
pdf { … } — solo cuando format = "pdf"
CampoTipoPor defectoDescripción
paperstring"A4" Tamaño del papel. Valores: A4 A3 A5 A6 Letter Legal Tabloid.
landscapebooleanfalse Orientación apaisada.
marginobjectsin margen Márgenes CSS. Campos: top right bottom left. Valores: "1cm", "10mm", "0.5in", "20px".
scalenumber1 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.
headerTemplatestring HTML para la cabecera de página. Si se proporciona, activa automáticamente la cabecera. Clases especiales: .pageNumber .totalPages .title .date.
footerTemplatestring HTML para el pie de página. Mismas clases especiales que headerTemplate.
Cabecera/pie: el HTML se renderiza en una capa separada; los estilos externos de la plantilla no se heredan. Usa CSS inline y unidades absolutas (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
footerTemplate
"<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"
CampoTipoPor defectoDescripción
widthnumber1200 Ancho del viewport en píxeles. Rango 100–3840.
heightnumber630 Alto del viewport en píxeles. Rango 100–3840. Con fullPage: true el documento puede crecer verticalmente más allá de este valor.
deviceScaleFactornumber1 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.
omitBackgroundbooleanfalse Solo PNG. Fondo transparente. Requiere que la plantilla no tenga background-color propio. Ignorado en JPEG (JPEG no soporta transparencia).
qualitynumber90 Solo JPEG. Calidad de compresión. Rango 0–100. Un valor de 60–75 ofrece buen equilibrio tamaño/calidad para contenido fotográfico.
fullPagebooleantrue Captura el documento completo independientemente del alto del viewport. Con false solo captura el área visible.
Ejemplo: Open Graph image (1200×630 @2x)
Request
{
  "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
Request
{
  "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 }
}
POST /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.

Configura la URL del webhook en el dashboard de Stripe apuntando a https://tu-dominio.com/v1/webhooks/stripe y copia el Webhook Secret (whsec_…) a la variable de entorno STRIPE_WEBHOOK_SECRET.

Evento soportado

EventoAcció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

.env
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

200Evento procesado (o ya procesado anteriormente — idempotente).
400Firma inválida o cuerpo malformado.
POST /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

CampoTipoDescripción
code string requerido Código del cupón. No distingue mayúsculas/minúsculas.

Ejemplo

Request
POST /v1/coupons/redeem
authorization: Bearer er_••••••••••••••••
content-type: application/json

{ "code": "PROMO25" }
Response · 200 OK
{
  "success":         true,
  "credits_granted": 25
}

Códigos de respuesta

200Cupón canjeado. Los créditos se añaden inmediatamente al saldo.
400code ausente.
404Código inválido o no existe.
409Ya has canjeado este código anteriormente.
410Código expirado o agotado (usos máximos alcanzados).
GET /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.

Response · 200 OK
{
  "data": [
    {
      "id":         "f3a1c2d4-…",
      "label":     "producción",   // null si sin etiqueta
      "revoked":   0,              // 0 = activa · 1 = revocada
      "created_at": "2026-05-19 10:00:00"
    }
  ]
}
POST /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

CampoTipoDescripción
label string opcional Etiqueta descriptiva para identificar el uso de la key (p. ej. "producción", "staging").

Ejemplo

Request
POST /v1/api-keys
authorization: Bearer er_••••••••••••••••
content-type: application/json

{ "label": "staging" }
Response · 201 Created
{
  "id":      "f3a1c2d4-…",
  "label":   "staging",
  "api_key": "er_9f3c1a…"   // ⚠ solo aquí
}

Códigos de respuesta

201Key creada. Incluye el valor de la key en claro.
403Tu plan no permite más keys activas. Actualiza a Scale.
DELETE /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.

Response · 200 OK
{ "revoked": true }

Códigos de respuesta

200Key revocada.
400No puedes revocar tu única API key activa.
404Key no encontrada o ya estaba revocada.