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

# Selection

> Send a flat or grouped list picker and receive stable option IDs beside a readable reply.

Send a `selection` part with a short `title` and 1 to 25 options, flat or grouped into sections. Each option has a readable `label` and a stable `id` or legacy `value`. The user opens the picker and submits their choices in one reply.

## Send a selection

This flat example keeps the existing `value` format. Its optional text appears above the card; `title` heads the card and picker. Open the prompt below, check options, then press **Send**; this demo stays in your browser and sends no request.

**Preview**

Interactive selection preview

**JSON**

```json theme={null}
{
  "message": {
    "parts": [
      {"type":"text","value":"Which topics interest you?"},
      {"type":"selection","title":"Topics","options":[
        {"value":"research","label":"Research"},
        {"value":"design","label":"Design"}
      ]}
    ],
    "idempotency_key":"topics-prompt-001"
  }
}
```

Checking a row sends nothing. The sheet lists every option in source order with a checkbox on the leading edge, and its Send stays disabled until at least one row is checked, so one submission carries the whole answer.

**`multiple` defaults to `true`; set it to `false` for one choice.** Older Relay app versions ignore `multiple: false` and can send several choices, which Relay refuses with a 422, so prefer `multiple: true` until the new app ships. Send exactly one of flat `options` or titled `sections`. [Selection fields](https://docs.staging.relayapp.im/interactions/selection-reference.md) covers limits, aliases, and returned fields.

Use an Agent Token and an existing chat ID to send the same request:

```typescript TypeScript SDK theme={null}
import Relay, { partsWithSelection } from "@relaymessenger/sdk";

const relay = new Relay({
  apiKey: process.env.RELAY_AGENT_TOKEN!,
  baseURL: "https://api.staging.relayapp.im",
});

await relay.chats.messages.send("CHAT_ID", {
  message: {
    parts: partsWithSelection("Which topics interest you?", {
      type: "selection",
      title: "Topics",
      options: [
        { value: "research", label: "Research" },
        { value: "design", label: "Design" },
      ],
    }),
    idempotency_key: "topics-prompt-001",
  },
});
```

```bash HTTPS theme={null}
curl -sS "https://api.staging.relayapp.im/v1/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "parts": [
        {"type":"text","value":"Which topics interest you?"},
        {"type":"selection","title":"Topics","options":[
          {"value":"research","label":"Research"},
          {"value":"design","label":"Design"}
        ]}
      ],
      "idempotency_key":"topics-prompt-001"
    }
  }'
```

The send path returns `202` after storing the message. Keep the returned message ID so your application can associate responses with this question. Reuse the same request and idempotency key after an uncertain send.

```http theme={null}
HTTP/1.1 202 Accepted
```

### Group options and set the answered text

Use `sections` to group rows, `subtitle` for the card's second line, and `reply_message` for the answered bubble. Row subtitles and images belong inside the picker. This example requests one delivery choice.

```typescript TypeScript SDK theme={null}
import Relay from "@relaymessenger/sdk";

const relay = new Relay({
  apiKey: process.env.RELAY_AGENT_TOKEN!,
  baseURL: "https://api.staging.relayapp.im",
});

await relay.chats.messages.send("CHAT_ID", {
  message: {
    parts: [{
      type: "selection",
      title: "Delivery",
      subtitle: "Choose your delivery speed.",
      multiple: false,
      sections: [{
        title: "Shipping",
        options: [
          { id: "express", label: "Express", subtitle: "Next business day",
            image_url: "https://example.com/express.png" },
          { id: "standard", label: "Standard", subtitle: "Three business days" },
        ],
      }],
      reply_message: {
        title: "Delivery selected",
        subtitle: "We have your choice.",
      },
    }],
    idempotency_key: "delivery-picker-001",
  },
});
```

```bash HTTPS theme={null}
curl -sS "https://api.staging.relayapp.im/v1/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "parts": [{
        "type": "selection",
        "title": "Delivery",
        "subtitle": "Choose your delivery speed.",
        "multiple": false,
        "sections": [{
          "title": "Shipping",
          "options": [
            {"id":"express","label":"Express","subtitle":"Next business day",
             "image_url":"https://example.com/express.png"},
            {"id":"standard","label":"Standard","subtitle":"Three business days"}
          ]
        }],
        "reply_message": {
          "title": "Delivery selected",
          "subtitle": "We have your choice."
        }
      }],
      "idempotency_key": "delivery-picker-001"
    }
  }'
```

```python Python SDK theme={null}
import asyncio
import os
from relaymessenger import Relay

async def main():
    relay = Relay(
        api_key=os.environ["RELAY_AGENT_TOKEN"],
        base_url="https://api.staging.relayapp.im",
    )
    await relay.chats.messages.send("CHAT_ID", {
        "message": {
            "parts": [{
                "type": "selection",
                "title": "Delivery",
                "subtitle": "Choose your delivery speed.",
                "multiple": False,
                "sections": [{
                    "title": "Shipping",
                    "options": [
                        {"id": "express", "label": "Express",
                         "subtitle": "Next business day",
                         "image_url": "https://example.com/express.png"},
                        {"id": "standard", "label": "Standard",
                         "subtitle": "Three business days"},
                    ],
                }],
                "reply_message": {
                    "title": "Delivery selected",
                    "subtitle": "We have your choice.",
                },
            }],
            "idempotency_key": "delivery-picker-001",
        },
    })

asyncio.run(main())
```

The returned prompt retains flat `options` as well as the supplied `sections`. Options return both `id` and an equal `value` alias, so existing readers can keep using `value`.

## Receive stable values

The user's submission contains exactly two ordered parts: readable text, then structured metadata. Its `reply_to` identifies the source message and selection part.

**Preview**

Selection reply preview

**What you receive**

```json theme={null}
{
  "event_type":"message.received",
  "data": {
    "parts": [
      {"type":"text","value":"• Research\n• Design"},
      {"type":"selection_response","selected_values":["research","design"],"selected_ids":["research","design"]}
    ],
    "reply_to": {
      "message_id":"01993d50-ef7b-7b37-886b-23fd80c7ec13",
      "part_index":1
    }
  }
}
```

The event example shows the response fields used by your handler. Relay returns source-derived `selected_ids` even when the user submitted only `selected_values`. Both arrays name the same options in source order.

When the prompt supplies `reply_message`, Relay copies it into the answer metadata for the answered bubble, independently of the source Message. Without it, the reply uses the source title and chosen labels, as in the preview. This presentation does not replace the portable text.

New text equals literal `• ` + each selected source label joined with `\n`, in source-option order. It supplies portable bubble, notification, and copy text; iOS may draw a checkmark in place of each bullet, and repeat the selection's `title` above the lines, as presentation only. The `selection_response` part carries metadata only.

A user answers a given selection once. Opening the answered prompt again, or opening the reply, presents the same sheet read only: the chosen rows are checked, no row is tappable, and the sheet carries no Send. Try it in the preview above.

## Read whether someone answered

When you read a Message back, its `selection` part carries `has_responded`, `selected_values`, and `selected_ids`, and its `reactions` is always `null`:

```json theme={null}
{"type":"selection","title":"Topics","options":[{"id":"research","value":"research","label":"Research"},{"id":"design","value":"design","label":"Design"}],"has_responded":false,"selected_values":null,"selected_ids":null,"reactions":null}
```

These fields belong to whoever is reading. `has_responded` becomes `true` after that user answers; both arrays hold their choices in source order and are `null` before an answer or when the answer Message no longer exists. For an agent, `has_responded` is always `false` and both arrays are always `null`, so read answers from received `selection_response` Messages.

The user's devices receive a `message.upserted` for the prompt with the updated viewer fields.

A `selection_response` part you read back has no `reactions` key, and neither selection part can receive a reaction.

Your existing signed [webhook](https://docs.staging.relayapp.im/webhooks/verify-signatures.md) or [WebSocket](https://docs.staging.relayapp.im/websocket/index.md) handler receives the ordinary `message.received` event. The SDK's default event types expose the metadata:

```typescript Application handler theme={null}
import type { RelayWebhookEvent } from "@relaymessenger/sdk";

function selectionFrom(event: RelayWebhookEvent) {
  if (event.event_type !== "message.received") return;
  const response = event.data.parts.find(
    (part) => part.type === "selection_response",
  );
  if (!response || !event.data.reply_to) return;
  return {
    source: event.data.reply_to,
    selectedValues: response.selected_values,
    selectedIds: response.selected_ids,
  };
}
```

Dispatch your own application handler using the source message ID, part index, and stable values. Preserve the metadata when forwarding events to a runtime. Use the existing event deduplication and durable processing path before acknowledging delivery.

## Send from a runtime

Selections use each runtime's existing send path. The flat `title` and `options` examples remain valid; each [integration](https://docs.staging.relayapp.im/integrations/index.md) describes its accepted input. Preserve `selected_values`, `selected_ids`, and `reply_to` beside the readable text when forwarding a reply.

## When it fails

The server checks an answer against its source: the user must see the prompt in the same chat, text must match the chosen labels, and identifiers must be known and in source order. `selected_values` remains required; optional `selected_ids` must match it exactly, and `multiple: false` requires one choice. For compatibility it also accepts the exact selected source labels joined with `, `; new clients send bullet lines.

Dispatch on the identifiers and `reply_to`, never by parsing text. The [field reference](https://docs.staging.relayapp.im/interactions/selection-reference.md) separates client reply inputs from server-owned metadata.

| Status | Code | Cause |
| - | - | - |
| `400` | [1005](https://docs.staging.relayapp.im/api-reference/errors.md#1005) | A prompt violates the field limits, supplies both or neither option arrays, repeats an ID across sections, or supplies mismatched `id` and `value`. A Message has two selections or buttons beside one. Reply metadata is malformed, its arrays disagree, or it supplies `reply_message`; the answer is not plain text then metadata, or lacks `reply_to.part_index`. |
| `403` | [2003](https://docs.staging.relayapp.im/api-reference/errors.md#2003) | Someone other than an agent sent a selection, or someone other than a user answered one. |
| `404` | [2001](https://docs.staging.relayapp.im/api-reference/errors.md#2001) | The answer's `reply_to.message_id` is not a Message the user can see in this chat. |
| `409` | [1005](https://docs.staging.relayapp.im/api-reference/errors.md#1005) | The user already answered this selection with a different idempotency key. A retry with the same key and body returns the original answer instead. |
| `422` | [2006](https://docs.staging.relayapp.im/api-reference/errors.md#2006) | The answer's text, identifiers, order, or number of choices does not match the source; `reply_to.part_index` does not name a selection, or a reaction targets one. |

Keep the original body and idempotency key when retrying an uncertain submission.

## Next steps

* [Interactions](https://docs.staging.relayapp.im/interactions/index.md)
* [Selection fields](https://docs.staging.relayapp.im/interactions/selection-reference.md)
* [Buttons](https://docs.staging.relayapp.im/interactions/buttons.md)
* [Replies](https://docs.staging.relayapp.im/messages/replies.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.