Documentación técnica
TextOnFlow es una API de renderizado de imágenes personalizadas, diseñada para agencias ManyChat que gestionan campañas de marketing por WhatsApp Business. Cada solicitud produce una imagen PNG con texto dinámico, overlays y efectos visuales en milisegundos.
⚡ Inicio rápido
Una sola llamada POST a /generate-multi recibe una imagen base + texto dinámico y devuelve la URL de la imagen renderizada lista para enviar por WhatsApp.
# Ejemplo mínimo — cURL
curl -X POST https://www.textonflow.com/generate-multi \
-H "Content-Type: application/json" \
-d '{
"template_name": "mi-plantilla.jpg",
"texts": [
{
"text": "Hola, {nombre}",
"x": 100, "y": 80,
"font_size": 72,
"font_color": "#FFFFFF",
"font_name": "DejaVuSans-Bold"
}
],
"vars": { "nombre": "Ana García" }
}'
{"url": "https://www.textonflow.com/image/abc123.png"}. Esa URL es la imagen final lista para usar en el nodo "Send Image" de ManyChat.
🌐 Base URL
Todos los endpoints son HTTPS. No se requiere API key para /generate-multi. El servidor está alojado en Railway (región US-West) con dominio personalizado via SiteGround.
*). Puedes llamar la API directamente desde ManyChat HTTP Request, Make.com, n8n, Zapier o cualquier cliente HTTP.
🖼️ Renderizar imagen
Recibe una imagen base (plantilla) y un array de campos de texto, y devuelve una imagen PNG compuesta con todos los elementos renderizados.
Body (JSON)
| Campo | Tipo | Req. | Defecto | Descripción |
|---|---|---|---|---|
| template_name | string | ✓ | — | Nombre del archivo en /templates/ (ej. oferta.jpg) o URL completa HTTPS de la imagen base |
| texts | TextField[] | ✓ | — | Array de campos de texto a renderizar (ver modelo TextField) |
| vars | object | {} | Variables de sustitución. Reemplaza {clave} en cualquier text del array | |
| overlays | ImageOverlay[] | [] | Imágenes a superponer (foto de perfil del cliente, logo, etc.) | |
| shapes | CanvasShape[] | [] | Formas geométricas (rectángulos, círculos, estrellas) | |
| filter_name | string | "none" | Filtro de color aplicado a la imagen base antes de renderizar texto. Ver lista de filtros. | |
| render_scale | int | 1 | 1 = rápido (recomendado para ManyChat), 2 = alta calidad (editor) | |
| watermark | bool | false | Si true, agrega el sello textonflow.com en la esquina inferior derecha |
Respuesta exitosa (200)
{
"url": "https://www.textonflow.com/image/a3f8c2d1.png",
"width": 1080,
"height": 1080,
"texts_rendered": 2
}
Devuelve el archivo PNG generado por /generate-multi. La URL completa viene en el campo url de la respuesta de renderizado — úsala directamente en el nodo de imagen de ManyChat.
✨ Generación de imágenes con IA
Genera imágenes con Google Gemini 2.5 Flash Image. El proceso es asíncrono: primero inicias el job y luego haces polling hasta recibir el resultado. El texto del prompt pasa por el modelo Gemini 2.0 Flash para enriquecimiento antes de generarse.
Body (JSON)
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| prompt | string | ✓ | Descripción en texto de la imagen a generar |
| style | string | Estilo visual aplicado al prompt. Ver lista completa abajo. | |
| aspect_ratio | string | 1:1, 9:16, 3:4, 4:3, 16:9, 4:5, 3:2, 2:3 — Defecto: 1:1 | |
| width | integer | Ancho de salida en px (si se usa aspecto personalizado). Rango: 64–4096. | |
| height | integer | Alto de salida en px (si se usa aspecto personalizado). Rango: 64–4096. | |
| reference_images | array | Hasta 5 imágenes de referencia. Cada objeto: {"mime_type": "image/jpeg", "data": "<base64>"} |
Estilos visuales disponibles
| Valor | Descripción |
|---|---|
| cinematografico | Cinematográfico — iluminación dramática, colores profundos |
| boceto | Boceto a lápiz sobre papel |
| acuarela | Pintura en acuarela con bordes suaves |
| steampunk | Estética victoriana con maquinaria de vapor |
| risografia | Risografía — colores planos, trama de puntos |
| monocromo | Blanco y negro con alto contraste |
| pintura-oleo | Pintura al óleo con textura de pinceladas |
| dibujo-antiguo | Grabado antiguo / ilustración vintage |
| cyborg | Cyberpunk / neón con elementos tecnológicos |
| plumilla | Plumilla — tinta china con trazos nítidos |
| amanecer | Paleta de amanecer: rosados, naranjas y azules |
| color | Explosión de color vibrante |
Respuesta (200)
{ "job_id": "550e8400-e29b-41d4-a716-446655440000" }
Estados de respuesta (polling)
// Pendiente — seguir haciendo polling cada 2 segundos
{ "status": "pending" }
// Completado — imagen disponible
{
"status": "done",
"url": "https://www.textonflow.com/storage/gen_abc123.png"
}
// Error — el modelo rechazó el prompt o falló la generación
{ "status": "error", "error": "Prompt rejected by safety filters" }
status: "done" o "error".
📱 Código QR
Genera un QR como PNG en base64, listo para usarse como overlay en una imagen de plantilla.
Body (JSON)
| Campo | Tipo | Req. | Defecto | Descripción |
|---|---|---|---|---|
| text | string | ✓ | — | URL o texto a codificar en el QR |
| dark_color | string | "#000000" | Color de los módulos del QR (hex) | |
| light_color | string | "#FFFFFF" | Color de fondo del QR (hex) | |
| bg_color | string | "#FFFFFF" | Color del área de padding (hex) | |
| padding | int | 20 | Padding en píxeles alrededor del QR (0–120) |
Respuesta (200)
{ "image": "data:image/png;base64,iVBORw0KGgo..." }
📦 Modelo — MultiTextRequest
El body completo del endpoint /generate-multi. Combina todos los elementos visuales en una sola solicitud atómica.
{
"template_name": "plantilla-oferta.jpg", // nombre en /templates/ o URL HTTPS
"texts": [ /* array de TextField */ ],
"vars": { // variables de sustitución
"nombre": "Ana García",
"descuento": "30%"
},
"overlays": [ /* array de ImageOverlay */ ],
"shapes": [ /* array de CanvasShape */ ],
"filter_name": "none",
"render_scale": 1,
"watermark": false
}
✏️ Modelo — TextField
Define un bloque de texto con posición, tipografía, sombra, borde, fondo y efectos warp. Todos los campos son opcionales excepto text, x e y.
Posición y texto
| Campo | Tipo | Defecto | Descripción |
|---|---|---|---|
| text | string | requerido | Texto a mostrar. Acepta {variables}, emojis y saltos de línea \n |
| x | int | requerido | Posición X en píxeles desde la izquierda |
| y | int | requerido | Posición Y en píxeles desde arriba |
| font_size | int | 60 | Tamaño de fuente en puntos |
| font_color | string | "#FFFFFF" | Color del texto en hex o rgb() |
| font_name | string | "DejaVuSans-Bold" | Nombre de la fuente (ver sección Fuentes) |
| rotation | int | 0 | Rotación en grados (0–360) |
| alignment | string | "left" | Anclaje del bloque: left, center, right |
| text_align | string | "center" | Alineación interna del texto: left, center, right |
| line_spacing | int | 10 | Espaciado extra entre líneas en píxeles |
Text wrap automático
| Campo | Tipo | Defecto | Descripción |
|---|---|---|---|
| text_wrap_enabled | bool | false | Activa salto de línea automático por palabra |
| text_wrap_padding | int | 60 | Margen izquierdo+derecho en px. El texto ocupa ancho_imagen − 2×padding |
Sombra del texto
| Campo | Tipo | Defecto | Descripción |
|---|---|---|---|
| shadow_enabled | bool | false | Activa la sombra |
| shadow_color | string | "#000000" | Color de la sombra |
| shadow_opacity | int | 100 | Opacidad 0–100 |
| shadow_offset_x | int | 2 | Desplazamiento horizontal en px |
| shadow_offset_y | int | 2 | Desplazamiento vertical en px |
| shadow_blur | int | 0 | Radio de desenfoque gaussiano (0 = sombra dura) |
| shadow_blend_mode | string | "normal" | normal, multiply, darken, overlay, soft_light, screen |
Contorno (stroke) del texto
| Campo | Tipo | Defecto | Descripción |
|---|---|---|---|
| stroke_enabled | bool | false | Activa el contorno del texto |
| stroke_color | string | "#000000" | Color del contorno |
| stroke_opacity | int | 100 | Opacidad 0–100 |
| stroke_width | int | 2 | Grosor del contorno en píxeles |
Caja de fondo del texto
| Campo | Tipo | Defecto | Descripción |
|---|---|---|---|
| background_enabled | bool | false | Activa la caja de fondo detrás del texto |
| background_color | string | "#000000" | Color del fondo |
| background_opacity | int | 80 | Opacidad 0–100 |
| background_color_type | string | "solid" | solid, gradient2, instagram |
| background_gradient_color2 | string | "#FFFFFF" | Segundo color para tipo gradient2 |
| background_gradient_angle | int | 135 | Ángulo del gradiente en grados |
| background_radius | int | 0 | Radio de esquinas redondeadas en px |
| background_padding_top/right/bottom/left | int | 10 | Relleno interior de la caja (px por lado) |
| background_stroke_color | string | "#FFFFFF" | Color del borde de la caja |
| background_stroke_width | int | 0 | Grosor del borde de la caja (0 = sin borde) |
| background_stroke_type | string | "solid" | solid, gradient2, instagram |
| background_stroke_dash | string | "solid" | Estilo de línea: solid, dashed, dotted |
🖼️ Modelo — ImageOverlay
Superpone una imagen sobre la plantilla. Ideal para fotos de perfil del cliente, logos o imágenes generadas por IA.
| Campo | Tipo | Req. | Defecto | Descripción |
|---|---|---|---|---|
| src | string | ✓ | — | URL HTTPS de la imagen o Data URL base64 (data:image/png;base64,...) |
| x | int | 0 | Posición X en px | |
| y | int | 0 | Posición Y en px | |
| width | int | 100 | Ancho en px | |
| height | int | 100 | Alto en px | |
| opacity | float | 1.0 | Opacidad 0.0–1.0 | |
| rotation | float | 0 | Rotación en grados | |
| mask_type | string | "none" | Recorte: none, circle, ellipse, square, rect, star12 | |
| mask_radius | int | 0 | Radio de esquinas para mask_type: "rect" | |
| mask_border_width | int | 0 | Grosor del borde alrededor del overlay (px) | |
| mask_border_color | string | "#ffffff" | Color del borde | |
| mask_shadow_enabled | bool | false | Activa sombra bajo el overlay | |
| mask_shadow_blur | int | 8 | Desenfoque de la sombra (px) |
Ejemplo — foto de perfil circular
{
"src": "https://cdn.example.com/foto-cliente.jpg",
"x": 60, "y": 60,
"width": 200, "height": 200,
"mask_type": "circle",
"mask_border_width": 4,
"mask_border_color": "#FFFFFF",
"mask_shadow_enabled": true,
"mask_shadow_blur": 12
}
🔷 Modelo — CanvasShape
Forma geométrica renderizada debajo de los textos. Útil para crear fondos, marcos y divisores.
| Campo | Tipo | Defecto | Descripción |
|---|---|---|---|
| shape_type | string | "rect" | rect, square, ellipse, circle, star12 |
| x, y | int | 0 | Posición en px |
| width, height | int | 100 | Dimensiones en px |
| fill_color | string | "#667eea" | Color de relleno |
| fill_opacity | float | 0.8 | Opacidad 0.0–1.0 |
| stroke_color | string | "#000000" | Color del borde |
| stroke_width | int | 0 | Grosor del borde (0 = sin borde) |
| rotation | float | 0 | Rotación en grados |
| z_index | int | 0 | Orden de capas — mayor = encima |
| cover_blur | int | 0 | Desenfoque gaussiano aplicado a la zona de la imagen debajo de la forma (glass morphism) |
🔧 Variables dinámicas
Usa {nombre_variable} dentro de cualquier campo text de un TextField. El motor sustituye automáticamente cada llave con el valor correspondiente del objeto vars antes de renderizar.
{
"texts": [
{ "text": "Hola, {nombre} 👋", "x": 100, "y": 80, "font_size": 64 },
{ "text": "Tu descuento: {descuento}", "x": 100, "y": 180, "font_size": 48 }
],
"vars": {
"nombre": "{{first name}}", // custom field de ManyChat
"descuento": "{{descuento_pct}}"
}
}
{{custom_field_name}}. TextOnFlow entonces sustituye {nombre} con el valor real del suscriptor.
⏱️ Contadores regresivos
Un TextField puede convertirse en un contador regresivo activando countdown_mode. El texto del campo se ignora y se reemplaza automáticamente por el tiempo restante.
Modo evento (fecha fija)
{
"text": "",
"x": 540, "y": 400,
"font_size": 80,
"font_color": "#FFFFFF",
"countdown_mode": "event",
"countdown_event_end_utc": "2026-12-31T23:59:59Z",
"countdown_format": "DD:HH:MM:SS",
"countdown_expired_text": "¡Oferta expirada!"
}
Modo urgencia (timer por suscriptor)
{
"text": "",
"x": 540, "y": 400,
"countdown_mode": "urgency",
"countdown_ts_var": "timer_final", // nombre del custom field ManyChat
"countdown_format": "HH:MM:SS",
"countdown_urgency_hours": 3.0, // cambia color al llegar a 3h
"countdown_urgency_color": "#FF3B30"
}
| countdown_format | Ejemplo de salida |
|---|---|
| HH:MM:SS | 14:32:07 |
| DD:HH:MM:SS | 03:14:32:07 |
| HH:MM | 14:32 |
🎨 Filtros de imagen
El campo filter_name de MultiTextRequest aplica un filtro de color a la imagen base antes de renderizar los textos.
Cinematográficos / LUT
🌀 Efectos Warp (deformación de texto)
El campo warp_style de un TextField deforma el texto con distintas curvaturas. Combínalo con warp_bend (−100 a 100) para controlar la intensidad.
🔤 Fuentes disponibles
Pasa el nombre exacto en el campo font_name del TextField. Todas las fuentes están precargadas en el servidor.
🤖 Flujo ManyChat completo
Ejemplo de integración end-to-end: cuando un suscriptor activa el flujo, se genera una imagen personalizada y se envía por WhatsApp.
Nodo "Action" — Establecer custom field timer_final (solo si usas countdown urgencia)
Calcula el timestamp Unix de expiración y guárdalo en el custom field del suscriptor.
Fórmula ManyChat: {{NOW}} + 86400 (24 horas en segundos).
Nodo "HTTP Request" — Llamar a TextOnFlow
Method: POST
URL: https://www.textonflow.com/generate-multi
Headers: Content-Type: application/json
{
"template_name": "mi-plantilla.jpg",
"texts": [
{
"text": "Hola, {nombre} 🎉",
"x": 540, "y": 120,
"font_size": 72,
"font_color": "#FFFFFF",
"alignment": "center",
"shadow_enabled": true,
"shadow_blur": 6
}
],
"vars": {
"nombre": "{{first name}}"
}
}
url de la respuesta en un custom field, por ejemplo imagen_url.
Nodo "Send Message" → Image
En el campo URL de la imagen, usa el custom field donde guardaste la respuesta:
{{imagen_url}}
ManyChat descargará la imagen de TextOnFlow y la enviará al suscriptor por WhatsApp.
¡Listo! 🎯
El suscriptor recibe una imagen personalizada con su nombre, datos de la oferta y/o contador regresivo en tiempo real — todo sin ningún diseñador ni exportación manual.
⚠️ Códigos de error
| HTTP | Código | Causa | Solución |
|---|---|---|---|
| 200 | OK | Solicitud exitosa | — |
| 400 | Bad Request | JSON inválido o campo requerido faltante | Revisa que template_name y texts estén presentes |
| 404 | Not Found | El archivo de plantilla no existe | Verifica el nombre exacto del archivo en template_name |
| 422 | Unprocessable Entity | Tipo de dato incorrecto (ej. string donde se espera int) | Revisa los tipos del modelo. FastAPI devuelve detalles en detail |
| 500 | Internal Server Error | Error en el renderizado (fuente no encontrada, imagen corrupta) | Verifica el font_name y que la imagen base sea accesible |
| 503 | Service Unavailable | Servidor en mantenimiento o reinicio | Reintenta en 10–30 segundos |
📏 Límites y buenas prácticas
Límites técnicos
- Tamaño máximo recomendado de imagen base: 3840 × 3840 px (para render_scale 1)
- Imágenes externas (overlays, template_name por URL): máximo 15 segundos de timeout de descarga
- Tiempo de respuesta típico: 200–800 ms para plantillas locales con render_scale 1
- La generación IA puede tardar 8–30 segundos — usa el sistema de polling
Buenas prácticas
- Usa render_scale: 1 para flujos automáticos de ManyChat — es significativamente más rápido y el resultado es idéntico para pantallas móviles
- Subir plantillas al editor en lugar de URLs externas — elimina el delay de descarga y es más estable
- Tamaño recomendado de plantillas: 1080×1080 px para stories cuadradas, 1080×1920 px para vertical
- Fonts: Usa
Montserrat-BoldoBebasNeue-Regularpara texto de marketing — mayor impacto visual - Overlays de fotos de perfil: Usa
mask_type: "circle"conmask_border_width: 4para un acabado profesional - Variables: Si un custom field puede estar vacío en ManyChat, define un fallback en el texto:
{nombre|Cliente}— TextOnFlow usa el valor antes del|si está disponible