# Messaging

> Public API for sending, threading, impersonating, and editing bot messages on Slack, Microsoft Teams, Discord, and Google Chat — including interactive buttons with signed callbacks, modals, and workflow triggers.

Language: en
Canonical: https://www.dailybot.com/developers/api/messaging
Markdown: send header `Accept: text/markdown` on any URL to receive Markdown instead of HTML.
Last Updated: 2026-07-24

---

The Dailybot public API exposes messaging endpoints for delivering bot messages, emails, and private conversations across Slack, Microsoft Teams, Discord, and Google Chat. This page includes the full structured field tables for `POST /v1/send-message/`, `POST /v1/send-email/`, and `POST /v1/open-conversation/` (including the complete Button / `modal_body` / `response` / `callback_auth` contracts), plus the `send_as_user` identity override and the Interactive buttons deep-dive.

## Button Builder

Below the `send-message` reference, an interactive **Button Builder** lets you compose interactive buttons visually — link, plain, approve/reject, workflow, form, command, prompt, and modal buttons — and copy the exact `buttons` JSON array, the equivalent [`dailybot chat send`](/developers/cli-reference#chat-send) command (using `--link-button`, `--button`, `--approve-button`/`--reject-button` with `--callback-url`/`--callback-bearer`, or `--workflow-button`, falling back to `--buttons '<json>'` for anything those flags can't express), or a ready-to-run curl body for `POST /v1/send-message/`.

## send_as_user identity override (Slack only)

`send_as_user` lets an authorized caller post a message that appears to come from another user — the message shows that user's Slack display name and avatar instead of the bot's identity. This is useful for automated workflows that need to present messages from the perspective of a specific team member.

**Requirements:**

- Caller must be an organization admin, or have a key owned by an admin.
- The target user (`send_as_user` UUID) must be active and in the same organization.
- Only applies on Slack — other platforms ignore the field and fall back to the standard bot identity.
- `send_as_user` is **mutually exclusive** with `bot_username`, `bot_icon_url`, and `bot_icon_emoji`. Combining them returns `400` with `code: "send_as_user_conflict"`.

When `send_as_user` is set, Dailybot looks up the user's connected Slack identity and uses it as the message author. The message is still sent through the Dailybot bot token — it does not use the user's own Slack token.

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "The sprint is closed and all tickets are resolved.",
    "target_channels": ["C0123456789"],
    "send_as_user": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }'
```

**Error codes for send_as_user:**

| `code` | HTTP | When |
|--------|------|------|
| `send_as_user_conflict` | 400 | `send_as_user` combined with `bot_username`, `bot_icon_url`, or `bot_icon_emoji` |
| `send_as_user_invalid_uuid` | 400 | UUID format is invalid |
| `send_as_user_not_found` | 400 | User not found, inactive, or in a different org |
| `org_admin_required` | 403 | Caller is not an admin |

## Interactive buttons

Every message may carry up to **25** buttons rendered as native interactive elements on Slack, Microsoft Teams, Discord, and Google Chat. Two `button_type` values are supported:

- `link` — clicking opens the `url` in the user's browser.
- `interactive` — clicking triggers one of four capabilities described below.

### Button fields

| Field | Type | When | Notes |
|-------|------|------|-------|
| `label` | `string` (≤ 40 chars) | required | Rendered on the button. |
| `label_after_click` | `string` (≤ 40) | optional | Replaces `label` after the button is clicked. Defaults to `label`. |
| `button_type` | `"link" \| "interactive"` | required | See below. |
| `url` | `string` (https) | required when `button_type = "link"` | The URL the user opens. |
| `value` | `string` (≤ 2000) | required when `button_type = "interactive"` | Sent back in the callback POST body so the caller knows which button was clicked. |
| `callback_url` | `string` (https, ≤ 2048) | optional (interactive) | Callback URL that receives a signed POST on click and on modal submit. Mutually exclusive with `callback_form` and `callback_command`. See [Interactive callback URLs](#interactive-callback-urls) below. |
| `modal_body` | `object` | optional (interactive) | Opens a modal on click. Requires `callback_url` if the modal has input fields. See [Modal buttons](#modal-buttons) below. |
| `callback_form` | `string` (form UUID) | optional (interactive) | Triggers an internal Dailybot form, referenced by its UUID. Mutually exclusive with `callback_url` and `callback_command`. See [Internal form triggers](#internal-form-triggers) below. |
| `callback_command` | `string` (≤ 200) | optional (interactive) | Triggers a KNOWN internal Dailybot command (e.g. `help`). The legacy `"prompt: …"` prefix is rejected — use `callback_prompt`. Mutually exclusive with the other callbacks. |
| `callback_prompt` | `string` (≤ 2000) | optional (interactive) | Free-text AI prompt run as the clicking user. Mutually exclusive with the other callbacks. See [AI prompt triggers](#ai-prompt-triggers) below. |
| `callback_workflow` | `string` (workflow UUID) | optional (interactive) | Triggers an internal Dailybot workflow, referenced by its UUID. Mutually exclusive with the other callbacks. See [Workflow triggers](#workflow-triggers) below. |
| `response` | `object` | optional | Auto-reply sent to the clicker: `{message (≤2000, required), buttons? (recursive), replace_original?, ephemeral?}`. Composes with EVERY button shape. See [Auto-replies](#auto-replies) below. |
| `callback_auth` | `object` | optional (requires `callback_url`) | Static transport auth for the outbound POST (`bearer` \| `basic` \| `custom_header`) — additive to the always-on HMAC signature. See [Callback authentication](#callback-authentication) below. |
| `destroy_button` | `bool` | optional (default `true`) | When `false`, the button remains clickable after the first click. |
| `platform_settings` | `object` | optional | Per-platform overrides (rarely needed). |
| `payload` | `object` | optional | Free-form extra payload; opaque to the API. **The new fields above must live at the top level, not inside `payload`.** |

Every server-side interactive button carries a server-minted `button_id = "$btn/<uuid4>"` that is echoed back in every callback body so the caller can uniquely identify each click. Client-supplied `button_id` values are silently overwritten.

**Legacy note:** `payload.callback_config` and action `value` fields from older integrations still work and are additive — prefer the top-level fields above for new integrations.

### Compatibility matrix

✔ = allowed together, ✖ = rejected. The five callbacks are mutually exclusive with each other (`button_callback_conflict`).

| | `callback_url` | `callback_form` | `callback_command` | `callback_prompt` | `callback_workflow` | `modal_body` | `response` |
|---|---|---|---|---|---|---|---|
| `callback_url` | — | ✖ | ✖ | ✖ | ✖ | ✔ | ✔ |
| `callback_form` | ✖ | — | ✖ | ✖ | ✖ | ✖ | ✔ |
| `callback_command` | ✖ | ✖ | — | ✖ | ✖ | ✖ | ✔ |
| `callback_prompt` | ✖ | ✖ | ✖ | — | ✖ | ✖ | ✔ |
| `callback_workflow` | ✖ | ✖ | ✖ | ✖ | — | ✔ | ✔ |
| `modal_body` | ✔ | ✖ | ✖ | ✖ | ✔ | — | ✔ |
| `response` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | — |

A modal with `input` blocks requires `callback_url` OR `callback_workflow` (`400 button_modal_body_invalid`, detail `input_without_callback`).

`label_after_click` and `destroy_button` compose with **everything** (including `response`): `destroy_button: false` keeps the button clickable — every click is a fresh dispatch with a fresh `X-Dailybot-Delivery` id; for modal buttons the `label_after_click` flip happens at modal SUBMIT, not at open, so a cancelled modal leaves the button clickable. A plain interactive button may carry `response` with no callback at all (the "Skip" case).

### Interactive callback URLs

An `interactive` button with `callback_url` receives an HTTP POST from Dailybot when the user clicks. This is the canonical shape for approval, feedback, and lightweight triage flows built on top of `/v1/send-message/`.

**Request example:**

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Juan wants to buy a coffee. Approve?",
    "target_users": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
    "buttons": [
      { "label": "Yes", "button_type": "interactive", "value": "approve",
        "callback_url": "https://approvals.acme.example/v1/decisions/req_9812" },
      { "label": "No",  "button_type": "interactive", "value": "deny",
        "callback_url": "https://approvals.acme.example/v1/decisions/req_9812" }
    ]
  }'
```

**Callback POST body** — Dailybot fires this when the user clicks:

```
POST https://approvals.acme.example/v1/decisions/req_9812
Content-Type: application/json
User-Agent: Dailybot-Chatbot/1.0
X-Dailybot-Event: button_click
X-Dailybot-Delivery: 5e2d…-uuid
X-Dailybot-Timestamp: 1782001872
X-Dailybot-Signature: t=1782001872, v1=6d9a3e…hex_hmac
```

```json
{
  "event": "button_click",
  "bot_message_id": "$db/ae007b43-dde2-4fa9-bce3-71fb0975a249",
  "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` values.** One of `slack`, `msteams`, `discord`, or `hangouts`. Google Chat is delivered as **`hangouts`** (the historical platform identifier) — match on that value, not on "gchat" or "googlechat". `sent_at` is stamped when the platform message was actually delivered; `clicked_at` when the user clicked. `metadata` round-trips verbatim from your original `/v1/send-message/` call.

**Signature verification.** Every POST carries an HMAC-SHA256 signature computed with your organization's callback signing secret over the ASCII string `"{unix_timestamp}.{raw_body}"`.

Node.js:

```js
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;
  const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
}
```

Python:

```py
import hmac, hashlib, base64, time
def verify(raw_body: bytes, sig_header: str, secret_b64: str) -> bool:
    secret = base64.urlsafe_b64decode(secret_b64 + '==')
    parts = dict(p.strip().split('=', 1) for p in sig_header.split(','))
    ts = int(parts['t'])
    if abs(time.time() - ts) > 300:  # 5 min replay window
        return False
    expected = hmac.new(secret, f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts['v1'])
```

**Idempotency.** `X-Dailybot-Delivery` is unique per click attempt. If Dailybot's outbound POST is retried, the same delivery id is sent — use it as a dedup key.

**Retries.** Dailybot retries **once** with a 500 ms backoff on 5xx / 429 / network errors. Successful `2xx` completes the fanout; any other 4xx is treated as terminal and dropped.

### Modal buttons

Add a `modal_body` object to an `interactive` button to open a lightweight modal on click. On submit, the modal's input values are POSTed to `callback_url` alongside `event: "modal_submit"` and the `value` of the clicked button.

**`modal_body` schema:**

Supported block types are exactly `text`, `input`, and `divider`. The serialized `modal_body` must be ≤ **8 KiB**. Input `name` values must match `^[a-z][a-z0-9_]{0,62}$` and be unique within the modal.

```jsonc
{
  "title": "string, ≤ 200 chars, required",
  "submit_label": "string, ≤ 200 chars, default 'Submit'",
  "blocks": [
    // 1..10 blocks; supported block types:
    { "type": "text", "text": "≤ 3000 chars" },
    {
      "type": "input",
      "name": "snake_case_identifier",   // required, unique per modal; regex ^[a-z][a-z0-9_]{0,62}$
      "label": "Field label, ≤ 200",     // required
      "multiline": true,                  // optional, default false
      "required": true,                   // optional, default false
      "max_length": 1000,                 // optional, ≤ 3000
      "placeholder": "≤ 200",             // optional
      "default": "prefilled value"        // optional
    },
    { "type": "divider" }
  ]
}
```

**Request example (approve-with-comment):**

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Sprint retro — how did it feel?",
    "target_teams": ["team_engineering_uuid"],
    "buttons": [
      { "label": "🚀 Great",   "button_type": "interactive", "value": "great",
        "callback_url": "https://retros.acme.example/v1/sprint-42" },
      { "label": "😐 Fine",    "button_type": "interactive", "value": "fine",
        "callback_url": "https://retros.acme.example/v1/sprint-42" },
      {
        "label": "📝 Add feedback",
        "button_type": "interactive",
        "value": "feedback",
        "callback_url": "https://retros.acme.example/v1/sprint-42",
        "modal_body": {
          "title": "Sprint feedback",
          "submit_label": "Send",
          "blocks": [
            { "type": "text", "text": "Anything on your mind about this sprint?" },
            {
              "type": "input",
              "name": "sentiment_note",
              "label": "Your feedback",
              "multiline": true,
              "required": true,
              "max_length": 1000,
              "placeholder": "One or two sentences is perfect."
            }
          ]
        }
      }
    ]
  }'
```

**Modal-submit POST body** (Dailybot posts this to `callback_url`):

```json
{
  "event": "modal_submit",
  "bot_message_id": "$db/…",
  "button":       { "id": "$btn/…", "value": "feedback" },
  "user":         { "id": "…", "email": "…", "display_name": "…", "external_id": "U…" },
  "organization": { "id": "…", "name": "…" },
  "platform":     "slack",
  "channel":      { "id": "C…", "type": "channel" },
  "modal_fields": {
    "sentiment_note": "Retro was great, ship-per-day cadence is working."
  },
  "metadata":     { },
  "sent_at":      "2026-07-22T14:30:00Z",
  "clicked_at":   "2026-07-22T14:31:12Z"
}
```

Same headers / signature as `button_click`; only `event` and `modal_fields` differ.

Display-only modals (blocks contain only `text` and `divider`, no `input`) are allowed **without** `callback_url` — clicking Submit just closes the modal and no POST fires.

### Internal form triggers

An `interactive` button with `callback_form` opens a Dailybot form on click. No external service is called; the caller's `callback_url` is ignored (and rejected if both are set).

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Need to request access to a system?",
    "target_channels": ["C0AF0FBT23D"],
    "buttons": [
      { "label": "Open access-request form",
        "button_type": "interactive",
        "value": "open_access_form",
        "callback_form": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
    ]
  }'
```

`callback_form` must be the **UUID of a Dailybot form** in the caller's organization (list your forms via `/v1/forms/` to find it). Names and slugs are rejected — form names are mutable and ambiguous. Unknown, archived, or not-accessible forms → `400 button_callback_form_not_found`. The form itself is filled by the user through the existing forms flow — see `/v1/forms/` for how to read the resulting responses.

### Internal command triggers

An `interactive` button with `callback_command` runs a KNOWN internal Dailybot command on click (e.g. `help`, `kudos`, `checkin`). Max 200 characters.

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Need help getting started?",
    "target_users": ["…"],
    "buttons": [
      { "label": "Open help",
        "button_type": "interactive",
        "value": "help",
        "callback_command": "help" }
    ]
  }'
```

**No more `prompt:` prefix.** The legacy `"prompt: <text>"` convention has been removed. A `callback_command` starting with `prompt:` (case-insensitive) is rejected with `400 button_callback_command_invalid` — use the dedicated `callback_prompt` field instead.

**Attribution.** The command runs under the **clicking user's** identity (role scope, usage attribution) — not the sender's. Callers cannot use `callback_command` to make another user run a command they aren't allowed to run.

Unknown commands (not in the built-in commands list) fail softly with a localized "That command isn't available" reply to the clicker — no error to the sender.

### AI prompt triggers

An `interactive` button with `callback_prompt` sends a free-text prompt (≤ 2000 chars) to the Dailybot AI assistant on click, attributed to the **clicking user** (their AI quota, their permissions).

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Curious about last week?",
    "target_users": ["…"],
    "buttons": [
      { "label": "Ask the assistant",
        "button_type": "interactive",
        "value": "ask_ai",
        "callback_prompt": "Summarize incidents from the past 7 days." }
    ]
  }'
```

Blank or oversized prompts → `400 button_callback_prompt_invalid`.

### Workflow triggers

An `interactive` button with `callback_workflow` triggers an internal Dailybot workflow on click, attributed to the clicking user. `callback_workflow` must be the **UUID of a workflow** in the caller's organization — names/slugs are rejected. The workflow must exist, belong to the organization, and be active; otherwise `400 button_callback_workflow_not_found`.

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Deploy is ready.",
    "target_users": ["…"],
    "buttons": [
      { "label": "Run release workflow",
        "button_type": "interactive",
        "value": "run_release",
        "callback_workflow": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
    ]
  }'
```

No external POST fires and no signing material is involved — the trigger is fully internal.

**Trigger variables.** Steps of the triggered workflow can reference the click context through the `{{trigger.*}}` namespace: `{{trigger.source}}` (`api` \| `button_click` \| `modal_submit`), `{{trigger.button_value}}`, `{{trigger.button_id}}`, `{{trigger.fields.<name>}}` (modal inputs), `{{trigger.clicked_at}}`, `{{trigger.body.*}}` (raw payload), `{{trigger.user.*}}` (`uuid`, `full_name`, `first_name`, `email`, `role` of the clicker) and `{{trigger.triggered_by_user_uuid}}`. For plain value-branching, point several buttons at the SAME workflow with different `value`s and branch on `{{trigger.button_value}}`. See [/developers/api/workflows](/developers/api/workflows) for the full trigger-variable reference.

### Modal → Workflow

`modal_body` composes with `callback_workflow`: the click opens the modal, and on submit the field values are delivered to the workflow as `{{trigger.fields.<input.name>}}` — no external server involved.

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Report an incident:",
    "target_users": ["…"],
    "buttons": [
      { "label": "Report", "button_type": "interactive", "value": "report",
        "callback_workflow": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "modal_body": {
          "title": "New incident",
          "blocks": [
            { "type": "input", "name": "summary", "label": "What happened?",
              "multiline": true, "required": true }
          ]
        } }
    ]
  }'
```

The workflow (trigger type `api_trigger`) can then use `{{trigger.fields.summary}}` in any step — pre-fill a form answer, compose a message, feed an AI prompt. Modal-to-workflow buttons carry no signing secret (internal route). A modal with `input` blocks and NEITHER `callback_url` nor `callback_workflow` is rejected (`input_without_callback`).

### Auto-replies

Any button (including a plain interactive button with no callback) may carry a `response` object — an automatic reply shown to the clicker:

```json
{
  "message": "string — required, ≤ 2000 chars",
  "buttons": [],
  "replace_original": false,
  "ephemeral": false
}
```

**The "Skip" case** — a button whose only job is to acknowledge:

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Time for your weekly reflection!",
    "target_users": ["…"],
    "buttons": [
      { "label": "Start", "button_type": "interactive", "value": "start",
        "callback_form": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" },
      { "label": "Skip this week", "button_type": "interactive", "value": "skip",
        "response": { "message": "No problem — see you next week! 👋" } }
    ]
  }'
```

**Timing.** With `callback_url`, the response is sent **immediately on click, in parallel with the outbound POST** — it is instant user feedback ("Processing your approval…"), never gated on the dispatch outcome. This is the canonical **slow-approval pattern**: acknowledge instantly via `response`, then have your server follow up on its own time with a new `POST /v1/send-message/` or by editing the original message within the 72h `bot_message_id` window. With `modal_body`, the response is sent after modal SUBMIT. With the internal callbacks (`callback_form` / `callback_command` / `callback_prompt` / `callback_workflow`), the response is sent BEFORE handing off.

**`replace_original`** — replace the source message instead of posting a new one. **`ephemeral`** — Slack-only; ignored elsewhere.

**Recursive buttons.** `response.buttons` uses the same Button schema and is validated recursively: max nesting depth **3**, ≤ 25 buttons per level, server-minted `button_id` at every depth, and a total serialized size cap of **16 KiB** per top-level button. Violations → `400 button_response_invalid` with `detail` one of `response_message_required`, `response_message_too_long`, `nesting_too_deep`, `too_many_buttons`, `button_too_large`.

### Callback authentication

Every callback POST is ALWAYS signed with the HMAC `X-Dailybot-Signature` header (see [Callback signing secret](#callback-signing-secret)). If your endpoint additionally requires static credentials — an API gateway token, basic auth, a fixed header — attach a `callback_auth` object. It is only valid together with `callback_url`.

```bash
curl -X POST "https://api.dailybot.com/v1/send-message/" \
  -H "X-API-KEY: $DAILYBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Approve the vendor payment?",
    "target_users": ["…"],
    "buttons": [
      { "label": "Approve", "button_type": "interactive", "value": "approve",
        "callback_url": "https://approvals.acme.example/v1/req_77",
        "callback_auth": { "type": "bearer", "token": "acme-gw-token-…" } }
    ]
  }'
```

Types (exactly the fields of the chosen type — extras are rejected):

| `type` | Fields | Sent as |
|--------|--------|---------|
| `bearer` | `token` (≤ 4096) | `Authorization: Bearer <token>` |
| `basic` | `username`, `password` | `Authorization: Basic <base64>` |
| `custom_header` | `header_name`, `header_value` | `<header_name>: <header_value>` |

`header_name` must be a valid RFC 7230 header token and may not be one of `host`, `content-length`, `content-type`, `transfer-encoding`, `connection`, `user-agent`, or anything starting with `x-dailybot-`. Violations → `400 button_callback_auth_invalid`.

Credentials are write-only: they are never returned by any read API and never appear in logs. The HMAC signature remains mandatory — verify it even when you also require static auth.

### Interactive button error codes

All are HTTP `400`. Response shape follows the existing envelope (`{ "detail": "…", "code": "…" }`).

| `code` | When |
|--------|------|
| `button_link_and_callback_conflict` | A `link` button was combined with `value`, `modal_body`, `response`, `callback_auth`, or any callback field. |
| `button_callback_conflict` | An `interactive` button combines more than one of `callback_url`, `callback_form`, `callback_command`, `callback_prompt`, `callback_workflow` — or combines `modal_body` with an internal callback. |
| `button_callback_url_invalid` | `callback_url` is missing scheme, non-`https`, malformed, longer than 2048 chars, or resolves to a private / loopback / IMDS range. |
| `button_modal_body_invalid` | `modal_body` failed structural validation — wrong block type, ≥ 11 blocks, missing / duplicate `name`, oversized field, or a modal with inputs but no `callback_url` / `callback_workflow` (`input_without_callback`). |
| `button_callback_form_not_found` | `callback_form` does not resolve to a form in the caller's organization. |
| `button_callback_command_invalid` | `callback_command` is longer than 200 chars or uses the removed `prompt:` prefix (use `callback_prompt`). |
| `button_callback_prompt_invalid` | `callback_prompt` is blank or longer than 2000 chars. |
| `button_callback_workflow_not_found` | `callback_workflow` is not a UUID or does not resolve to an active workflow in the caller's organization. |
| `button_response_invalid` | `response` failed validation — see `detail`: `response_message_required`, `response_message_too_long`, `nesting_too_deep`, `too_many_buttons`, `button_too_large`. |
| `button_callback_auth_invalid` | `callback_auth` without `callback_url`, unknown type, wrong/extra fields, or a denied/invalid `header_name`. |

The existing button-related behaviors (`buttons.length` cap of 25, `send_as_user_*` errors, `invalid_thread_responses`, etc.) are unchanged.

### Callback signing secret

Every organization has a single callback signing secret used for HMAC. It is auto-generated the first time a message with `callback_url` is sent. The secret is never returned by the public API; obtain it from your organization's admin (a rotation / retrieval endpoint is on the roadmap). Keep it in your service's secret store; treat it like an OAuth client secret.

## See it in action

Automation recipes built on the send-message API, the CLI, and the agent skill — each with copy-paste curl, CLI, and agent-prompt examples.

- [Chat automation & interactive messages](/solutions/chat-automation)
- [CI/CD approval gates](/solutions/cicd-approval-gates)
- [Workflow triggers from chat](/solutions/chat-workflow-triggers)
- [Incident intake with modals](/solutions/incident-intake-modals)
- [Forms lifecycle automation](/solutions/forms-lifecycle-automation)
- [Check-ins automation](/solutions/check-ins-automation)
- [Agent fleet reporting](/solutions/agent-fleet-reporting)
- [All automation recipes](/solutions)

## Endpoints in this group

Send bot messages to users, channels, and teams. Thread replies limited to 10 per parent, one level deep. Editing a message is supported within 72h by re-sending the same `bot_message_id`.

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/v1/send-message/` | Send a bot message |
| POST | `/v1/open-conversation/` | Open a conversation (API-Key only) |
| POST | `/v1/send-email/` | Send an email (API-Key only) |

### POST `/v1/send-message/`

**Send a bot message**

Sends a message to users, channels, or teams. Attach up to 25 interactive buttons per message — link buttons, signed callback URLs, modals, internal forms, commands, AI prompts, workflows, and auto-replies. The same endpoint powers thread replies (up to 10 replies per parent, one level deep), bot impersonation via `platform_settings` (Slack bot name/avatar/ephemeral) and message editing — re-POSTing with the same `bot_message_id` within 72 hours edits the original.

- **Auth:** API key (`X-API-KEY`), CLI Bearer (write)
- **Rate limit:** `send_message_cli`
- **Pagination:** No pagination

#### Request body

##### Targeting

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `target_users` | array<string> | No | User UUIDs, emails, or external IDs. At least one of `target_users` / `target_channels` / `target_teams` is required. |
| `target_channels` | array<string | object> | No | Channel IDs as strings, or objects `{ id, channel_type?, thread? }`. Set `thread` (parent timestamp/id) to reply inside an existing thread. |
| `target_teams` | array<string> | No | Team UUIDs. Every member of the team receives the message. |
| `skip_users_on_time_off` | boolean | No | When true, users flagged as OOO / on leave are skipped. |

##### Content

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `message` | string | No | Plain text or a small HTML-safe subset. Required unless `messages` is set. |
| `messages` | array | No | Platform-specific payloads (advanced). Required if `message` is omitted. |
| `image_url` | string (https) | No | HTTPS image URL to attach to the message. |
| `metadata` | object | No | Free-form custom metadata attached to the message and echoed in callback bodies. |

##### Threading & editing

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `bot_message_id` | string | No | Custom idempotency ID. Re-POST the same value within 72 hours to edit the original message. |
| `thread_responses` | array<{ message: string }> | No | Up to 10 replies posted under the same parent in a single call. Only one level of nesting is allowed. |

##### Identity & impersonation

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `send_as_user` | string (UUID) | No | Slack-only. Admin-only. Post with another org member's display name and avatar. Mutually exclusive with `platform_settings.bot_username`, `bot_icon_url`, and `bot_icon_emoji`. |

##### Buttons

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `buttons` | array<Button> | No | Up to **25** interactive or link buttons. Full field-by-field contract: [Button object ↓](#send-message-button). Server mints `button_id` as `$btn/<uuid4>` (client-supplied ids are overwritten). |

##### Platform settings

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `platform_settings` | object | No | Slack-only identity/ephemeral overrides: `{ bot_username?, bot_icon_url? | bot_icon_emoji?, is_ephemeral? }`. Other platforms ignore this block. Mutually exclusive with `send_as_user`. |

#### Button object (`buttons[]`)

Complete contract for each entry in [`buttons`](#send-message) (max **25** per message). The five callbacks (`callback_url`, `callback_form`, `callback_command`, `callback_prompt`, `callback_workflow`) are mutually exclusive. `modal_body` composes with `callback_url` OR `callback_workflow`. `response` composes with everything. See also the page prose **Interactive buttons** section for the compatibility matrix, signature verification, and recipes.


| Name | Type | Required | Description |
|------|------|----------|-------------|
| `label` | string (≤ 40) | Yes | Text rendered on the button. |
| `label_after_click` | string (≤ 40) | No | Replaces `label` after the button is clicked (or after modal submit for modal buttons). Defaults to `label`. |
| `button_type` | "link" | "interactive" | Yes | `link` opens `url` in the browser. `interactive` triggers one of the callback capabilities below. |
| `url` | string (https) | No | Required when `button_type = "link"`. The URL the user opens. |
| `value` | string (≤ 2000) | No | Required when `button_type = "interactive"`. Echoed in callback POST bodies so you know which button was clicked. Also available as `{{trigger.button_value}}` for workflows. |
| `callback_url` | string (https, ≤ 2048) | No | Signed outbound POST on click and on modal submit. Mutually exclusive with the other four callbacks. See Interactive buttons prose for HMAC verification. |
| `modal_body` | object | No | Opens a modal on click. Compose with `callback_url` OR `callback_workflow`. Input-modals require one of those (`input_without_callback`). Full contract: [modal_body object ↓](#send-message-modal-body) and [blocks ↓](#send-message-modal-block). |
| `callback_form` | string (form UUID) | No | Opens an internal Dailybot form. UUID only (list via `/v1/forms/`). Mutually exclusive with other callbacks. Unknown/archived → `400 button_callback_form_not_found`. |
| `callback_command` | string (≤ 200) | No | Runs a **known** Dailybot command as the clicker (e.g. `help`). The legacy `"prompt: …"` prefix is rejected — use `callback_prompt`. Mutually exclusive with other callbacks. |
| `callback_prompt` | string (≤ 2000) | No | Free-text AI prompt run as the clicking user (their quota/permissions). Blank or oversized → `400 button_callback_prompt_invalid`. Mutually exclusive with other callbacks. |
| `callback_workflow` | string (workflow UUID) | No | Triggers an active `api_trigger` workflow in the caller's org. UUID only. Unknown/inactive/cross-org → `400 button_callback_workflow_not_found`. Mutually exclusive with other callbacks. |
| `response` | object | No | Auto-reply shown to the clicker: `{ message (≤2000, required), buttons? (recursive), replace_original?, ephemeral? }`. Composes with every button shape. Full contract: [response object ↓](#send-message-response). |
| `callback_auth` | object | No | Static transport auth for the outbound POST — only valid with `callback_url`. Additive to the always-on HMAC signature. Credentials are write-only. Full contract: [callback_auth object ↓](#send-message-callback-auth). |
| `destroy_button` | boolean | No | Default `true`. When `false`, the button stays clickable; every click is a fresh dispatch with a new `X-Dailybot-Delivery` id. |
| `button_id` | string ($btn/<uuid4>) | No | Server-minted. Echoed in every callback body. Client-supplied values are silently overwritten — do not set this field. |
| `platform_settings` | object | No | Rare per-platform overrides (legacy). Prefer top-level button fields. |
| `payload` | object | No | Free-form opaque payload. New interactive fields must live at the top level of the button, not inside `payload`. |

#### modal_body object

Attached to an `interactive` [`Button`](#send-message-button) to open a lightweight modal. Serialized size ≤ **8 KiB**. Block items are detailed in [modal_body.blocks[] ↓](#send-message-modal-block).


| Name | Type | Required | Description |
|------|------|----------|-------------|
| `title` | string (≤ 200) | Yes | Modal title. |
| `submit_label` | string (≤ 200) | No | Submit button label. Default: `"Submit"`. |
| `blocks` | array (1..10) | Yes | Exactly the block types `text`, `input`, and `divider` (1–10). Total serialized `modal_body` ≤ **8 KiB**. Slack renders natively; other platforms collect conversationally. Full block fields: [modal_body.blocks[] ↓](#send-message-modal-block). |

#### modal_body.blocks[] item

1–10 blocks inside [`modal_body.blocks`](#send-message-modal-body). Only `text`, `input`, and `divider` are allowed.


| Name | Type | Required | Description |
|------|------|----------|-------------|
| `type` | "text" | "input" | "divider" | Yes | Block kind. Only these three values are accepted. |
| `text` | string (≤ 3000) | No | Required for `type: "text"`. Display-only copy. |
| `name` | string | No | Required for `type: "input"`. Must match `^[a-z][a-z0-9_]{0,62}$` and be unique within the modal. Submitted values arrive as `modal_fields.<name>` / `{{trigger.fields.<name>}}`. |
| `label` | string (≤ 200) | No | Required for `type: "input"`. Field label shown to the user. |
| `multiline` | boolean | No | Input only. Default `false`. |
| `required` | boolean | No | Input only. Default `false`. |
| `placeholder` | string (≤ 200) | No | Input only. Optional placeholder. |
| `max_length` | integer (1..3000) | No | Input only. Optional max length for the value. |
| `default` | string | No | Input only. Optional prefilled value. |

#### response object

Optional auto-reply on any [`Button`](#send-message-button). With `callback_url`, sent immediately in parallel with the outbound POST (slow-approval pattern).


| Name | Type | Required | Description |
|------|------|----------|-------------|
| `message` | string (≤ 2000) | Yes | Auto-reply text shown to the clicker. |
| `buttons` | array<Button> | No | Recursive nested buttons using the same Button schema. Caps: nesting depth ≤ 3, ≤ 25 buttons/level, ≤ 16 KiB serialized per top-level button. Full Button fields: [Button object ↑](#send-message-button). |
| `replace_original` | boolean | No | Replace the source message instead of posting a new one. Default `false`. |
| `ephemeral` | boolean | No | Slack-only; ignored on other platforms. Default `false`. |

#### callback_auth object

Optional static transport auth on a [`Button`](#send-message-button). Only valid together with `callback_url`. Always additive to `X-Dailybot-Signature`.


| Name | Type | Required | Description |
|------|------|----------|-------------|
| `type` | "bearer" | "basic" | "custom_header" | Yes | Auth kind. Exactly the fields of the chosen type — extras are rejected (`button_callback_auth_invalid`). |
| `token` | string (≤ 4096) | No | Required for `type: "bearer"`. Sent as `Authorization: Bearer <token>`. |
| `username` | string | No | Required for `type: "basic"` (with `password`). Sent as `Authorization: Basic <base64>`. |
| `password` | string | No | Required for `type: "basic"` (with `username`). |
| `header_name` | string (RFC 7230 token) | No | Required for `type: "custom_header"`. Denied names: `host`, `content-length`, `content-type`, `transfer-encoding`, `connection`, `user-agent`, and anything starting with `x-dailybot-`. |
| `header_value` | string | No | Required for `type: "custom_header"`. Sent as `<header_name>: <header_value>`. |

#### Response body

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `bot_message_id` | string | Yes | The ID you sent, or a server-generated `$db/<uuid>`. Reuse within 72h to edit. |
| `thread_responses` | array<string> | No | IDs of thread replies (only when `thread_responses` was sent). Each can be edited the same way as `bot_message_id`. |

#### Error codes

| Status | When |
|--------|------|
| `400` | Validation error — see `code` field for specifics: button_link_and_callback_conflict, button_callback_conflict, button_callback_url_invalid, button_modal_body_invalid, button_callback_form_not_found, button_callback_command_invalid, button_callback_prompt_invalid, button_callback_workflow_not_found, button_response_invalid, button_callback_auth_invalid, buttons_count_out_of_range; plus send_as_user_conflict, send_as_user_invalid_uuid, send_as_user_not_found, invalid_thread_responses, missing_targets. |
| `401` | Missing, invalid, or expired credential. |
| `403` | Authenticated but not allowed — `org_admin_required` when a non-admin uses `send_as_user`; CLI role scoping returns `cli_send_message_target_not_allowed`. |
| `429` | Rate-limited. Respect the `Retry-After` header. |

#### Example (curl)

```bash
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 done","target_channels":["C0123456789"]}'
```

#### Scenario examples

##### Basic message

```bash
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"]}'
```

##### Thread report

```bash
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"}]}'
```

##### Link buttons

```bash
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":"New release is live","target_channels":["C0123456789"],"buttons":[{"label":"Release notes","button_type":"link","url":"https://dailybot.com/changelog"},{"label":"Docs","button_type":"link","url":"https://dailybot.com/developers"}]}'
```

##### Approval flow

```bash
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 to production - approve?","target_users":["U0123456789"],"buttons":[{"label":"Approve","button_type":"interactive","value":"approve","callback_url":"https://ci.example.com/dailybot/approve","callback_auth":{"type":"bearer","token":"$DEPLOY_CALLBACK_TOKEN"},"response":{"message":"Approved - deploying now."}},{"label":"Reject","button_type":"interactive","value":"reject","callback_url":"https://ci.example.com/dailybot/approve","callback_auth":{"type":"bearer","token":"$DEPLOY_CALLBACK_TOKEN"},"response":{"message":"Rejected."}}]}'
```

##### Form / workflow / command / prompt

```bash
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":"Pick a follow-up action","target_users":["U0123456789"],"buttons":[{"label":"Submit expense","button_type":"interactive","value":"expense_form","callback_form":"$EXPENSE_FORM_UUID"},{"label":"Trigger rollback","button_type":"interactive","value":"rollback","callback_workflow":"$ROLLBACK_WORKFLOW_UUID"},{"label":"Run help","button_type":"interactive","value":"help","callback_command":"help"},{"label":"Summarize thread","button_type":"interactive","value":"summarize","callback_prompt":"Summarize the last 20 messages in this channel."}]}'
```

##### Modal flow

```bash
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":"Log a quick update","target_users":["U0123456789"],"buttons":[{"label":"Add update","button_type":"interactive","value":"log_update","callback_url":"https://ci.example.com/dailybot/log-update","modal_body":{"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"}]}}]}'
```

#### Notes

- Threads: pass `thread_responses` (max 10) to post a parent plus replies in one call. Reply to an existing message by adding `thread` inside a `target_channels` object: `{ id, thread: <parent_ts_or_id> }`.
- Edit: re-POST with the same `bot_message_id` within 72h. Identity flags (`bot_username`, `bot_icon_*`) are ignored on edit — the platform keeps the original bot identity.
- Impersonation: `platform_settings.bot_username` plus `bot_icon_url` OR `bot_icon_emoji` change the bot's display for that message. Slack only today; other platforms ignore the identity block.
- Ephemeral (Slack only): set `platform_settings.is_ephemeral: true` together with `target_users` to send a private in-channel message visible only to those users.
- CLI role scoping: admin/manager reach every user/team/public channel; team member reaches shared-team members + public channels; guest can only send to themself.
- Cross-platform: works on Slack, Microsoft Teams, Discord, and Google Chat. Threading in DMs varies — Teams / Discord / Google Chat post flat in DMs.
- Interactive buttons: the five callbacks (`callback_url`, `callback_form`, `callback_command`, `callback_prompt`, `callback_workflow`) are mutually exclusive on a single button (`button_callback_conflict`). `modal_body` composes with `callback_url` OR `callback_workflow` (not with internal callbacks). `response` auto-replies compose with every button shape.
- Signed callbacks: `callback_url` buttons receive HMAC-signed POSTs with `X-Dailybot-Signature` (and `X-Dailybot-Event`, `X-Dailybot-Delivery`, `X-Dailybot-Timestamp`). Google Chat is reported as platform `hangouts` in the callback body — match on that value, not "gchat" or "googlechat".
- See the page prose section Interactive buttons for signature-verification snippets (Node.js / Python), the `modal_body` block schema, the compatibility matrix, and callback-auth types.

### POST `/v1/open-conversation/`

**Open a conversation (API-Key only)**

Open a conversation (API-Key only)

- **Auth:** API key (`X-API-KEY`)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing/invalid/expired credential |
| `403` | Authenticated but not permitted |
| `429` | Throttled - Retry-After header set |
| `400` | Validation error |

#### Example (curl)

```bash
curl -sS -X POST 'https://api.dailybot.com/v1/open-conversation/' -H 'X-API-KEY: $DAILYBOT_API_KEY'
```

### POST `/v1/send-email/`

**Send an email (API-Key only)**

Body: to, subject, body_html, body_text.

- **Auth:** API key (`X-API-KEY`)
- **Rate limit:** `default`
- **Pagination:** No pagination

#### Error codes

| Status | When |
|--------|------|
| `401` | Missing/invalid/expired credential |
| `403` | Authenticated but not permitted |
| `429` | Throttled - Retry-After header set |
| `400` | Validation error |

#### Example (curl)

```bash
curl -sS -X POST 'https://api.dailybot.com/v1/send-email/' -H 'X-API-KEY: $DAILYBOT_API_KEY'
```

---

## Developer portal navigation

**Getting Started**

- [Overview](/developers)
- [Quick start](/developers/getting-started)
- [Authentication](/developers/authentication)

**API Reference**

- [API Overview](/developers/api)
- [Users](/developers/api/users)
- [Organization](/developers/api/organization)
- [Teams](/developers/api/teams)
- [Invitations](/developers/api/invitations)
- [Check-ins](/developers/api/check-ins)
- [Forms](/developers/api/forms)
- [Report channels](/developers/api/report-channels)
- [Templates](/developers/api/templates)
- [Kudos](/developers/api/kudos)
- [Mood tracking](/developers/api/mood)
- [Important dates](/developers/api/important-dates)
- [Messaging](/developers/api/messaging) (this page)
- [Automations](/developers/api/workflows)
- [Webhooks](/developers/api/webhooks)
- [Commands platform](/developers/api/commands-platform)
- [Agents](/developers/api/agents)
- [OAuth2](/developers/api/oauth2)
- [Integrations](/developers/api/integrations)
- [CLI](/developers/api/cli)

**API guides**

- [Errors & Status Codes](/developers/errors)
- [Rate Limits](/developers/rate-limits)
- [Conventions](/developers/conventions)
- [API Changelog](/developers/api-changelog)
- [Recipes](/developers/recipes)

**Developer Features**

- [Custom commands](/developers/custom-commands)
- [Serverless commands](/developers/serverless)
- [Webhooks & events](/developers/webhooks)
- [Automation API trigger](/developers/workflow-trigger)
- [Activity API](/developers/activity-api)

**CLI**

- [Overview](/developers/cli)
- [Authentication](/developers/cli-authentication)
- [Command reference](/developers/cli-reference)
- [CI/CD recipes](/developers/cli-ci-cd)
- [Configuration](/developers/cli-configuration)
- [Troubleshooting](/developers/cli-troubleshooting)

**Agent Skill**

- [Overview](/developers/agent-skill)
- [Skills catalog](/skills)

---

## Site navigation

**Product:**
- [Home](/)
- [Product](/product)
- [Pricing](/pricing)
- [Enterprise](/enterprise)
- [Integrations](/integrations)
- [Templates](/templates)

**Resources:**
- [Blog](/blog)
- [Academy](/academy)
- [Changelog](/changelog)
- [Help Center](/help)
- [Developers](/developers)
- [Agents](/agents)

**Company:**
- [About](/about)
- [Careers](/careers)
- [Security](/security)
- [Contact Sales](/demo)

**Connect:**
- [LinkedIn](https://www.linkedin.com/company/dailybot/)
- [X/Twitter](https://twitter.com/dailybot)
- [GitHub](https://github.com/Dailybot-Inc)
- [YouTube](https://www.youtube.com/channel/UC3uM9V52vwX7e3vQpCc4qvA)

