# 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://ro.am/assets/9b1c2d3e-4f50-6a7b-8c9d-0e1f2a3b4c5d/content/avatar.webp"
  }
}
```

- **`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://ro.am/assets/9b1c2d3e-4f50-6a7b-8c9d-0e1f2a3b4c5d/content/avatar.webp" }
}
```

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. From API version `2026-08-25` it must also be a **Roam-hosted
  avatar URL**: create one with [`/asset.create`](/docs/api/asset-create)
  `purpose: "avatar"`, upload the bytes, then pass the returned `imageUrl`.
  Legacy `/card-images/` and `/photos/people/` URLs are also accepted.
  Third-party image URLs return 400. File-purpose asset URLs
  (`/assets/{id}/content/content`, `/thumbnail`) are rejected even though
  they share the `/assets/{id}/` prefix — only
  `/assets/{id}/content/avatar.webp` counts. Older version pins still
  accept any absolute HTTP(S) URL. `/v0` is outside this gate. See
  [API Versioning](/docs/guides/api-versioning).
- 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).

## Hosting an avatar

From version `2026-08-25`, `imageUrl` cannot point at a third-party host. Host
the image with Roam:

1. `POST /v1/asset.create` with `purpose: "avatar"`, a `name` (include the
   extension), and `size` (required, max 10 MiB).
2. Upload the image bytes in one `POST` to `uploadUrl` with every
   `uploadHeaders` entry. Processing produces a 512×512 WebP.
3. Use the response `imageUrl` (`https://ro.am/assets/{id}/content/avatar.webp`)
   as `sender.imageUrl` or as On-Air `hosts[].imageUrl`. Do not reuse a
   `purpose: "file"` content or thumbnail URL.

The URL is deterministic from `assetId` and is returned immediately, but it
404s until processing finishes — typically a few seconds after the upload
returns. Reuse the same `imageUrl` on later messages; the asset does not
expire.

The same `imageUrl` works for On-Air display hosts on
[`/onair.event.create`](/docs/onair-api/onair-event-create) and
[`/onair.event.update`](/docs/onair-api/onair-event-update).

## 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://ro.am/assets/9b1c2d3e-4f50-6a7b-8c9d-0e1f2a3b4c5d/content/avatar.webp" },
  "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.