Referencia del payload del webhook
Cada cabecera y cada campo que GrowWriter envía a tu endpoint de webhook, con su tipo, si está siempre presente, su formato exacto y en qué puedes confiar.
Última actualización el
Esta página es el contrato, escrito para quien construya tu endpoint. Si no eres tú, puedes parar aquí. La guía cubre la configuración; esta página lista exactamente qué llega, para que nadie tenga que adivinar.
Cómo leer esta página
Todas las tablas tienen las mismas cinco columnas:
- Campo: la ruta dentro del cuerpo JSON, en código.
images[].urlsignifica laurlde cada elemento del arrayimages. - Tipo:
string,string[](una lista de cadenas),object,object[],number,boolean. - Presente: una de tres expresiones. Siempre significa que la clave
está en todas las entregas. Siempre, puede estar vacío significa que la
clave está, pero el valor puede ser una cadena vacía o una lista vacía.
Se omite si no hay valor significa que la clave simplemente no aparece
en el JSON cuando no hay valor. Nunca es
null. - Formato y garantías: qué aspecto tiene el valor y en qué puedes confiar. Cuando algo no se garantiza, la tabla lo dice.
- Ejemplo: un valor con aspecto real.
Dos reglas se aplican en todas partes. Primera, la última entrega es la autoridad: si un valor difiere entre dos entregas del mismo artículo, usa el más reciente. Segunda, ignora los campos que no reconozcas: pueden aparecer campos opcionales nuevos sin cambio de versión (mira Versionado).
Un pequeño glosario para los términos de las tablas: una fecha ISO 8601
tiene esta pinta: 2026-09-01T09:30:00.000Z (año-mes-día, una T, la hora
hasta el milisegundo y una Z que significa UTC). Un UUID es un id
aleatorio como 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e.
La petición
GrowWriter envía un POST por entrega a la URL del endpoint que
introdujiste.
| Propiedad | Valor |
|---|---|
| Método | POST |
Content-Type | application/json |
User-Agent | GrowWriter-Webhooks/1.0 |
| Tamaño del cuerpo | Sin tope. Un artículo estándar suele ocupar unos cientos de kilobytes porque el contenido viaja tres veces (markdown, HTML, bloques). Los artículos largos pueden superar 1 MB. |
Qué cuenta como éxito, cómo funcionan los reintentos y qué responder está en Entrega. Las cabeceras que necesitas:
| Cabecera | Tipo | Formato y garantías | Ejemplo |
|---|---|---|---|
X-GrowWriter-Event | string | ping, article.published, article.updated o article.unpublished. Mismo valor que event en el cuerpo. | article.published |
X-GrowWriter-Delivery | string | UUID v4. Idéntico en todos los intentos de una entrega. Mismo valor que deliveryId en el cuerpo. | 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e |
X-GrowWriter-Signature | string | t=<segundos unix>,v1=<64 caracteres hex>. v1 es el HMAC-SHA256 de "<t>.<cuerpo sin procesar>" con tu secreto de firma completo como clave. Nuevo en cada intento. | t=1756718400,v1=5257a8… |
Entrega
Una publicación a una conexión es una entrega, y una entrega hace hasta tres intentos antes de rendirse:
| Intento | Cuándo ocurre |
|---|---|
| 1 | En el momento en que publicas |
| 2 | 1 segundo después de que falle el intento 1 |
| 3 | 3 segundos después de que falle el intento 2 |
Las reglas en las que puede confiar tu código:
- Timeout. Cada intento espera hasta 10 segundos tu respuesta.
- Éxito. Cualquier estado
2xx. Responde con el JSON{"url": "https://…"}para indicar la entrada publicada y GrowWriter muestra un enlace Ver publicación hacia ella (mira La respuesta); cualquier otro cuerpo de éxito se ignora. Cuando un intento se rechaza, GrowWriter muestra el estado y los primeros 200 caracteres del cuerpo en su diálogo, así que un motivo corto en texto plano te ayuda a depurar. - Fallo. Un timeout, cualquier
3xx(las redirecciones nunca se siguen; todos los intentos van a la URL exacta que introdujiste) o cualquier otro estado. - Mismo id, firma nueva. Todos los intentos comparten un mismo id
X-GrowWriter-Delivery, y cada intento va firmado con su propio timestamp, así que un reintento nunca hace saltar tu comprobación de los cinco minutos. - Al menos una vez. Un
2xxlento puede llegar cuando GrowWriter ya ha reintentado, así que el mismo id de entrega puede llegarte dos veces. Trata un id repetido como ya hecho. - Nada después del tercer intento. GrowWriter no reintenta más tarde. El
canal muestra un botón Reintentar en el diálogo de publicación, que
empieza una entrega nueva con un id nuevo. Publicar el artículo otra vez
más adelante también lo entrega de nuevo, como
article.updated. - Ritmo. La publicación está limitada a 20 peticiones de publicación por hora y cuenta, una petición por canal. El evento de prueba está limitado a 60 por hora. Las entregas en sí no tienen límite de ritmo.
El evento de prueba (Enviar evento de prueba en el diálogo de conexión) sigue las mismas reglas, así que un endpoint que nunca responde tarda alrededor de medio minuto en fallar.
La respuesta
GrowWriter solo necesita de ti un estado 2xx. Opcionalmente, dile dónde
quedó publicada la entrada para que el diálogo de publicación pueda
enlazarla:
{ "url": "https://www.example.com/blog/mi-articulo" }Las reglas:
- URL absoluta
httpohttps. Las rutas relativas y cualquier otro esquema se ignoran, porque el valor se muestra como un enlace. - Las demás claves se ignoran. Puedes devolver la url junto a tus propios campos.
- Nada es nunca un error. Un
OKa secas, un cuerpo vacío, algo que no sea JSON o un JSON sinurlcuentan como éxito; el diálogo simplemente no muestra ningún enlace. - Mantenlo pequeño. GrowWriter lee los primeros 4 KB del cuerpo.
- Cada entrega reemplaza el enlace. El enlace guardado es el que devolvió la última entrega exitosa. Responde sin url en una republicación y el enlace desaparece.
Devuelve la url cuando tu handler crea la entrada antes de responder. Si respondes primero y creas la entrada en un trabajo en segundo plano, todavía no conoces la url: omítela, y el diálogo no muestra ningún enlace para esa publicación.
El sobre
Los campos de primer nivel, presentes en todas las entregas:
| Campo | Tipo | Presente | Formato y garantías | Ejemplo |
|---|---|---|---|---|
version | string | Siempre | Una fecha que da nombre a la forma del payload. Solo cambia con cambios incompatibles; mira Versionado. | "2026-09-06" |
event | string | Siempre | ping, article.published, article.updated o article.unpublished. | "article.published" |
deliveryId | string | Siempre | UUID v4. Un valor por entrega, compartido por sus reintentos. | "9d2f7c1e-…" |
sentAt | string | Siempre | ISO 8601, UTC, milisegundos. Cuándo se construyó el payload (una vez por entrega, no por intento). | "2026-09-01T09:30:00.412Z" |
Los cuatro eventos:
| Evento | Cuándo se dispara | Cuerpo |
|---|---|---|
ping | Haces clic en Enviar evento de prueba en el diálogo de conexión. | Solo el sobre |
article.published | La primera entrega exitosa de un artículo a esta conexión de webhook. | Sobre + el article completo |
article.updated | Cualquier publicación posterior del mismo artículo a esta conexión, editado o no. | Sobre + el article completo |
article.unpublished | El artículo se elimina en GrowWriter, o esta conexión de webhook se desconecta (una vez por cada artículo que había publicado). | Sobre + solo article.id, article.slug y article.language |
"Esta conexión" importa: publicar el mismo artículo en una segunda conexión
de webhook envía article.published a esa conexión, porque nunca ha visto el
artículo. Un primer intento que falló también cuenta como nunca visto, así
que el reintento vuelve a ser article.published. Los dos eventos de
publicación llevan el artículo completo, así que tu handler puede tratarlos
igual: crea o actualiza la entrada usando article.id como clave. En
article.unpublished, oculta o elimina la entrada con ese id; cuál de las dos
cosas es decisión tuya, y con una eliminación no viaja ningún contenido.
El evento ping
Enviar evento de prueba en el diálogo de conexión entrega un ping. Solo
lleva el sobre, sin article:
{
"version": "2026-09-06",
"event": "ping",
"deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
"sentAt": "2026-09-01T09:30:00.000Z"
}El evento unpublished
Eliminar un artículo en GrowWriter, o desconectar el webhook, entrega
article.unpublished por cada artículo que esta conexión había publicado.
Identifica la entrada y nada más:
{
"version": "2026-09-06",
"event": "article.unpublished",
"deliveryId": "0b7d3e2a-9c41-4f58-8e6d-2a1b3c4d5e6f",
"sentAt": "2026-09-03T08:12:44.000Z",
"article": {
"id": "cme8x2k9r0001l7042n5q8w3v",
"slug": "como-elegir-zapatillas-de-running",
"language": "es"
}
}Oculta o elimina la entrada usando article.id como clave. Se aplican las
mismas reglas de entrega (tres intentos y después nada), así que un receptor
que estaba caído se pierde la eliminación: trata un article.published
posterior para un id que aún conservas como el estado autoritativo.
article: identidad
| Campo | Tipo | Presente | Formato y garantías | Ejemplo |
|---|---|---|---|---|
article.id | string | Siempre | 25 caracteres, letras minúsculas y dígitos, empieza por c. Seguro en URLs y nombres de archivo. Nunca cambia. Esta es tu clave. | "cme8x2k9r0001l7042n5q8w3v" |
article.language | string | Siempre | Dos letras minúsculas, ISO 639-1. Nunca una etiqueta de región (es, nunca es-ES). El noruego es nb. Por defecto en. | "es" |
article.slug | string | Siempre | Minúsculas, derivado del título cuando se genera el brief. Nunca está vacío. No es único, sin longitud máxima, puede contener letras no latinas. | "como-elegir-zapatillas-de-running" |
Notas sobre la identidad:
- Cada idioma es su propio artículo. Una traducción es un borrador aparte
con su propio
id. Empareja los idiomas con tus propios datos, usandoarticle.language. - Los 41 códigos de idioma posibles:
ar bg ca cs da de el en es et fa fi fr he hi hr hu hy id is it ja ko lt lv nb nl pl pt ro ru sk sl sr sv th tl tr uk vi zh. - Cómo se construye el slug. Los acentos latinos se eliminan (
quépasa aque), los espacios se convierten en guiones, la puntuación se descarta y las letras de otros alfabetos se conservan, así que un título en japonés da un slug en japonés. Codifícalo con porcentajes antes de ponerlo en una URL (encodeURIComponent(article.slug)en JavaScript). - Cuándo cambia el slug. Editar el título en el editor no lo cambia.
Elegir un título nuevo en el writer regenera el brief y el slug, así que un
article.updatedpuede llevar un slug nuevo bajo el mismoid. - Nunca uses el slug como clave. Puede cambiar en una actualización y
dos artículos pueden compartir uno. Guárdalo como dato de presentación
junto al
id.
article: campos de texto
| Campo | Tipo | Presente | Formato y garantías | Ejemplo |
|---|---|---|---|---|
article.title | string | Siempre | Unicode. Sin longitud máxima. No se repite dentro del contenido, así que renderízalo tú. | "Cómo elegir zapatillas de running que de verdad te queden bien" |
article.summary | string | Se omite si no hay valor | Texto plano, un párrafo. Sin longitud máxima. | "Una guía práctica para…" |
article.metaDescription | string | Se omite si no hay valor | Texto plano escrito para tener entre 140 y 160 caracteres. Nunca supera los 160: lo que sobra se corta en un límite de palabra antes de la entrega. | "Aprende a elegir zapatillas de running…" |
article.takeaways | string[] | Siempre, puede estar vacío | Frases en texto plano, de 3 a 5 tal como se generan; trata la cantidad como ilimitada. Cada una es "<punto>: <detalle>" o solo <detalle>. | ["El ajuste importa más que la marca: …"] |
article.keyword | string | Se omite si no hay valor | El único término de búsqueda para el que se escribió el artículo, tal como lo tecleó el autor. Para tus herramientas SEO; no es una etiqueta. | "cómo elegir zapatillas de running" |
article.relatedQuestions | string[] | Siempre, puede estar vacío | Las preguntas de "otras preguntas de los usuarios" de Google para la palabra clave, en el idioma y el país del artículo, capturadas cuando se generaron los títulos. Como máximo 8, en el orden de Google. Material para FAQ: muéstralas bajo el artículo o como marcado FAQPage. El artículo no promete responder a cada una, y no son etiquetas. | ["¿cada cuánto hay que cambiar las zapatillas de running?"] |
article.coverImage y article.images
La portada y cada imagen interior comparten una misma forma de objeto:
| Campo | Tipo | Presente | Formato y garantías | Ejemplo |
|---|---|---|---|---|
url | string | Siempre | URL pública https:// en el almacenamiento de GrowWriter. Hoy por hoy no caduca, pero no es un alojamiento a largo plazo: descárgala. | "https://….public.blob.vercel-storage.com/…" |
altText | string | Se omite si no hay valor | Texto de accesibilidad. | "Vista lateral de un corredor en plena zancada" |
caption | string | Se omite si no hay valor | Pie de foto visible. También aparece en content.markdown como un párrafo en cursiva justo después de la imagen. | "Una comprobación rápida de la pisada…" |
filename | string | Se omite si no hay valor | Nombre sugerido sin extensión. No se garantiza: al generador se le piden de 3 a 7 palabras ASCII en minúsculas unidas por guiones. Genera tus propios nombres a partir de article.id si necesitas certeza. | "runner-gait-check" |
sectionSlug | string | Se omite si no hay valor | La sección del esquema para la que se generó la imagen, o un valor user-… en las imágenes que el autor insertó a mano. Nunca en la portada. No lo parsees, no lo uses como clave. | "section-1-4f1a4848" |
attribution | object | Se omite si no hay valor | { authorName?, pageUrl?, licenseShortName?, licenseUrl? }, todos los campos opcionales. Presente en las fotos de Wikimedia Commons. También se renderiza como una línea en cursiva bajo el pie de foto en content.markdown. | mira el ejemplo |
Reglas que difieren entre las dos:
article.coverImagese omite si no hay valor. Nunca tienesectionSlug.article.imagesestá siempre presente y puede estar vacío. Lista las imágenes interiores en el orden del documento y no incluye la portada. No hay un número máximo.sectionSluges estable durante toda la vida del artículo; solo regenerar el esquema en el writer produce ids nuevos.
Archivos de imagen
- Formato: JPEG, PNG, WebP o AVIF. Nunca GIF ni SVG.
- Tamaño: los archivos se guardan tal como se obtuvieron, a su resolución original, sin recodificar. Las fotos de stock suelen ocupar de 1 a 6 MB; algunos originales superan los 10 MB. Redimensiona y comprime para tu sitio.
- Estabilidad: una imagen que no cambió conserva exactamente la misma URL en entregas posteriores. Una imagen sustituida recibe una URL nueva. Las URLs antiguas siguen funcionando.
article.content
| Campo | Tipo | Presente | Formato y garantías |
|---|---|---|---|
article.content.markdown | string | Siempre | El artículo como markdown de estilo CommonMark, normalizado por GrowWriter. Mira qué sobrevive. |
article.content.html | string | Siempre | HTML listo para insertar, construido desde la misma estructura. Mira qué contiene el HTML. |
article.content.blocks | object[] | Siempre | El artículo como árbol JSON para renderizadores propios. Mira el formato blocks. |
Los tres llevan el mismo artículo. Elige uno al renderizar; el título no está en ninguno de ellos.
Qué sobrevive en markdown
GrowWriter vuelve a renderizar content.markdown a partir de la estructura
del artículo, así que es predecible:
- Presente: encabezados de
#a######, párrafos, listas ordenadas y no ordenadas (anidadas con dos espacios), citas, bloques de código delimitados con un lenguaje, separadores---,**strong**,*em*,~~tachado~~,`code`,[texto](https://…),. Los bloques van separados por líneas en blanco. - Imágenes:
, luego un párrafo en cursiva con el pie de foto y después, solo en las fotos con atribución, una línea en cursiva como*Photo: Jane Doe · [Source](url) · [CC BY-SA 4.0](license-url)*. Solo se conservan las direcciones de imagenhttp(s); cualquier otra se descarta. - No presente: tablas, notas al pie, listas de tareas. Si un autor las escribió, llegan como párrafos normales.
- HTML sin procesar: si un autor pegó HTML en el editor, llega como texto literal, no como marcado.
- Escapado: la prosa normal contiene
{,}y<. Escápalos si compilas el markdown como MDX, JSX o una plantilla.
Qué contiene el HTML
content.html se construye elemento a elemento, nunca se copia de ningún
sitio:
- Etiquetas usadas: de
h1ah6,p,ul,ol(constartcuando hace falta),li,blockquote,pre,code(conclass="language-…"),hr,figure,img,figcaption,a,strong,em,dely unspanusado como separador dentro de los pies de foto. Ninguna otra etiqueta, sin ids, sin estilos en línea, sin scripts. - El texto siempre va escapado (
&,<,>,"). - Los enlaces solo se conservan con direcciones
http://ohttps://; cualquier otra cosa se convierte en texto plano. - Las imágenes se renderizan como
<figure><img src alt><figcaption>…</figcaption></figure>, sin elfigcaptioncuando no hay pie de foto ni atribución. Una imagen cuya dirección no eshttp(s)se descarta por completo. - Sin elemento envolvente y sin doctype: insértalo en tu página tal cual.
article.dates
| Campo | Tipo | Presente | Formato y garantías | Ejemplo |
|---|---|---|---|---|
article.dates.createdAt | string | Siempre | ISO 8601, UTC, milisegundos. Cuándo se creó el borrador en GrowWriter. | "2026-08-28T14:00:00.000Z" |
article.dates.firstPublishedAt | string | Siempre | ISO 8601. Cuándo se publicó el artículo por primera vez en esta conexión. Idéntico en todas las entregas posteriores. | "2026-09-01T09:30:00.000Z" |
article.dates.modifiedAt | string | Siempre | ISO 8601. El momento en que se pidió esta publicación. Avanza en cada publicación, incluso sin ediciones. Nunca anterior a firstPublishedAt. | "2026-09-01T09:30:00.000Z" |
Cómo se relacionan las tres:
- En la primera entrega,
firstPublishedAtymodifiedAtson iguales. modifiedAtno es "última edición". Editar sin volver a publicar no cambia nada; volver a publicar sin ediciones la hace avanzar. Usa eleventpara distinguir una primera publicación de una actualización, no una comparación de fechas.- Los campos de fecha sin hora de un CMS suelen necesitar
YYYY-MM-DD: toma los diez primeros caracteres.
article.brand
| Campo | Tipo | Presente | Formato y garantías | Ejemplo |
|---|---|---|---|---|
article.brand | object | Se omite si no hay valor | Presente cuando el artículo pertenece a un kit de marca (el caso normal). | |
article.brand.name | string | Siempre dentro de brand | El nombre visible de la marca. Para el marcado de editor u organización, no de autor. | "Stride Lab" |
article.brand.websiteUrl | string | Se omite si no hay valor | URL https:// del sitio web de la marca. | "https://stridelab.example.com" |
El payload identifica una marca, nunca a una persona. Si tus entradas necesitan un autor, asígnalo en tu propia lógica de guardado.
El formato blocks
content.blocks es el artículo como árbol JSON, para sitios que renderizan
el contenido por su cuenta en lugar de confiar en el HTML ya hecho. Es un
array de objetos block, y cada bloque es de uno de estos siete tipos:
| Bloque | Campos |
|---|---|
heading | level (de 1 a 6) y children (nodos inline) |
paragraph | children (nodos inline) |
list | ordered (booleano), un número start opcional e items; cada elemento tiene su propio array blocks, así que las listas anidadas aparecen como bloques de lista dentro de un elemento |
blockquote | blocks: bloques completos anidados dentro de la cita |
code | language opcional y code como una sola cadena |
hr | nada más; un separador temático |
image | src, más alt, caption, filename, sectionSlug y attribution, todos opcionales (mismo significado que en imágenes) |
Siempre que un bloque tiene children, esos son nodos inline: text
(con text), code (con text), link (con href y children) y
strong / em / del (cada uno con children, para que los estilos
puedan anidarse).
Un párrafo con texto en negrita, por ejemplo:
{
"type": "paragraph",
"children": [
{ "type": "text", "text": "Ese es todo el secreto: " },
{ "type": "strong", "children": [{ "type": "text", "text": "el ajuste primero" }] },
{ "type": "text", "text": ", todo lo demás después." }
]
}Payload de ejemplo completo
Una entrega article.published completa, generada por el mismo código que
llamará a tu endpoint. Las URLs de las imágenes son de relleno: las reales
apuntan al almacenamiento público de GrowWriter.
Versionado
La versión actual es 2026-09-06, y esta página la describe por completo. El
campo version es una fecha que da nombre a la forma del payload. Pueden
aparecer campos opcionales nuevos, y una clave que se omitía puede
empezar a aparecer, sin que esa fecha cambie, así que escribe tu endpoint
para que ignore los campos que no reconozca en lugar de rechazarlos. Nada se
elimina, se renombra ni cambia de tipo dentro de una versión. La fecha solo
cambia con un cambio incompatible, y cualquier cambio así se anuncia en el
registro de cambios antes de que
llegue a tu endpoint.
Publicar con webhooks
Envía cada artículo que publicas en GrowWriter directamente a tu propio sitio web. Configura la integración de webhooks paso a paso, con código listo para usar.
Solución de problemas de webhooks
Arregla un evento de prueba o una entrega fallidos por síntoma, con los mensajes exactos que muestra GrowWriter, qué significa cada uno y qué hacer.