# Sender Profiles

Apps can control the name and avatar a chat message appears with. The optional
`sender` object on message-sending endpoints covers two independent concepts:

```json
{
  "sender": {
    "id": "support-bot",
    "name": "Support (EMEA)",
    "imageUrl": "https://example.com/support-emea.png"
  }
}
```

- **`sender.name` / `sender.imageUrl` — per-message display overrides.** The
  supplied name and image are stored on that one message and rendered in place
  of the author's profile. They are display data, not an identity: they never
  create or modify anything beyond the message itself.
- **`sender.id` — a configured bot persona.** If the id matches a persona you
  configured for your app in Developer settings, the message is authored by
  that persona's stable, @-mentionable identity. Any other value is accepted
  and ignored.

You can use either mechanism on its own or combine them.

## Per-message display overrides

Pass `sender.name` and/or `sender.imageUrl` on
[`/chat.post`](/docs/api/chat-post),
[`/chat.postEphemeral`](/docs/api/chat-post-ephemeral), or
[`/chat.startStream`](/docs/api/chat-start-stream) to control how that single
message is displayed:

```json
{
  "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
  "text": "Build **#4021** passed",
  "sender": { "name": "CI · frontend", "imageUrl": "https://example.com/ci.png" }
}
```

Rules:

- `name` is capped at **128 characters** (UTF-16 code units). Longer values
  return 400.
- `imageUrl` must be an **absolute HTTP(S) URL** with a host. Anything else
  returns 400.
- Both fields are optional and independent. An omitted field falls back to the
  author's profile (the app's configured display, or the selected persona's)
  at render time.
- Overrides are **immutable**: they are snapshotted onto the message at send
  time. Renaming your app or reconfiguring a persona later never retroactively
  changes past messages, and sending with an override never renames your app
  or a persona.

Display overrides are presentation, not authentication. Clients always retain
the real author (bot badge and via-app attribution), and API consumers should
rely on the message's `userId` / `userType` — never the display name — for
attribution and bot-loop prevention. See
[Identity & Principals](/docs/guides/identity-and-principals).

## Configured bot personas

A persona is a stable secondary identity for your app — for example one app
posting as `support-bot`, `onboarding-bot`, and `sales-bot`. Unlike a display
override, a persona is a real address in the workspace: users can @-mention
it, and its profile (name and avatar) is managed centrally in your app's
configuration.

### Configuring personas

Personas are configured on the app, not through the message API:

1. Open **Roam Administration → Developer** and edit your app (API key or
   OAuth app).
2. In the bot configuration section, click **Add Bot Persona**.
3. Give the persona a **code** (its `sender.id`), a **display name**, and
   optionally an avatar image.

Constraints:

- Up to **10 personas** per app.
- Codes are 1–16 characters, unique per app (case-insensitive), and stored
  lowercase. The code `_` is reserved for the app's root identity.
- Display names are 1–128 characters.

Persona addresses are provisioned when you save the configuration (for OAuth
apps, across existing installs shortly after saving). Removing a persona stops
it from being selectable for new messages; messages it already sent keep
rendering with its identity.

### Sending as a persona

Pass the persona's code as `sender.id`:

```json
{
  "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
  "text": "A new ticket was assigned to you.",
  "sender": { "id": "support-bot" }
}
```

- Matching is trimmed and **case-insensitive**.
- Selection is **lookup-only**: sending never creates or updates persona
  addresses. Provisioning happens only through configuration.
- An omitted, empty, or `_` id posts as the app's root identity.
- You can combine `sender.id` with `name`/`imageUrl`: the message is authored
  by the persona, with the supplied display override applied to that message
  only. The persona's configured profile is unchanged.

### Unknown ids are accepted and ignored

A `sender.id` that does not match a configured persona — including malformed
or over-length values — is **not an error**. The request succeeds, the message
is authored by the app's root identity, and any supplied `name`/`imageUrl` is
still applied as a per-message display override.

This keeps integrations that put dynamic data in `sender.id` (dates, ticket
ids, phone numbers) working, without minting a new bot identity per value. If
you want messages attributed to a distinct, mentionable identity, configure it
as a persona; merely supplying an id never creates one.

## Authorization is always the app root

Personas affect authorship and display only. Chat and group **access**,
attachment ownership, and `chat.update` / `chat.delete` ownership always
resolve through your app's root identity — the same one used when you send
without a `sender`. Adding your app to a channel is sufficient for all of its
personas to post there; personas cannot be granted or denied access
individually, and a persona cannot be added to a channel as a member.

## Endpoint support

| Endpoint | `sender.id` | `sender.name` / `imageUrl` |
|---|---|---|
| [`/chat.post`](/docs/api/chat-post) | Selects a configured persona | Per-message display override |
| [`/chat.postEphemeral`](/docs/api/chat-post-ephemeral) | Selects a configured persona | Per-message display override |
| [`/chat.startStream`](/docs/api/chat-start-stream) | Selects a configured persona (fixed for the stream's lifetime) | Per-message display override |
| [`/chat.typing`](/docs/api/chat-typing) | Selects a configured persona | Rejected (400) — typing shows the persona's configured profile |
| [`/chat.update`](/docs/api/chat-update), [`/chat.delete`](/docs/api/chat-delete) | Rejected (400) — sender is derived from the original message | Rejected (400) |
| `/v0/chat.sendMessage` (deprecated) | Same semantics as `/chat.post` | Same semantics as `/chat.post` |

**Personal access tokens do not support `sender`.** A PAT always posts as the
personal bot provisioned with the token; any `sender` field on a PAT request
returns 400 (`access_mode_not_supported`). OAuth apps installed with personal
access do support `sender`, with personas configured on the app.

## Reading sender display from the API

Messages sent with a display override carry it in read payloads, so mirrors
and bridges can render the same name/avatar users saw:

- **v1** ([`/chat.history`](/docs/api/chat-history),
  [`/chat.search`](/docs/api/chat-search),
  [`/chat.link.resolve`](/docs/api/chat-link-resolve),
  [`/chat.list`](/docs/api/chat-list) message previews, and the
  [`chat.message`](/docs/webhooks/chat-message) webhook): an optional `sender`
  object with `name` and/or `imageUrl`. Present only when the stored message
  carries an override.
- **v0** surfaces expose the same data as `senderDisplayInfo`.

The field is additive: `userId` (v1) remains the authoring identity — the
app's root or a configured persona — and resolves through
[`user.info`](/docs/api/user-info) as usual.

```json
{
  "userId": "b7c34e90-2f1a-4d8e-9a6b-1c2d3e4f5a6b",
  "userType": "bot",
  "text": "Build **#4021** passed",
  "sender": { "name": "CI · frontend", "imageUrl": "https://example.com/ci.png" },
  "timestamp": 1718900000000000
}
```

## Behavior change (August 2026)

Previously, sending with an arbitrary `sender.id` **created** a new bot
address for that id on the fly, and re-sending could rename it. This minted
junk identities (dates, ticket numbers, phone numbers as ids) and is no longer
the case:

| Before | Now |
|---|---|
| Unknown `sender.id` created a new bot address on send | Accepted and **ignored**; message is authored by the app root |
| `sender.name` / `imageUrl` renamed the sender's stored profile | Applied as an immutable **per-message** display override |
| Configured persona ids selected the persona | Unchanged — still selects the persona (lookup-only) |
| PAT with any `sender` → 400 | Unchanged |

Requests that relied on minting a durable, mentionable identity from an
arbitrary id still return 200, but the message is authored by the app root
with the supplied display applied per-message. To keep a distinct stable
identity, [configure it as a persona](#configuring-personas). Identities
created under the old behavior remain valid for rendering historical
messages, but can no longer be selected for new sends unless configured.