> ## Documentation Index
> Fetch the complete documentation index at: https://inkbox.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Messages and actions

> Read live Slack history and threads, send idempotent messages, and inspect sent, failed, or unknown outcomes

All paths on this page are relative to `https://inkbox.ai/api/v1/slack`. Use a `connection_id` UUID from the [connection list](/docs/api/slack/connections#list-connections).

## List messages

```text theme={null}
GET /connections/{connection_id}/conversations/{conversation_id}/messages
```

Reads messages live from Slack. Supply `thread_ts` to request a thread's messages instead of conversation history. Your identity must be active and the connection must have access to the conversation.

| Query parameter | Type | Default | Meaning |
| :- | :- | :- | :- |
| `limit` | integer | `15` | Page size, `1`–`100` |
| `cursor` | string | Omitted | Cursor from the previous response; up to 2048 characters |
| `thread_ts` | string | Omitted | Root message timestamp for thread reads; digits, a decimal point, then digits; up to 64 characters |

### Response (200)

```json theme={null}
{
  "messages": [
    {
      "type": "message",
      "text": "Can you check the project notes?",
      "ts": "1789552800.000100",
      "user": "U0123456789"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

Message objects can include `type`, `subtype`, `text`, `ts`, `thread_ts`, `user`, `bot_id`, `reply_count`, `edited`, and file metadata in `files`. Available fields depend on the message. These are supported message fields, not a promise to return every Slack message property.

Keep `ts` and `thread_ts` as strings to preserve their exact values. Paginate explicitly using `next_cursor`. History visibility, thread-read permissions, retention, and rate limits come from Slack. For captured message text, use the separate [retained-history API](/docs/api/slack/archive). Retained reads still check current conversation access.

## Get one message

```text theme={null}
GET /connections/{connection_id}/conversations/{conversation_id}/messages/{message_ts}
```

Returns `200` with the live message object, without a wrapper. Supply optional query parameter `thread_ts` when reading a reply. A reply may return `404` without its root timestamp. Keep both timestamps as strings.

## Read message context

```text theme={null}
GET /connections/{connection_id}/conversations/{conversation_id}/messages/{message_ts}/context
```

`limit` defaults to `5` and accepts `1`–`15`. Supply optional `thread_ts` for a thread window. Returns `200` with `messages`, `next_cursor`, `has_more`, `window`, and `complete: false`.

Without `thread_ts`, the window includes messages at or before the selected timestamp and reports `window: "messages_at_or_before_timestamp"`. With `thread_ts`, it includes the selected timestamp and following replies in that thread and reports `window: "messages_at_or_after_timestamp"`. It does not start from the oldest replies in a long thread.

This is a bounded live window, not a complete conversation or a window on both sides of the selected message. The context endpoint does not accept a cursor. Do not reuse its `next_cursor` with another endpoint; start an explicit message-list request when you need more history.

## Get a message link

```text theme={null}
GET /connections/{connection_id}/conversations/{conversation_id}/messages/{message_ts}/permalink
```

Returns `200` with `conversation_id`, `message_ts`, and `permalink`, a Slack message URL. The link does not grant its recipient access to the conversation.

## Send message

```text theme={null}
POST /connections/{connection_id}/messages
```

### Required header

```text theme={null}
Idempotency-Key: project-reply-01
```

The key must contain 1–128 characters from `A–Z`, `a–z`, `0–9`, `.`, `_`, `:`, or `-`. Choose a stable key for each logical send and save it before sending.

### Request body

| Field | Type | Required | Meaning |
| :- | :- | :- | :- |
| `conversation_id` | string | Yes | `C`, `D`, or `G` followed by uppercase letters or digits; up to 64 characters |
| `text` | string | Yes | Message text, 1–12,000 characters |
| `thread_ts` | string or null | No | Root message timestamp for a thread reply; decimal timestamp string, up to 64 characters |

```json theme={null}
{
  "conversation_id": "C0123456789",
  "text": "I will check the project notes.",
  "thread_ts": "1789552800.000100"
}
```

Omit `thread_ts` for a top-level message. The bot must have permission to post in the conversation. This endpoint does not upload files or create interactive message layouts.

### Response (200)

```json theme={null}
{
  "id": "33333333-3333-4333-8333-333333333333",
  "connection_id": "22222222-2222-4222-8222-222222222222",
  "status": "sent",
  "conversation_id": "C0123456789",
  "message_ts": "1789552860.000200",
  "thread_ts": "1789552800.000100",
  "error_code": null,
  "retry_after": null
}
```

The returned `id` identifies the send action, not the Slack message. Save it to inspect the outcome later. HTTP `200` does not by itself mean the message was sent.

| Status | Meaning | What to do |
| :- | :- | :- |
| `sending` | The outcome is not yet recorded. | Read the same action's status later. |
| `sent` | Slack accepted the message. | Use `message_ts` to identify it; do not interpret this as a delivery or read receipt. |
| `failed` | The send failed with a known outcome. | Inspect `error_code` and resolve the cause before deciding whether to make a new send. |
| `unknown` | The request may have reached Slack, but the result could not be established. | Reconcile with live conversation history before deciding on a new send. This status does not automatically settle later; do not blindly resend. |

A stale `sending` action becomes `unknown` after roughly two minutes. Stop polling for automatic resolution once the action is terminal.

### Idempotency

* Repeating the same key and body on the same connection returns the existing action without submitting another message.
* Reusing that key with a different body returns `409`.
* A retry does not restart a failed or unknown action. A new key represents a new send and can duplicate an earlier message if its result was uncertain.
* Keep the original key if the HTTP response is lost. [Look up the action by that key](#recover-action-by-key) without sending another message. Do not generate a replacement key just because a request timed out.

A definite rate-limit rejection can return `200` with a `failed` action and
`error_code: "rate_limited"`; HTTP success alone does not mean the message was sent.
The initial response includes `retry_after` in seconds and the `Retry-After` header
when a delay is available. The SDK field is `retry_after` in Python and Rust, or
`retryAfter` in TypeScript and CLI output. Respect that delay before deliberately
creating a new send. Reusing the same key returns the stored failed action without
posting again. Later lookups and same-key repeats do not retain the original delay;
their `retry_after` is `null` and the original retry header is not repeated.

You can also subscribe to `slack.message_sent`, `slack.message_send_failed`, and `slack.message_send_unknown` [webhooks](/docs/api/slack/webhooks).

## Get action

```text theme={null}
GET /connections/{connection_id}/actions/{action_id}
```

Both path IDs are UUIDs. Returns `200` with the same action shape as send. This reads the recorded action, not a message receipt from Slack.

| Field | Type | Meaning |
| :- | :- | :- |
| `id` | UUID | Action ID |
| `connection_id` | UUID | Selected workspace connection |
| `status` | string | `sending`, `sent`, `failed`, or `unknown` |
| `conversation_id` | string | Destination conversation |
| `message_ts` | string or null | Slack message timestamp when known |
| `thread_ts` | string or null | Requested thread root, if any |
| `error_code` | string or null | Error code when known |
| `retry_after` | integer or null | Retry delay in seconds on an initial rate-limited send response, when available; `null` on later action reads or same-key repeats |

You can inspect recorded actions after the connection is disconnected. The caller must still have access to the identity. Unknown or inaccessible actions return `404`.

## Recover action by key

```text theme={null}
GET /connections/{connection_id}/actions/by-key
Idempotency-Key: project-reply-01
```

Use this read-only lookup when a send response was lost and you saved its idempotency key but not the action ID. The required `Idempotency-Key` header follows the [send key format](#required-header). Returns `200` with the [action object](#get-action). The lookup does not send a message or restart an action, and remains available after the connection is disconnected.

In SDK version `0.7.11` or later, use `inkbox.slack.get_action_by_key(connection_id, idempotency_key)` in Python or `inkbox.slack.getActionByKey(connectionId, idempotencyKey)` in TypeScript. In the CLI, use `inkbox slack action get-by-key --connection-id CONNECTION_ID --idempotency-key project-reply-01`.

```bash cURL theme={null}
curl "https://inkbox.ai/api/v1/slack/connections/22222222-2222-4222-8222-222222222222/actions/by-key" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: project-reply-01"
```

A `404` means no matching action is currently visible to this request. It is **not proof that the message was never sent**: the original request may still be in progress before its action becomes visible. Keep the original key and retry the lookup. If you repeat the send, preserve both that key and the identical body. Do not use a missing result as permission to send with a new key.

## Other message operations

Use [Slack operations](/docs/api/slack/operations) to edit or delete a message, manage reactions and pins, or set an eligible native processing status. Those writes use `in_progress`/`succeeded`/`failed`/`unknown` operation statuses, not the send action’s `sending`/`sent` statuses.
