La API send-message a fondo: botones, callbacks y modales
Una guía de implementación completa de POST /v1/send-message/ — anatomía del request, hilos, identidad, los cinco tipos de callback, modal_body, códigos de error y los flags de CLI que los generan.
POST /v1/send-message/ es un único endpoint que hace cinco trabajos: envía un mensaje, encadena un reporte en hilo, suplanta una identidad, edita un envío anterior y — la parte que la mayoría de equipos subutiliza — convierte el mensaje en una pequeña pieza de UI. Esta guía es la versión a nivel de implementación del caso de uso insignia de automatización de chat: cada campo, cada tipo de callback, cada código de error y los flags de CLI que arman el mismo payload sin escribir JSON a mano.
Anatomía del request
El cuerpo del request se agrupa en seis frentes. Rara vez usarás todos en una sola llamada, pero toda integración en producción termina tocando la mayoría a lo largo de su vida.
| Grupo | Campos |
|---|---|
| Targeting | target_users, target_channels, target_teams, skip_users_on_time_off |
| Contenido | message, messages, image_url, metadata |
| Hilos | bot_message_id, thread_responses |
| Identidad | send_as_user, platform_settings |
| Botones | buttons (arreglo de objetos Button) |
| Configuración de plataforma | platform_settings (overrides exclusivos de Slack) |
Se requiere al menos uno de target_users / target_channels / target_teams (missing_targets si no). target_channels acepta un string simple con el id del canal, o un objeto { id, channel_type?, thread? } — usa thread para responder dentro de un hilo existente de la plataforma en lugar de iniciar uno nuevo. Se requiere message (string) o messages (arreglo de payloads por plataforma).
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 finished","target_channels":["C0123456789"]}'
La respuesta es {"bot_message_id": "$db/<uuid>"} — un id generado por el servidor que guardas para editar después, o el valor que tú mismo enviaste en el request para controlarlo.
Hilos en una sola llamada
thread_responses publica un mensaje padre más hasta 10 respuestas, un nivel de profundidad, en un solo request — sin llamadas adicionales:
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": "Daily report — 2026-07-24",
"target_channels": ["C0123456789"],
"bot_message_id": "daily-report-2026-07-24",
"thread_responses": [
{"message": "alice: shipped the billing migration"},
{"message": "bob: on PTO, no updates"}
]
}'
La respuesta devuelve bot_message_id para el padre más un id por cada respuesta en thread_responses — cada uno editable de forma independiente después, con el mismo truco de reutilizar bot_message_id. El threading se renderiza nativamente en canales y DMs de Slack; en Microsoft Teams, Discord y Google Chat, las respuestas se anidan dentro de canales pero llegan planas en DMs (igual llegan, solo sin anidación visual).
Editando en el lugar
Vuelve a hacer POST con el mismo bot_message_id dentro de 72 horas para editar ese mensaje en lugar de enviar uno nuevo:
curl -sS -X POST 'https://api.dailybot.com/v1/send-message/' \
-H 'X-API-KEY: $DAILYBOT_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"target_channels":["C0123456789"],"bot_message_id":"daily-report-2026-07-24","message":"Daily report — 2026-07-24 (updated)"}'
Los overrides de identidad (bot_username, bot_icon_url, bot_icon_emoji) se ignoran al editar — la plataforma conserva la identidad original del mensaje. Los botones se preservan (round-trip): los existentes sobreviven la edición a menos que envíes nuevos flags de botones.
Identidad: bot personalizado vs. send-as-user
Dos formas independientes de cambiar de quién parece venir un mensaje:
- Identidad de bot personalizada (cualquier plan) —
platform_settings.bot_usernamemásbot_icon_urlobot_icon_emoji(solo Slack, requiere el scopechat:write.customize; sin él, Slack cae silenciosamente en la identidad de bot por defecto y la llamada igual devuelveok: true). send_as_user(solo Slack, solo admin) — publica con el nombre y avatar de Slack de un compañero real. El mensaje sigue saliendo por el token del bot de Dailybot, no por el del usuario.
Son mutuamente excluyentes (send_as_user_conflict). Un UUID inválido en send_as_user falla del lado del cliente; un UUID bien formado que no resuelve a un usuario devuelve send_as_user_not_found; un llamante que no es admin recibe 403 org_admin_required.
dailybot chat send -c C0123 -m "Deploying the hotfix now" \
--send-as-user 294bf2cc-e3c7-401d-a1d6-bf20aa64bb33
Los cinco tipos de callback
Cada botón interactivo lleva exactamente uno de cinco callbacks — combinar dos devuelve 400 button_callback_conflict:
| Callback | Dispara | ¿Firmado? |
|---|---|---|
callback_url |
Tu propio endpoint HTTPS | Sí — HMAC + callback_auth opcional |
callback_form |
Abre un formulario interno de Dailybot | No — totalmente interno |
callback_command |
Ejecuta un comando ChatOps conocido como quien hizo clic | No — totalmente interno |
callback_prompt |
Envía un prompt de texto libre a la IA de Dailybot como quien hizo clic | No — totalmente interno |
callback_workflow |
Dispara un workflow api_trigger como quien hizo clic |
No — totalmente interno |
callback_url — tu propio servidor
{"label": "Approve", "button_type": "interactive", "value": "approve",
"callback_url": "https://ci.example.com/hooks/deploy",
"callback_auth": {"type": "bearer", "token": "$DEPLOY_CALLBACK_TOKEN"}}
Al hacer clic, Dailybot hace POST de un cuerpo firmado a tu callback_url:
{
"event": "button_click",
"bot_message_id": "$db/ae007b43-...",
"button": {"id": "$btn/...", "value": "approve"},
"user": {"id": "...", "email": "...", "display_name": "...", "external_id": "U01ABCDEFG"},
"organization": {"id": "...", "name": "..."},
"platform": "slack",
"channel": {"id": "D0123456", "type": "im"},
"modal_fields": null,
"metadata": {"campaign": "coffee-approvals"},
"sent_at": "2026-07-22T14:30:00Z",
"clicked_at": "2026-07-22T14:31:12Z"
}
platform es uno de slack, msteams, discord o hangouts — Google Chat se reporta como hangouts, compara con ese valor. callback_auth (bearer / basic / custom_header) es auth de transporte estática adicional, válida solo con callback_url; la firma HMAC siempre está activa sin importar eso.
callback_form — abrir un formulario interno
{"label": "Submit expense", "button_type": "interactive", "value": "expense_form",
"callback_form": "$EXPENSE_FORM_UUID"}
Solo UUID — nombres y slugs se rechazan. Formulario desconocido/archivado → 400 button_callback_form_not_found. No se dispara ningún request externo; el propio ciclo de vida del formulario (ver la guía de formularios) toma el control desde ahí.
callback_command — ejecutar un comando ChatOps
{"label": "Run help", "button_type": "interactive", "value": "help",
"callback_command": "help"}
Se ejecuta como el usuario que hizo clic (sus permisos, su cuota) — máximo 200 caracteres. El prefijo legado "prompt: …" se rechaza (400 button_callback_command_invalid); usa callback_prompt en su lugar.
callback_prompt — pasarle el clic a la IA
{"label": "Summarize thread", "button_type": "interactive", "value": "summarize",
"callback_prompt": "Summarize the last 20 messages in this channel."}
Vacío o demasiado largo (> 2000 caracteres) → 400 button_callback_prompt_invalid.
callback_workflow — disparar un workflow
{"label": "Trigger rollback", "button_type": "interactive", "value": "rollback",
"callback_workflow": "$ROLLBACK_WORKFLOW_UUID"}
Solo son elegibles los workflows con tipo de evento api_trigger — encuéntralos con dailybot workflow list --filter api_trigger. UUID desconocido/inactivo/de otra organización → 400 button_callback_workflow_not_found. Los pasos del workflow disparado leen el contexto del clic vía {{trigger.*}}: {{trigger.button_value}}, {{trigger.button_id}}, {{trigger.fields.<name>}} (inputs del modal), {{trigger.user.*}} y más — referencia completa en /es/developers/api/workflows.
modal_body — recolectar información antes de que dispare el callback
Adjunta modal_body a un botón interactivo para abrir primero un modal ligero. Solo existen tres tipos de bloque: text, input, divider — de 1 a 10 bloques, tamaño serializado ≤ 8 KiB.
{
"title": "Log an update",
"submit_label": "Save",
"blocks": [
{"type": "text", "text": "Share a quick status update."},
{"type": "input", "name": "update_text", "label": "Update", "multiline": true, "required": true},
{"type": "divider"}
]
}
name debe coincidir con ^[a-z][a-z0-9_]{0,62}$ y ser único en el modal — los valores enviados llegan como modal_fields.<name> en callback_url, o {{trigger.fields.<name>}} en callback_workflow. Un modal con bloques input requiere callback_url o callback_workflow (input_without_callback); un modal solo de visualización (text/divider) no requiere ninguno.
response — auto-respuestas instantáneas
Cualquier botón puede llevar un objeto response, mostrado a quien hizo clic de inmediato (en paralelo con el dispatch a callback_url, nunca condicionado a su resultado — el patrón canónico de aprobación lenta):
{"message": "Approved — deploying now.", "buttons": [], "replace_original": false, "ephemeral": false}
response.buttons es recursivo (mismo esquema Button), con un límite de profundidad de anidación de 3, 25 botones por nivel, 16 KiB por botón de nivel superior.
Códigos de error de un vistazo
code |
Significado |
|---|---|
button_link_and_callback_conflict |
Un botón link mezcló un callback o value junto con url |
button_callback_conflict |
Más de uno de los cinco callbacks en el mismo botón |
buttons_count_out_of_range |
Más de 25 botones en un mensaje |
button_modal_body_invalid |
Tipo de bloque inválido, ≥ 11 bloques, o un modal con inputs sin callback |
button_callback_auth_invalid |
callback_auth configurado sin callback_url, o malformado |
send_as_user_conflict |
send_as_user combinado con bot_username/bot_icon_* |
Flags de CLI: los mismos payloads sin JSON escrito a mano
# Flujo de aprobación
dailybot chat send -c C0123456789 -m "Deploy 4.12.0 to production?" \
--approve-button "Approve=approved" --reject-button "Reject=rejected" \
--callback-url "https://ci.example.com/hooks/deploy" \
--callback-bearer "$DEPLOY_CALLBACK_TOKEN"
# Botón que dispara un workflow
dailybot chat send -c C0123456789 -m "Ready to redeploy staging?" \
--workflow-button "Redeploy staging=$REDEPLOY_WORKFLOW_UUID"
# Cualquier cosa que los flags abreviados no puedan expresar — pass-through completo de JSON
dailybot chat send -c C0123456789 -m "Log a quick update" \
--buttons '[{"label":"Add update","button_type":"interactive","value":"log_update",
"callback_url":"https://ci.example.com/hooks/log-update",
"modal_body":{"title":"Log an update","submit_label":"Save",
"blocks":[{"type":"input","name":"update_text","label":"Update","multiline":true,"required":true}]}}]'
--buttons reenvía las claves sin tocarlas, incluyendo campos futuros que los flags abreviados aún no exponen — es la vía de escape para todo lo que este artículo muestra como JSON crudo arriba.
Todo esto mapea 1:1 con la demo interactiva y las recetas copiar-y-pegar del caso de uso de automatización de chat — lee esa página para la versión orientada a producto del mismo flujo, o vuelve aquí cuando necesites el campo exacto, el código de error exacto, o el flag de CLI exacto.
FAQ
- ¿Cuál es el cuerpo mínimo de request para POST /v1/send-message/?
- Al menos uno de target_users, target_channels o target_teams, más message (un string) o messages (un arreglo de payloads específicos por plataforma). Todo lo demás — hilos, botones, overrides de identidad — es opcional.
- ¿Cuántos botones interactivos puede llevar un mensaje y qué límites aplican?
- Hasta 25 botones por mensaje. Cada botón es exactamente button_type: "link" o "interactive". Un botón interactivo lleva como máximo uno de los cinco callbacks (callback_url, callback_form, callback_command, callback_prompt, callback_workflow) — combinar dos devuelve 400 button_callback_conflict.
- ¿Cómo verifico que un POST a callback_url realmente vino de Dailybot?
- Cada POST de callback lleva X-Dailybot-Signature: t={unix_timestamp}, v1={hex_hmac}. Calcula HMAC-SHA256 sobre el string ASCII "{timestamp}.{raw_body}" usando el secreto de firma de callbacks de tu organización, compáralo en tiempo constante, y rechaza firmas con un timestamp de más de 5 minutos.
- ¿Cómo edito un mensaje que ya envié?
- Vuelve a hacer POST a /v1/send-message/ con el mismo bot_message_id que recibiste (o enviaste) la primera vez, dentro de una ventana de 72 horas. La plataforma conserva la identidad original del bot — bot_username y bot_icon_* se ignoran al editar.
- ¿Cuál es la diferencia entre callback_workflow y callback_form en un botón?
- callback_workflow dispara un workflow interno de Dailybot (solo tipo api_trigger) completamente del lado del servidor — sin POST externo, sin firma. callback_form abre un formulario interno de Dailybot para que quien hizo clic lo llene. Ambos son mutuamente excluyentes con callback_url y entre sí.