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.
Última actualización el
Puede que tu sitio web no funcione con ninguna de las plataformas que GrowWriter conecta de serie. No pasa nada. Con la integración de webhooks, GrowWriter entrega cada artículo directamente en tu sitio en el momento en que pulsas Publicar. Stack propio, CMS casero, sitio estático, lo que sea. Si puede recibir una petición web, puede recibir tus artículos.
Esta guía te acompaña durante toda la configuración. No necesitas experiencia con webhooks. Conectarás el webhook en GrowWriter, añadirás un pequeño trozo de código a tu sitio web y lo probarás. Al terminar, publicar un artículo en GrowWriter creará o actualizará una entrada en tu propio sitio web automáticamente. Solo necesitas un sitio web al que puedas añadir código.
¿Quieres el camino más corto primero? El inicio rápido lleva un stack hasta un evento de prueba entregado en una sola página; vuelve aquí para guardar el artículo. Cuando necesites todos los detalles, dos páginas complementarias los tienen: la referencia del payload y la solución de problemas.
¿Ya sabes qué es un webhook? Salta a la configuración.
¿Qué es un webhook?
Un webhook es la forma más simple de comunicar dos aplicaciones: cuando algo ocurre en una, esta envía un mensaje a una dirección web que tú eliges.
Piensa en un servicio de mensajería. Le das a GrowWriter tu dirección (una URL de tu sitio web). Cuando un artículo está listo, GrowWriter llama a esa puerta y entrega el artículo completo como datos estructurados. Tu sitio hace el resto: guardarlo, darle formato y publicarlo a tu manera.
Por qué es mejor que las alternativas:
- Sin copiar y pegar. El artículo llega con su título, contenido, imágenes y campos SEO ya organizados.
- Sin comprobar novedades. Tu sitio no tiene que preguntar "¿hay algo nuevo?" cada pocos minutos. GrowWriter solo llama cuando hay algo que entregar.
- Sin depender de una plataforma. Los webhooks hablan HTTPS y JSON puros, que cualquier stack web entiende.
Cómo funciona
El flujo completo son tres pasos:
- Le das a GrowWriter la dirección de un endpoint de tu sitio (una URL que acepta peticiones, servida por un pequeño trozo de código que llamamos el receptor), y GrowWriter te da una clave secreta a cambio.
- Cuando publicas un artículo, GrowWriter envía una petición
POSTa ese endpoint. El cuerpo de la petición es el artículo completo en JSON, y la petición va firmada con tu secreto para que puedas confirmar que viene de GrowWriter. - Tu sitio verifica la firma, guarda el artículo y responde con un estado de
éxito (cualquier
2xx), incluyendo opcionalmente la URL de la entrada nueva para que GrowWriter pueda enlazarla. Una redirección (3xx) no es un éxito. Listo.
¿Por qué la firma y el secreto?
Tu endpoint es una URL pública. Cualquiera en internet puede enviarle una
petición POST, y un "artículo" falso de un desconocido tendría el mismo
aspecto que uno real. La firma es lo que te permite distinguirlos. Esta es
toda la idea en cuatro pasos:
- El secreto es una contraseña que solo conocéis tú y GrowWriter. Lo recibes una vez, durante la configuración, y lo guardas en tu servidor. Nunca viaja dentro de las peticiones.
- GrowWriter sella cada petición con una huella. Antes de enviar, calcula un HMAC: un hash del cuerpo de la petición y la hora actual, mezclado con tu secreto. Solo alguien que tenga el secreto puede producir esa huella.
- Tu endpoint recalcula y compara. Ejecutas el mismo cálculo sobre el
cuerpo que recibiste. Si tu resultado coincide con la huella de la
cabecera
X-GrowWriter-Signature, quedan demostradas dos cosas a la vez: la petición viene realmente de GrowWriter y nadie cambió el cuerpo por el camino. - El timestamp bloquea las repeticiones. La hora de envío va incorporada en la huella, así que un atacante no puede grabar una petición real y volver a enviarla más tarde. Rechaza todo lo que tenga más de cinco minutos y una petición capturada caduca antes de poder reutilizarse.
No tienes que construir nada de esto tú mismo. Cada ejemplo de código de más abajo hace las cuatro partes. Esta sección solo explica para qué sirve ese código.
Configúralo paso a paso
Conecta el webhook en GrowWriter
Abre la página Publicar en GrowWriter y busca la tarjeta Webhook en el grupo Automatización. Haz clic en Conectar.

Introduce tu URL del endpoint. Es la dirección de tu sitio web que
recibirá los artículos, por ejemplo https://example.com/webhooks/growwriter.
Tres reglas: debe empezar por https://, no puede contener un usuario ni una
contraseña, y su host debe ser accesible desde internet (localhost y las
direcciones de redes privadas se rechazan).
¿Todavía no tienes el endpoint construido? No hay problema. GrowWriter comprueba la dirección pero no la llama en este punto, así que puedes conectar primero y escribir el código justo después.
Usa la URL exacta en la que responde tu sitio, sin redirección
Si tu host redirige example.com a www.example.com (o al revés),
introduce la dirección a la que redirige. GrowWriter nunca sigue una
redirección, así que una dirección que redirige hace fallar todas las
entregas. ¿No sabes cuál usa tu sitio? Compruébalo en solución de
problemas. Puedes
cambiar la dirección más adelante con Editar URL en la fila del webhook,
y tu secreto no cambia.
Haz clic en Crear conexión. La fila del webhook muestra Sin verificar hasta que el primer evento de prueba o la primera publicación tenga éxito.

Copia tu secreto de firma
El diálogo muestra ahora tu Secreto de firma. Empieza por whsec_ y es
la contraseña compartida de
¿Por qué la firma y el secreto?: la clave que tu código
usa para comprobar que una petición viene realmente de GrowWriter.
Haz clic en Copiar secreto y guárdalo en un lugar seguro de tu servidor, normalmente como variable de entorno:
GROWWRITER_WEBHOOK_SECRET=whsec_your_secret_hereUn detalle que despista a mucha gente: cuando tu código verifica una firma,
la clave es la cadena completa, prefijo whsec_ incluido. No quites el
prefijo. Y si quieres validar el valor en tu configuración, un secreto es
siempre whsec_ seguido de 43 caracteres de base64 seguros para URL.
Este secreto se muestra una sola vez
GrowWriter lo guarda cifrado y no puede mostrarlo de nuevo. Si lo pierdes, desconecta y vuelve a conectar el webhook para obtener uno nuevo. Reconectar siempre crea un secreto nuevo, y el anterior deja de funcionar.

Construye tu endpoint
A partir de aquí, este es trabajo de un desarrollador. Pásale esta página a la persona que lleva tu sitio web; necesita el secreto del paso 2. Tú vuelves en el paso 4 para enviar el evento de prueba.
El endpoint es un pequeño trozo de código, el receptor, en la URL del paso 1.
Lee la petición exactamente como llegó, comprueba la firma, responde con un
estado 2xx y guarda el artículo. Si para entonces conoce la URL pública de
la entrada, puede devolverla como {"url": "https://…"} y GrowWriter
muestra un enlace Ver publicación en el diálogo de publicación.
¿No sabes qué ejemplo usar? Si tu sitio funciona con Next.js, Vercel o Netlify, empieza por la pestaña Next.js. Si tienes tu propio servidor, elige el lenguaje en el que está escrito. Copia el archivo de los ejemplos de código de más abajo, pon el secreto del paso 2 en las variables de entorno de tu proveedor de hosting (ajustes que tu host guarda fuera de tu código) y despliega para que el archivo responda en la URL de tu endpoint.
Todos los ejemplos ya responden al evento de prueba. Guardar el artículo es donde entra tu propia configuración: si tu sitio usa un CMS o una base de datos, Guarda el artículo en tu CMS te dice qué preparar.
Envía un evento de prueba
De vuelta en el diálogo de GrowWriter, haz clic en Enviar evento de
prueba. GrowWriter envía un pequeño ping firmado a tu endpoint. Tiene esta
pinta:
{
"version": "2026-09-06",
"event": "ping",
"deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
"sentAt": "2026-09-01T09:30:00.000Z"
}Fíjate en que no lleva ningún artículo. Así es como tu código distingue una prueba de una entrega real.
GrowWriter firma el ping exactamente igual que una entrega real y también lo
envía de la misma forma: hasta tres intentos, cada uno esperando 10 segundos
cualquier 2xx, así que un endpoint que nunca responde tarda alrededor de
medio minuto en fallar. Cuando tu endpoint responde, el diálogo muestra
Evento de prueba entregado. Haz clic en Hecho y ya estás conectado.

Si la prueba falla, el diálogo indica el motivo. La URL de tu endpoint
redirige. Introduce la dirección a la que redirige e inténtalo de nuevo
significa que tu dirección manda a GrowWriter a otro sitio, y el mensaje
muestra la dirección que debes usar en su lugar. Tu endpoint
rechazó la entrega cita el estado que devolvió tu endpoint y los primeros
200 caracteres del cuerpo de su respuesta, así que un 404 o un 500 con un
mensaje de error corto se explica solo. La entrega falló tras varios
intentos significa que GrowWriter no pudo conectar en absoluto: la
dirección no es pública o el sitio está caído. Cada mensaje tiene su entrada
en la página de solución de problemas.
Una prueba fallida nunca rompe la conexión, así que puedes reintentar tantas
veces como quieras.
Publica un artículo
Abre uno de tus artículos terminados y publícalo. Elige tu webhook como
canal y GrowWriter entregará el artículo completo a tu endpoint como un
evento article.published.

Si vuelves a publicar el mismo artículo más adelante (tras editarlo, por
ejemplo), tu endpoint recibirá un evento article.updated con el mismo id
de artículo. Ese id es lo que permite a tu sitio actualizar la entrada
existente en lugar de crear un duplicado. "Actualizado" solo significa que
una publicación anterior a este webhook tuvo éxito; una segunda conexión de
webhook recibe article.published otra vez, porque nunca ha visto el
artículo.
Si más adelante eliminas el artículo en GrowWriter, o desconectas el webhook,
tu endpoint recibe article.unpublished con solo el id, el slug y el
language del artículo. Ocultar o eliminar la entrada es decisión tuya; los
ejemplos de abajo lo registran en el log.
Ejemplos de código
Elige la pestaña que encaje con tu stack. Cada ejemplo verifica la firma (los cuatro pasos de ¿Por qué la firma y el secreto?), responde al ping de prueba, se salta las entregas que ya ha procesado y pasa el artículo a una función de guardado con la forma que espera un CMS. Los ejemplos de Next.js y Express registran la entrada en el log, porque los hosts serverless dan a tu código un sistema de archivos de solo lectura; los ejemplos de Flask, PHP, Rails y Go escriben un archivo JSON por artículo. Cambia cualquiera de los dos por tu propia llamada a base de datos. Los motivos de cada línea están en Guarda el artículo en tu CMS.
Dos detalles que verás en el código. Los ejemplos rechazan cualquier entrega cuyo timestamp tenga más de cinco minutos: es la ventana contra repeticiones de la sección de arriba, lo bastante estricta para inutilizar las repeticiones y lo bastante holgada para absorber el desfase de reloj entre servidores. Y comparan las firmas en tiempo constante: la comparación tarda lo mismo tanto si un intento se acerca como si va completamente desencaminado, de modo que un atacante no puede aprender nada midiendo los tiempos de respuesta.
Hay una regla que importa más que todas las demás: verifica la firma sobre el cuerpo de la petición sin procesar, exactamente como llegó. Si tu framework parsea el JSON primero y tú lo vuelves a serializar, los bytes cambian y la comprobación de la firma falla. Todos los ejemplos leen primero el cuerpo sin procesar exactamente por esta razón.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Antigüedad máxima en segundos de una entrega antes de rechazarla.
// Protege contra la repetición de una petición antigua capturada.
const MAX_AGE_SECONDS = 5 * 60;
// Ids de entrega que ya hemos terminado de procesar. Suficiente para un
// servidor; en producción usa una tabla en tu base de datos. Si tu host
// ejecuta tu código como funciones de corta vida (serverless), esta lista
// no sobrevive entre peticiones.
const processedDeliveries = new Set<string>();
function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
// La cabecera tiene esta forma: t=1756718400,v1=5257a869e7...
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('='))
);
// 1. El timestamp debe ser reciente. GrowWriter vuelve a firmar cada
// reintento, así que una entrega fresca siempre trae un timestamp fresco.
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < MAX_AGE_SECONDS)) {
return false;
}
// 2. Recalcula la firma: HMAC-SHA256 de "timestamp.cuerpoSinProcesar",
// con tu secreto de firma COMPLETO como clave (prefijo whsec_ incluido).
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
// 3. Compara en tiempo constante para que la comprobación no se pueda medir.
const provided = Buffer.from(parts.v1 ?? '', 'hex');
const wanted = Buffer.from(expected, 'hex');
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}
// Convierte el payload a la forma que espera un CMS. Cada línea aplica una
// de las seis reglas de "Guarda el artículo en tu CMS", más abajo.
function toPost(article: any) {
const images = [article.coverImage, ...article.images].filter(Boolean);
return {
growwriter_id: article.id, // regla 1: un campo propio, nunca una clave suelta
title: article.title,
slug: article.slug,
summary: article.summary ?? '', // se omite del payload si no hay valor
body: article.content.markdown, // regla 6: escapa si lo compilas como MDX
published_at: article.dates.firstPublishedAt.slice(0, 10), // regla 2: YYYY-MM-DD
updated_at: article.dates.modifiedAt.slice(0, 10),
status: 'published', // regla 3: el payload no tiene indicador de publicado
seo_keyword: article.keyword ?? '', // regla 4: la palabra clave es un campo SEO, no una etiqueta
faq: article.relatedQuestions, // regla 4: material para FAQ, no etiquetas
image_urls: images.map((image: any) => image.url), // regla 5: descarga y redimensiona
};
}
// Guarda la entrada en tu base de datos o CMS. Este ejemplo solo la
// registra en el log, a propósito: Vercel y la mayoría de los hosts
// serverless montan el directorio del código en solo lectura, así que
// escribir un archivo aquí haría fallar la primera entrega real con un 500.
// Guardes lo que guardes, usa article.id como clave: cuando el mismo
// artículo se publica de nuevo ("article.updated") sobrescribes el mismo
// registro. Ese crear-o-actualizar es todo lo que significa "upsert".
// Devuelve la URL pública de la entrada para que el handler pueda
// pasársela a GrowWriter.
async function saveArticle(article: any, event: string) {
const post = toPost(article);
const verb = event === 'article.published' ? 'Created' : 'Updated';
console.log(`${verb} ${post.growwriter_id}: ${post.title}`);
return `https://www.example.com/blog/${post.slug}`;
}
export async function POST(request: Request) {
// Lee el cuerpo como texto sin procesar PRIMERO. No uses request.json()
// antes de verificar: la firma cubre exactamente estos bytes.
const rawBody = await request.text();
const signature = request.headers.get('x-growwriter-signature') ?? '';
const secret = process.env.GROWWRITER_WEBHOOK_SECRET ?? '';
if (!verifySignature(rawBody, signature, secret)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
// El botón "Enviar evento de prueba" manda un ping sin artículo.
// Responder 200 aquí es lo que hace que la prueba tenga éxito.
if (payload.event === 'ping') {
return new Response('pong', { status: 200 });
}
// Eliminar el artículo en GrowWriter (o desconectar el webhook) manda
// article.unpublished con solo el id, el slug y el idioma. Oculta o elimina
// tu entrada aquí; este ejemplo solo lo registra en el log.
if (payload.event === 'article.unpublished') {
console.log(`Unpublished ${payload.article.id}`);
return new Response('OK', { status: 200 });
}
// Los reintentos reutilizan el mismo id de entrega. Si este id ya está
// hecho, el artículo está guardado: responde OK otra vez y sáltate el trabajo.
const deliveryId = request.headers.get('x-growwriter-delivery') ?? '';
if (processedDeliveries.has(deliveryId)) {
return new Response('OK', { status: 200 });
}
// payload.event dice de qué caso se trata: "article.published" para un
// artículo nuevo, "article.updated" cuando vuelve un id que ya has visto.
const postUrl = await saveArticle(payload.article, payload.event);
processedDeliveries.add(deliveryId);
// GrowWriter espera como máximo 10 segundos por intento. Una escritura
// en base de datos entra de sobra; si tu guardado es más lento, mira
// "¿Y el límite de 10 segundos?" más abajo. Devolver la URL de la entrada
// es opcional: le da al diálogo de publicación un enlace "Ver
// publicación". Un 'OK' a secas vale si aún no conoces la URL.
return Response.json({ url: postUrl }, { status: 200 });
}¿Usas otro stack?
Los ejemplos se traducen a cualquier lenguaje que pueda calcular un HMAC. El único paso en el que la gente se equivoca es el primero: verifica la firma sobre el cuerpo de la petición sin procesar, exactamente como llegó. Aquí tienes dónde encontrarlo en otros stacks comunes:
| Stack | Cuerpo sin procesar |
|---|---|
| Django | request.body |
| Laravel | $request->getContent() |
| Sinatra | request.body.read |
| Cloudflare Workers | await request.text() |
| Deno / Bun | await request.text() |
¿Y el límite de 10 segundos?
GrowWriter espera hasta 10 segundos por intento a que respondas. Guardar un registro JSON entra de sobra. Si tu endpoint hace trabajo más lento, como descargar todas las imágenes antes de guardar, tienes dos opciones:
- Apóyate en los reintentos (recomendado para endpoints sencillos). Deja
el handler exactamente como está: crea o actualiza por
article.idy se salta los ids de entrega ya terminados, así que ejecutarlo dos veces es inofensivo. Si el primer intento agota el tiempo, tu servidor sigue trabajando. El reintento encuentra el trabajo hecho, o lo repite, y responde dentro del plazo. No se pierde nada y no escribes código extra. - Responde primero, trabaja después. Guarda el payload sin procesar,
responde
200inmediatamente y procésalo en un trabajo en segundo plano. Así siempre le ganas al reloj, pero conoce el trato: en cuanto respondes200, la entrega cuenta como exitosa. Si después falla tu trabajo en segundo plano, GrowWriter no reintentará, y la única forma de recibir el artículo otra vez es volver a publicarlo. Y como la entrada todavía no existe cuando respondes, no puedes devolver su URL, así que el diálogo de publicación no muestra ningún enlace Ver publicación para esa publicación.
Guarda el artículo en tu CMS
El payload tiene la forma de GrowWriter. Tu CMS, tu base de datos o tu generador de sitios estáticos tiene su propia forma, y la mayoría la comprueban: un esquema es la lista de campos que admite un tipo de contenido y el formato que acepta cada uno. El código de arriba traduce entre las dos formas. Esta sección explica las seis reglas que hay detrás, para que puedas adaptarlas a tu configuración.
Antes de tu primera entrega, abre el panel de administración de tu CMS y responde cinco preguntas:
- ¿Dónde vivirá el id de GrowWriter?
- ¿Qué formato aceptan tus campos de fecha?
- ¿Qué campo marca una entrada como publicada, y cuál es su valor por defecto para el contenido nuevo?
- ¿Dónde deberían ir las palabras clave, si es que van a algún sitio?
- ¿Dónde se guardarán las imágenes?
Regla 1: dale al id de GrowWriter su propio campo
Qué. article.id son 25 letras minúsculas y dígitos, y empieza por c.
Es seguro en URLs y nombres de archivo y nunca cambia.
Por qué. La mayoría de los CMS rechazan los campos que no declaran.
Escribir el id como una clave suelta junto al título rompe la entrada en el
panel de administración, aunque el sitio siga renderizándola (el error
reportado tenía esta pinta:
Key on object value "growwriterId" is not allowed).
Haz esto. Añade un campo propio como growwriter_id a tu tipo de
contenido antes de la primera entrega, o mantén una tabla aparte que
relacione los ids con tus entradas. Nombra los archivos por el id, nunca por
el slug.
Regla 2: convierte las fechas
Qué. Todas las fechas son ISO 8601 en UTC con milisegundos, como
2026-09-02T11:29:14.967Z.
Por qué. Los campos de fecha sin hora de un CMS aceptan YYYY-MM-DD y
rechazan un timestamp completo.
Haz esto. Toma los diez primeros caracteres para los campos de fecha sin
hora. Usa dates.firstPublishedAt como fecha de publicación y
dates.modifiedAt como fecha de actualización. modifiedAt es la hora de la
petición de publicación, no la de la última edición, así que nunca compares
las dos fechas para detectar una actualización: el event te lo dice.
Regla 3: establece tú mismo el estado de publicación
Qué. El payload no tiene ningún indicador de publicado o borrador. El evento es el indicador, y desaparece en cuanto termina la petición.
Por qué. Muchos CMS tratan el contenido nuevo como borrador por defecto. Una entrada que estaba en línea porque tu build ignoraba ese valor por defecto se cae la primera vez que alguien la guarda en el panel de administración.
Haz esto. Escribe tu estado de publicación de forma explícita
(status: "published", draft: false o lo que use tu CMS) tanto en
article.published como en article.updated. Cuando llegue
article.unpublished (el artículo se eliminó en GrowWriter, o el webhook se
desconectó), revierte ese estado o elimina la entrada; el payload solo lleva
el id, el slug y el idioma, así que la decisión es tuya.
Regla 4: la palabra clave y las preguntas no son etiquetas
Qué. article.keyword es el único término de búsqueda para el que se
escribió el artículo, tal como lo tecleó el autor. Puede no estar.
article.relatedQuestions es la lista de preguntas que Google muestra como
"otras preguntas de los usuarios" para esa palabra clave, en el idioma del
artículo; siempre está presente y puede estar vacía.
Por qué. Un término de búsqueda no es una etiqueta, y una pregunta no es una categoría. Renderizados como etiquetas ponen frases de búsqueda en tus páginas públicas; usadas como FAQ bajo el artículo, las preguntas se ganan su sitio.
Haz esto. Guarda la palabra clave para tus herramientas SEO. Muestra las
preguntas como un bloque de FAQ, o como datos estructurados FAQPage si tu
renderizador lo admite. Si quieres etiquetas, decídelas en tu CMS.
Regla 5: descarga y optimiza las imágenes
Qué. Las URLs de las imágenes apuntan al almacenamiento público de GrowWriter. Hoy por hoy no caducan, pero GrowWriter no es un alojamiento de imágenes a largo plazo. Los archivos llegan tal como se obtuvieron, a su resolución original, y una sola imagen puede superar los 10 MB.
Por qué. Subir al repositorio o servir originales de varios megabytes ralentiza cada página.
Haz esto. Descarga cada imagen (portada e interiores), redimensiónala al
ancho máximo que necesite tu diseño, comprímela y guárdala en tu lado.
Después reescribe las URLs en el formato de contenido que guardes. Dos cosas
te lo ponen fácil: las imágenes que no cambian conservan su URL exacta entre
actualizaciones, así que puedes saltarte todo lo que ya tengas replicado, y
las mismas URLs aparecen dentro del contenido. Nombra tus archivos a partir de
article.id y la posición de la imagen; images[].filename es opcional y
no tiene extensión, y sectionSlug solo indica para qué sección del esquema
se creó la imagen, así que no lo parsees ni lo uses como clave.
let html = article.content.html;
for (const image of [article.coverImage, ...article.images].filter(Boolean)) {
const localUrl = await mirrorImage(image.url); // descarga y guarda en tu almacenamiento
html = html.replaceAll(image.url, localUrl);
}Regla 6: escapa para tu renderizador
Qué. La prosa normal contiene caracteres como {, } y <.
Por qué. MDX, JSX y los motores de plantillas los leen como código, y el build falla con el primer artículo que los contenga.
Haz esto. Guarda el markdown como datos y renderízalo con un renderizador
de Markdown, o escapa esos caracteres antes de compilar. content.html ya
viene escapado y usa un conjunto fijo de etiquetas, así que es la opción
segura para insertarlo directamente.
Cómo gestionar actualizaciones y traducciones
- Actualizaciones: mismo
id, sobrescribe la entrada. Editar el título conserva el slug; solo elegir un título nuevo en el writer produce uno nuevo. Cuando cambie, actualiza la dirección de la entrada y plantéate redirigir la antigua. Las imágenes sustituidas reciben URLs nuevas. Trata la última entrega como la autoridad para todos los campos. - Traducciones: cada idioma es un artículo aparte con su propio
id. GrowWriter no los enlaza. Empareja los idiomas con tus propios datos, usandoarticle.language(un código de dos letras comoes).
Pruébalo en local
No necesitas publicar un artículo real para poner a prueba tu endpoint. Este
snippet firma un ping de prueba exactamente igual que GrowWriter, usando
openssl, y lo envía a tu servidor local con curl:
# Usa el mismo secreto que tu servidor local tiene en GROWWRITER_WEBHOOK_SECRET.
SECRET="whsec_your_secret_here"
BODY='{"version":"2026-09-06","event":"ping","deliveryId":"9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e","sentAt":"2026-09-01T09:30:00.000Z"}'
# Firma "timestamp.cuerpo" con HMAC-SHA256, exactamente como GrowWriter.
TIMESTAMP=$(date +%s)
SIGNATURE=$(printf '%s.%s' "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST "http://localhost:3000/webhooks/growwriter" \
-H "Content-Type: application/json" \
-H "X-GrowWriter-Event: ping" \
-H "X-GrowWriter-Delivery: 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e" \
-H "X-GrowWriter-Signature: t=$TIMESTAMP,v1=$SIGNATURE" \
--data "$BODY"Ajusta el puerto y la ruta a donde se ejecute tu servidor. Un endpoint que
funciona responde pong con un 200. Ahora cambia un solo carácter dentro
de BODY y ejecútalo otra vez: deberías recibir un 401, lo que demuestra
que tu comprobación de firma comprueba de verdad. Para ensayar un artículo
completo, sustituye BODY por el
payload de ejemplo completo
(en una sola línea) y pon article.published en las dos cabeceras de evento.
Para recibir eventos reales de GrowWriter en tu máquina, recuerda que la
URL del endpoint debe ser pública, así que localhost no funcionará. Un
servicio de túnel como ngrok o cloudflared le da a tu servidor local una
dirección HTTPS pública temporal; úsala como URL del endpoint mientras
desarrollas y cambia a la real después.
Qué contiene el payload
Cada entrega es un POST con Content-Type: application/json, tres
cabeceras X-GrowWriter-* (nombre del evento, id de entrega, firma) y un
cuerpo JSON que lleva el artículo en tres formatos. La
referencia del payload es el
contrato: cada cabecera y cada campo con su tipo, si está siempre presente,
su formato exacto y el
ejemplo completo.
Los campos que hacen el trabajo importante:
| Campo | Cómo usarlo |
|---|---|
article.id | Tu clave estable. Guárdala en un campo propio y actualiza la entrada existente cuando vuelva el mismo id. |
article.slug | Dato de presentación para la dirección de la entrada. Cambia cuando se elige un título nuevo en el writer, así que nunca lo uses como clave. |
article.language | Código de idioma de dos letras (en, es, ...) para dirigir las entradas en un sitio multilingüe. Cada traducción es un artículo aparte con su propio id. |
article.content | El artículo en tres formatos: markdown, html listo para usar y blocks estructurados. Usa el que encaje con tu sitio. |
article.dates | firstPublishedAt es tu fecha de publicación, modifiedAt tu fecha de actualización. Toma los diez primeros caracteres para los campos de fecha sin hora. |
article.coverImage y article.images | La portada más cada imagen del cuerpo en orden. Descárgalas y redimensiónalas. |
article.keyword y article.relatedQuestions | El término de búsqueda al que apunta el artículo, y las preguntas que Google asocia con él. Campo SEO y material para FAQ; ninguno de los dos es una etiqueta. |
article.brand | El nombre y la web de tu marca para el marcado SEO, nunca una persona. Asigna los autores en tu propia lógica de guardado. |
Pueden aparecer campos opcionales nuevos sin que la fecha de version
cambie, así que ignora los campos que no reconozcas. Los cambios
incompatibles cambian la fecha y se anuncian antes en el
registro de cambios; la política completa está en
Versionado.
Entrega y reintentos
Una publicación a una conexión es una entrega, y una entrega hace hasta
tres intentos (inmediatamente, y después 1 segundo y 3 segundos más tarde),
cada uno esperando hasta 10 segundos cualquier 2xx. Todos los intentos
comparten un mismo id de entrega y van firmados de nuevo, las redirecciones
nunca se siguen y GrowWriter no reintenta más tarde: tras el tercer fallo el
canal muestra un botón Reintentar en el diálogo de publicación, que
empieza una entrega nueva. Las reglas exactas, incluida la nota de "al menos
una vez" de la que depende tu deduplicación, están en la referencia:
Entrega.
Solución de problemas
Cada fallo tiene su propia entrada en la página de solución de problemas, organizada por lo que ves. Los más comunes en una primera conexión:
- El evento de prueba falla con HTTP 301, 302, 303, 307 o 308
- "Tu endpoint no respondió a tiempo"
- "Tu endpoint rechazó la entrega"
- La prueba funciona, pero el primer artículo falla con un 500 en Vercel o Netlify
- La prueba funciona pero los artículos reales fallan
- El artículo está en línea, pero el panel de mi CMS muestra un error de validación
- Eliminé un artículo en GrowWriter, pero sigue en mi sitio
¿Sigues atascado? Escríbenos desde la aplicación y lo resolvemos juntos.
Registro de cambios
- Versión actual:
2026-09-06. El payload descrito en la referencia es el primer contrato publicado. Los cambios futuros se listan aquí, lo más reciente primero: los cambios aditivos conservan la fecha de versión, las eliminaciones o los renombrados la cambian y se anuncian aquí antes de desplegarse.
Inicio rápido de webhooks
Conecta GrowWriter a tu propio sitio web y recibe tu primer evento de prueba firmado, en una sola página, con un solo archivo de código.
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.