Docs

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[].url significa la url de cada elemento del array images.
  • 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.

PropiedadValor
MétodoPOST
Content-Typeapplication/json
User-AgentGrowWriter-Webhooks/1.0
Tamaño del cuerpoSin 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:

CabeceraTipoFormato y garantíasEjemplo
X-GrowWriter-Eventstringping, article.published, article.updated o article.unpublished. Mismo valor que event en el cuerpo.article.published
X-GrowWriter-DeliverystringUUID v4. Idéntico en todos los intentos de una entrega. Mismo valor que deliveryId en el cuerpo.9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e
X-GrowWriter-Signaturestringt=<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:

IntentoCuándo ocurre
1En el momento en que publicas
21 segundo después de que falle el intento 1
33 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 2xx lento 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:

Respuesta de éxito (cuerpo opcional)
{ "url": "https://www.example.com/blog/mi-articulo" }

Las reglas:

  • URL absoluta http o https. 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 OK a secas, un cuerpo vacío, algo que no sea JSON o un JSON sin url cuentan 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:

CampoTipoPresenteFormato y garantíasEjemplo
versionstringSiempreUna fecha que da nombre a la forma del payload. Solo cambia con cambios incompatibles; mira Versionado."2026-09-06"
eventstringSiempreping, article.published, article.updated o article.unpublished."article.published"
deliveryIdstringSiempreUUID v4. Un valor por entrega, compartido por sus reintentos."9d2f7c1e-…"
sentAtstringSiempreISO 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:

EventoCuándo se disparaCuerpo
pingHaces clic en Enviar evento de prueba en el diálogo de conexión.Solo el sobre
article.publishedLa primera entrega exitosa de un artículo a esta conexión de webhook.Sobre + el article completo
article.updatedCualquier publicación posterior del mismo artículo a esta conexión, editado o no.Sobre + el article completo
article.unpublishedEl 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:

ping
{
  "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:

article.unpublished
{
  "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

CampoTipoPresenteFormato y garantíasEjemplo
article.idstringSiempre25 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.languagestringSiempreDos 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.slugstringSiempreMinú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, usando article.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 a que), 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.updated puede llevar un slug nuevo bajo el mismo id.
  • 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

CampoTipoPresenteFormato y garantíasEjemplo
article.titlestringSiempreUnicode. 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.summarystringSe omite si no hay valorTexto plano, un párrafo. Sin longitud máxima."Una guía práctica para…"
article.metaDescriptionstringSe omite si no hay valorTexto 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.takeawaysstring[]Siempre, puede estar vacíoFrases 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.keywordstringSe omite si no hay valorEl ú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.relatedQuestionsstring[]Siempre, puede estar vacíoLas 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:

CampoTipoPresenteFormato y garantíasEjemplo
urlstringSiempreURL 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/…"
altTextstringSe omite si no hay valorTexto de accesibilidad."Vista lateral de un corredor en plena zancada"
captionstringSe omite si no hay valorPie 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…"
filenamestringSe omite si no hay valorNombre 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"
sectionSlugstringSe omite si no hay valorLa 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"
attributionobjectSe 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.coverImage se omite si no hay valor. Nunca tiene sectionSlug.
  • article.images está 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.
  • sectionSlug es 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

CampoTipoPresenteFormato y garantías
article.content.markdownstringSiempreEl artículo como markdown de estilo CommonMark, normalizado por GrowWriter. Mira qué sobrevive.
article.content.htmlstringSiempreHTML listo para insertar, construido desde la misma estructura. Mira qué contiene el HTML.
article.content.blocksobject[]SiempreEl 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://…), ![alt](url). Los bloques van separados por líneas en blanco.
  • Imágenes: ![alt](url), 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 imagen http(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 h1 a h6, p, ul, ol (con start cuando hace falta), li, blockquote, pre, code (con class="language-…"), hr, figure, img, figcaption, a, strong, em, del y un span usado 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:// o https://; cualquier otra cosa se convierte en texto plano.
  • Las imágenes se renderizan como <figure><img src alt><figcaption>…</figcaption></figure>, sin el figcaption cuando no hay pie de foto ni atribución. Una imagen cuya dirección no es http(s) se descarta por completo.
  • Sin elemento envolvente y sin doctype: insértalo en tu página tal cual.

article.dates

CampoTipoPresenteFormato y garantíasEjemplo
article.dates.createdAtstringSiempreISO 8601, UTC, milisegundos. Cuándo se creó el borrador en GrowWriter."2026-08-28T14:00:00.000Z"
article.dates.firstPublishedAtstringSiempreISO 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.modifiedAtstringSiempreISO 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, firstPublishedAt y modifiedAt son iguales.
  • modifiedAt no es "última edición". Editar sin volver a publicar no cambia nada; volver a publicar sin ediciones la hace avanzar. Usa el event para 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

CampoTipoPresenteFormato y garantíasEjemplo
article.brandobjectSe omite si no hay valorPresente cuando el artículo pertenece a un kit de marca (el caso normal).
article.brand.namestringSiempre dentro de brandEl nombre visible de la marca. Para el marcado de editor u organización, no de autor."Stride Lab"
article.brand.websiteUrlstringSe omite si no hay valorURL 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:

BloqueCampos
headinglevel (de 1 a 6) y children (nodos inline)
paragraphchildren (nodos inline)
listordered (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
blockquoteblocks: bloques completos anidados dentro de la cita
codelanguage opcional y code como una sola cadena
hrnada más; un separador temático
imagesrc, 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.

En esta página