Saltar al contenido principal
Desarrolladores Webhooks

Publica mensajes con un webhook

Un webhook es una URL que publica en tu comunidad. Envíale JSON desde cualquier cosa capaz de hacer una petición HTTP, como CI, monitorización, un cron job o un script, y el mensaje aparece en el canal.

Qué puedes hacer

  • Publica texto o una tarjetaTexto sin formato, o una tarjeta con título, color, markdown, campos e imágenes.
  • Adjunta archivosHasta cinco archivos por mensaje: logs, informes, capturas de pantalla.
  • Añade botonesEnlaces, o botones que cambian la tarjeta o llaman a tu servicio.
  • Cámbialo despuésLa respuesta incluye una callback URL para actualizar o borrar el mensaje.

En la app

$ curl -X POST "$MSSGS_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Mssgs-Signature: sha256=$SIG" \
-d '{"message_container": {"title": "Build #1847 superado", ...}}'
{"success": true, "message_id": "aZZ1a2b-..."}

Una petición desde CI, una tarjeta en #deploys. El nombre de arriba es el que le diste al webhook.

Inicio rápido

  1. Crea el webhook

    En la app de escritorio, abre Manage Server → Webhooks en tu comunidad, crea un webhook, elige los canales en los que puede publicar y copia la URL de un canal. Tiene esta forma:

    url
    https://mss.gs/api/v1/webhook/{webhook_guid}/{token}/{channel_guid}
  2. Envía un mensaje

    Firma el JSON con el secreto del webhook y envíalo con POST. Un webhook creado en la app de escritorio siempre tiene uno: cópialo de Webhook Secret en los ajustes del webhook.

    bash
    BODY='{"content": "Build #1847 passed on main"}'
    SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$MSSGS_WEBHOOK_SECRET" | sed 's/^.* //')
    
    curl -X POST "$MSSGS_WEBHOOK_URL" \
      -H "Content-Type: application/json" \
      -H "X-Mssgs-Signature: sha256=$SIG" \
      -d "$BODY"
  3. Lee la respuesta

    La respuesta te da el id del mensaje y una callback_url para cambiar el mensaje más tarde.

    json
    {
      "success": true,
      "message_id": "aZZ1a2b-...",
      "callback_url": "https://mss.gs/api/v1/trigger-callback/8f14e45f-...",
      "stream_url": "https://mss.gs/api/v1/instant/6b1e...c0?token=a1b2c3d4-..."
    }
Cualquiera que tenga la URL y el secreto puede publicar en ese canal. No los pongas en repositorios públicos ni en código del lado del cliente.

Qué puedes enviar

Un mensaje es o bien la forma corta (texto con un título y un color) o bien una tarjeta completa, y cualquiera de las dos puede llevar botones y archivos. El nombre de arriba de la tarjeta es siempre el nombre del propio webhook. En los ajustes del webhook también decides si puede publicar imágenes y mencionar a personas.

Forma corta

Suficiente para la mayoría de las alertas.

json
{
  "content": "Server backup completed at 03:00 UTC",
  "color": "green",
  "title": "Backup Bot"
}
CampoTipoQué hace
contentstringEl texto del mensaje. Obligatorio salvo que envíes una tarjeta o archivos.
colorstringblue (por defecto), green, orange, red, yellow o purple.
titlestringUn título encima del texto. Por defecto, el nombre del webhook.

Una tarjeta completa

Envía un message_container para una tarjeta con título enlazado, subtítulo, markdown, campos e imágenes. A través de un webhook, una tarjeta admite type, color, title, title_url, sub_title, description, fields, avatar_url, image_url, image_base64, images y los campos del loader. La píldora de estado, el distintivo, las estadísticas del diff y el razonamiento plegado son para respuestas a comandos. Todos los campos están en tarjetas de mensaje.

json
{
  "message_container": {
    "type": "embed_message",
    "color": "green",
    "title": "Build #1847 passed",
    "title_url": "https://ci.example.com/builds/1847",
    "description": "All 212 tests green on **main**.",
    "fields": [
      { "field": "Duration", "value": "2m 34s" },
      { "field": "Commit", "value": "1a2b3c4" }
    ]
  }
}
deploys
System
Message from CI

Build #1847 superado

Los 212 tests en verde en main.
Duration
2m 34s
Commit
1a2b3c4

Botones

Añade un array actions para poner botones bajo el mensaje. Cómo funcionan se explica en botones.

Archivos

Publica archivos de verdad con un mensaje: un log, un informe, una captura de pantalla. Se muestran como cualquier otro adjunto, como una fila de descarga o dentro del mensaje en el caso de imágenes, vídeo y audio. Un mensaje solo con archivos también vale: omite content y la tarjeta.

json
{
  "message_container": {
    "color": "orange",
    "title": "Log dump: ios",
    "description": "DMs stopped arriving after switching networks"
  },
  "attachments": [
    {
      "name": "mssgs-logs-20260803-141205.log",
      "content_base64": "MjAyNi0wOC0wMyAxNDoxMjowNSBbV1NdIGNvbm5lY3RlZAo=",
      "mime_type": "text/plain"
    }
  ]
}
CampoTipoQué hace
namestringEl nombre con el que se descarga el archivo. Obligatorio. Una ruta se reduce a su última parte.
content_base64stringLos bytes del archivo en base64, sin más o como URI data:. mssgs guarda el archivo y deja solo un enlace en el mensaje.
mime_typestringEl tipo de contenido de content_base64. Por defecto, text/plain.
urlstringUn archivo ya alojado en mssgs: una ruta /static/... o una URL https://mss.gs/....
Envía exactamente uno de los dos por archivo: content_base64 o url. Los dos a la vez, o ninguno, es un error.
LímiteValor
Archivos por mensaje5
Tamaño por archivo, una vez decodificado8 MB
Nombre de archivo200 caracteres
Petición completaUnos 10 MB. Base64 hace que un archivo ocupe un tercio más, así que un solo archivo de más de unos 7 MB no cabe.

Por qué url solo acepta direcciones de mssgs

Una URL de webhook suele acabar pegada en otros paneles. Si se filtra, no debe permitir que alguien haga que la app de cada miembro descargue un archivo de un servidor que haya elegido él. Si tu archivo está en otro sitio, envíalo como content_base64 y mssgs lo aloja.

Una subida fallida no hace fallar el mensaje

Los archivos se comprueban de antemano pero se suben después. Si una subida falla, ese archivo se queda fuera y el resto del mensaje se publica igualmente, sin error: mejor perder el archivo que el informe. Si un archivo es importante, comprueba que ha llegado.

Un archivo desde la línea de comandos

bash
BODY='{"attachments":[{"name":"report.csv","content_base64":"'"$(base64 < report.csv | tr -d '\n')"'","mime_type":"text/csv"}]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$MSSGS_WEBHOOK_SECRET" | sed 's/^.* //')

printf '%s' "$BODY" | curl -sS -X POST "$MSSGS_WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -H "X-Mssgs-Signature: sha256=$SIG" \
  --data-binary @-

Firmar las peticiones

Un webhook con secreto solo acepta peticiones que demuestren que lo conocen, y un webhook creado en la app de escritorio siempre tiene uno (Webhook Secret en sus ajustes). Firma el cuerpo sin procesar de la petición con HMAC-SHA256 usando el secreto, y envía el digest hexadecimal en minúsculas en la cabecera X-Mssgs-Signature como sha256=<hex>.

javascript
import crypto from 'node:crypto';

const body = JSON.stringify({ content: 'Deploy finished' });
const signature = crypto.createHmac('sha256', process.env.MSSGS_WEBHOOK_SECRET)
  .update(body)
  .digest('hex');

await fetch(process.env.MSSGS_WEBHOOK_URL, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Mssgs-Signature': `sha256=${signature}`
  },
  body
});
Una petición sin una firma válida recibe 401 y {"error": "INVALID_SIGNATURE"}. Solo un webhook sin secreto, como uno creado por MCP sin webhook_secret, acepta peticiones sin firmar.

También se acepta la cabecera propia de GitHub X-Hub-Signature-256, así que un webhook de GitHub con el mismo secreto funciona tal cual. La firma no lleva marca de tiempo, así que no impide que una petición interceptada se vuelva a enviar: el secreto que de verdad importa sigue siendo la URL.

Respuestas y errores

Un mensaje publicado vuelve con su id y una callback_url para actualizarlo o borrarlo durante 30 minutos.

json
{
  "success": true,
  "message_id": "aZZ1a2b-...",
  "callback_url": "https://mss.gs/api/v1/trigger-callback/8f14e45f-...",
  "stream_url": "https://mss.gs/api/v1/instant/6b1e...c0?token=a1b2c3d4-..."
}
Lee el cuerpo además del estado. Un payload rechazado lleva el motivo como código error en el cuerpo. Trata cualquier cuerpo con una clave error como un fallo, sea cual sea el código de estado.
http
HTTP/1.1 200 OK
Content-Type: application/json

{ "error": "ATTACHMENT_TOO_LARGE" }
javascript
const res = await fetch(webhookUrl, { method: 'POST', headers, body });
const reply = await res.json().catch(() => null);

// A rejected payload still comes back as 200: read the body.
if (!res.ok || (reply && reply.error)) {
  throw new Error(`webhook rejected: ${reply?.error ?? res.status}`);
}

Cuando el mensaje tiene botones, la respuesta incluye también una stream_url: un stream en directo de las respuestas, reacciones y pulsaciones de botón en ese mensaje, abierto durante 10 minutos, o una hora si envías "sse_event_extended_timeout": true. Consulta actualizaciones en directo.

Códigos de estado

EstadoCuándo
401El webhook tiene secreto y la firma falta o es incorrecta.
403Este webhook no puede publicar en ese canal.
404No hay ningún webhook en esta URL.
413La petición es demasiado grande.
429Demasiadas peticiones. Ve más despacio y vuelve a intentarlo.
502No se ha podido entregar el mensaje. Vuelve a intentarlo.

Códigos de error

CódigoSignificado
MISSING_CONTENTNada que publicar: ni texto, ni tarjeta, ni archivos.
INVALID_MESSAGE_CONTAINERmessage_container no es un objeto.
INVALID_MESSAGE_CONTAINER_TYPEEl tipo de tarjeta no es embed_message ni system_message.
MISSING_MESSAGE_CONTAINER_DESCRIPTIONUna tarjeta necesita una descripción, salvo que sea un loader.
MISSING_MESSAGE_CONTAINER_LOADER_TEXTUna tarjeta de loader necesita loader_text.
INVALID_WEBHOOK_BINDINGEste webhook no puede publicar en ese canal.
INVALID_SIGNATURELa cabecera de firma falta o es incorrecta.
REQUEST_BODY_TOO_LARGELa petición supera el límite de tamaño.
PUBLISH_FAILEDNo se ha podido entregar el mensaje.
INVALID_ACTIONS_FORMAT, INVALID_ACTION_MISSING_FIELDS, DUPLICATE_ACTION_ID, INVALID_TRIGGERS_FORMAT, INVALID_TRIGGER_MISSING_ACTION, INVALID_TRIGGER_ACTION_NOT_ALLOWEDAlgo falla en un botón. Consulta botones.

Errores de archivos

CódigoSignificado
INVALID_ATTACHMENTS_FORMATattachments no es una lista, o una entrada no es un objeto.
TOO_MANY_ATTACHMENTSMás de cinco archivos.
MISSING_ATTACHMENT_NAMEUn archivo no tiene nombre.
INVALID_ATTACHMENT_NAMEDel nombre no queda nada utilizable, como con ...
MISSING_ATTACHMENT_SOURCENi url ni content_base64.
AMBIGUOUS_ATTACHMENT_SOURCEurl y content_base64 a la vez.
INVALID_ATTACHMENT_BASE64El base64 no se puede decodificar.
ATTACHMENT_TOO_LARGEUn archivo supera los 8 MB una vez decodificado.
INVALID_ATTACHMENT_URLLa url no es una dirección de mssgs.

Límites

LímiteValor
Tamaño de la peticiónUnos 10 MB
Archivos por mensaje5, de hasta 8 MB cada uno
Descripción de la tarjetaHasta 50.000 bytes. A partir de 1.000 bytes, los miembros ven el principio y un botón Show more.
Actualizar el mensaje después30 minutos, mediante callback_url
Stream en directo de un mensaje con botones10 minutos, o una hora si lo pides

Las peticiones tienen un límite de frecuencia. Cuando recibas un 429, espera antes de volver a enviar, y agrupa en un solo mensaje las alertas que llegan en ráfagas.

GitHub, UniFi y App Store Connect

Apunta uno de estos servicios a una URL de webhook y mssgs lo reconoce y publica una tarjeta en condiciones, sin payload que escribir. Consulta integraciones. Estos reciben como respuesta {"success": true} y ninguna callback URL.

OrigenSe reconoce porQué publica
GitHubLa cabecera x-github-eventPushes, pull requests y revisiones, issues y comentarios, ramas y etiquetas, releases. Una ráfaga de cambios en una misma issue o pull request se agrupa en una sola tarjeta.
UniFi ProtectEl user agent protect-alarm-managerTimbrazos, movimiento, y personas, vehículos o paquetes detectados por tus cámaras.
App Store ConnectEl cuerpo de sus notificaciones o la cabecera x-apple-signatureNotificaciones de App Store Connect.

Sigue construyendo