Configurar puertas de aprobación de CI/CD con callbacks firmados
Pausa un pipeline en una puerta de despliegue, publica botones de aprobar/rechazar y verifica el callback firmado que recibe tu servidor — contrato completo, reintentos y timeouts.
Un pipeline de release que se pausa y le pregunta a una persona es un contrato de dos vías: Dailybot publica la solicitud de aprobación, y el webhook de tu sistema de CI recibe el clic. Si te equivocas en la verificación de firma o en el manejo de reintentos, terminas bloqueando despliegues por falsos negativos o — peor — confiando en una solicitud sin verificar. Este es el contrato exacto, de punta a punta.
La puerta: dos botones, una sola URL de callback
Publica la puerta como un mensaje interactivo con un botón de aprobar y uno de rechazar, ambos apuntando al mismo callback_url, ambos llevando un token bearer vía callback_auth para que tu webhook pueda autenticar el transporte además de la firma obligatoria:
curl -sS -X POST 'https://api.dailybot.com/v1/send-message/' \
-H 'X-API-KEY: $DAILYBOT_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"message": "Deploy 4.12.0 to production — approve?",
"target_channels": ["C0123456789"],
"buttons": [
{"label": "Approve", "button_type": "interactive", "value": "approved",
"callback_url": "https://ci.example.com/hooks/deploy",
"callback_auth": {"type": "bearer", "token": "$DEPLOY_CALLBACK_TOKEN"},
"response": {"message": "Approved — deploying now."}},
{"label": "Reject", "button_type": "interactive", "value": "rejected",
"callback_url": "https://ci.example.com/hooks/deploy",
"callback_auth": {"type": "bearer", "token": "$DEPLOY_CALLBACK_TOKEN"},
"response": {"message": "Rejected."}}
]
}'
El atajo de CLI para exactamente este patrón:
dailybot chat send -c C0123456789 -m "Deploy 4.12.0 to production — approve?" \
--approve-button "Approve=approved" \
--reject-button "Reject=rejected" \
--callback-url "https://ci.example.com/hooks/deploy" \
--callback-bearer "$DEPLOY_CALLBACK_TOKEN"
--approve-button / --reject-button reciben "Label=value" (separador de signo igual); --callback-url y --callback-bearer aplican a ambos. Prefiere --callback-bearer "$TOKEN" en lugar de un token literal para que nada quede en el historial de la shell ni en las listas de procesos. Nota el response inmediato en la versión con curl: se dispara en paralelo con el POST saliente, sin depender de la respuesta de tu webhook — el acuse de recibo es instantáneo, y la decisión real de despliegue ocurre de tu lado, de forma asíncrona (el patrón de aprobación lenta). Si necesitas actualizar el mensaje después de que tu pipeline termine, edítalo más tarde volviendo a hacer POST con el mismo bot_message_id dentro de las 72 horas.
Lo que recibe tu webhook
Dailybot hace POST con exactamente esta forma a callback_url al hacer clic:
POST https://ci.example.com/hooks/deploy
Content-Type: application/json
User-Agent: Dailybot-Chatbot/1.0
X-Dailybot-Event: button_click
X-Dailybot-Delivery: 5e2d1a9c-...-uuid
X-Dailybot-Timestamp: 1782001872
X-Dailybot-Signature: t=1782001872, v1=6d9a3e...hex_hmac
Authorization: Bearer $DEPLOY_CALLBACK_TOKEN
{
"event": "button_click",
"bot_message_id": "$db/ae007b43-dde2-4fa9-bce3-71fb0975a249",
"button": {"id": "$btn/...", "value": "approved"},
"user": {"id": "...", "email": "...", "display_name": "...", "external_id": "U01ABCDEFG"},
"organization": {"id": "...", "name": "..."},
"platform": "slack",
"channel": {"id": "C0123456789", "type": "channel"},
"modal_fields": null,
"metadata": {},
"sent_at": "2026-07-22T14:30:00Z",
"clicked_at": "2026-07-22T14:31:12Z"
}
Lee button.value ("approved" o "rejected") para decidir qué hace tu pipeline a continuación; usa user.display_name / user.email para atribuir la decisión en tu log de despliegue; bot_message_id te permite editar el mensaje original después para reflejar el resultado.
Verificar la firma — nunca te saltes esto
X-Dailybot-Signature es HMAC-SHA256 calculado sobre el string ASCII "{unix_timestamp}.{raw_body}" usando el secreto de firma de callbacks de tu organización (generado automáticamente la primera vez que envías un mensaje con callback_url; consíguelo con el admin de tu organización — nunca lo devuelve la API pública). Verifícala en cada request, tengas o no callback_auth:
const crypto = require('crypto');
function verify(rawBody, sigHeader, secretB64) {
const secret = Buffer.from(secretB64, 'base64url');
const parts = Object.fromEntries(sigHeader.split(',').map(s => s.trim().split('=')));
const ts = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5-minute replay window
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
}
Lee el cuerpo raw del request — antes de cualquier parseo JSON — para calcular la firma; parsear primero y volver a serializar producirá un string de bytes distinto y una discrepancia de firma falsa.
callback_auth — aditivo, no un reemplazo
callback_auth solo es válido junto con callback_url (400 button_callback_auth_invalid en cualquier otra combinación). Tres tipos, cada uno con sus propios campos:
type |
Campos | Se envía como |
|---|---|---|
bearer |
token (≤ 4096) |
Authorization: Bearer <token> |
basic |
username, password |
Authorization: Basic <base64> |
custom_header |
header_name, header_value |
<header_name>: <header_value> |
header_name debe ser un token válido según RFC 7230 y no puede ser host, content-length, content-type, transfer-encoding, connection, user-agent, ni nada que empiece con x-dailybot-. Las credenciales son solo de escritura — nunca se devuelven en ninguna API de lectura, nunca se registran en logs. Trata callback_auth como un segundo candado en la puerta que tu gateway ya exige, no como un sustituto de la verificación HMAC de arriba.
Comportamiento de reintentos y timeouts
Dailybot reintenta el POST saliente del callback una vez, con un backoff de 500 ms, ante una respuesta 5xx, 429, o un error de red. Un 2xx exitoso completa el envío. Cualquier otro 4xx se trata como terminal y no se reintenta — si tu webhook valida el payload y lo encuentra malformado, devuelve 4xx deliberadamente para evitar que Dailybot reintente un request que nunca va a funcionar. Diseña tu endpoint para:
- Verificar la firma primero, devolver
401/403de inmediato si falla (terminal, sin reintento). - Validar la forma del payload, devolver
400ante un input malformado (terminal, sin reintento). - Persistir lo suficiente para terminar de procesar de forma asíncrona, y luego devolver
2xxrápido — no dejes que la lógica del pipeline en sí misma bloquee la respuesta. - Usar
X-Dailybot-Deliverycomo clave de idempotencia: si el mismo delivery id llega dos veces (el único reintento, o un doble clic del lado del cliente antes de quedestroy_buttondeshabilite el botón), no dispares el despliegue dos veces.
Referencias cruzadas
- La API send-message, de punta a punta para el contrato completo de botones/callbacks sobre el que se construye esta puerta.
/es/developers/api/messaging#send-messagepara la referencia estructurada de campos./es/developers/clipara el conjunto completo de flags de CLI.
Explora el hub de Soluciones para el recorrido orientado a producto de este mismo flujo, con la demo interactiva de chat simulado.
FAQ
- ¿Cómo condiciono un despliegue al clic de una persona desde Dailybot?
- Envía un mensaje con dos botones interactivos que apunten ambos callback_url a tu webhook de CI y callback_auth para un token bearer. Tu pipeline espera el POST firmado que recibe tu webhook, lee button.value (por ejemplo, "approved" o "rejected"), y reanuda o cancela según corresponda.
- ¿Cómo verifico que el POST del callback realmente vino de Dailybot y no de una solicitud falsificada?
- Todo POST a callback_url lleva X-Dailybot-Signature: t={timestamp}, v1={hex_hmac}. Recalcula el HMAC-SHA256 sobre "{timestamp}.{raw_body}" con el secreto de firma de callbacks de tu organización, compáralo en tiempo constante, y rechaza cualquier cosa con más de 5 minutos de antigüedad.
- ¿Qué pasa si mi endpoint de callback está caído brevemente cuando alguien hace clic en aprobar?
- Dailybot reintenta el POST saliente una vez con un backoff de 500 ms ante respuestas 5xx, 429, o errores de red. Un 2xx exitoso completa el envío; cualquier otro 4xx se trata como terminal y se descarta — así que un 4xx de tu endpoint no se reintentará.
- ¿Puede callback_auth reemplazar la verificación de firma?
- No. callback_auth (bearer, basic o custom_header) es una autenticación de transporte estática adicional para gateways que la requieren — no reemplaza la firma HMAC, que siempre está presente y siempre debe verificarse.
- ¿callback_auth es válido en todos los tipos de botón?
- No — callback_auth solo es válido junto con callback_url. Configurarlo junto con callback_form, callback_command, callback_prompt o callback_workflow devuelve 400 button_callback_auth_invalid.