API — Episodes
API — Episodes
Section titled “API — Episodes”Purpose
Section titled “Purpose”The episodes API provides CRUD operations for conversation history episodes. Each episode represents a conversation thread between a user and the AI assistant, stored in DynamoDB with optional S3 overflow for large messages.
Episodes can be created in two ways:
- Organically — via the chat streaming endpoint, as users interact with the assistant.
- Programmatically — via the Episode Builder (POST /episodes), used by external systems to create episodes with pre-built content and components.
Base path: /episodes
Authentication
Section titled “Authentication”All endpoints require a userId header identifying the user.
Required Headers:
userId— User identifier (string)
Endpoints
Section titled “Endpoints”| Method | Path | Description |
|---|---|---|
| GET | /episodes | List episodes |
| GET | /episodes/{episode_id} | Get episode transcript |
| GET | /episodes/unread/count | Get unread episode count |
| GET | /episodes/unread | List unread episodes with transcripts |
| POST | /episodes | Create episode (Episode Builder) |
GET /episodes
Section titled “GET /episodes”List the user’s episodes sorted by most recent activity.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | config default | Number of episodes to return (1–100) |
cursor | string | — | Base64-encoded pagination cursor from previous response |
unread_only | bool | false | When true, return only unread episodes |
Response — 200 OK
Section titled “Response — 200 OK”{ "episodes": [ { "id": "968aed30-a619-4ac5-a00d-f4a3a581e9cd", "title": "Daily Deals Alert", "displayDate": "3 hours ago", "is_unread": true, "image_url": "https://cdn.fetchrewards.com/test/hero-deals.png", "icon_url": "https://cdn.fetchrewards.com/test/icon-deals.png", "subtitle": "3 new offers near you", "cta_label": "See Deals", "cta_action": "deeplink://offers/daily" }, { "id": "c0c48608-c1b4-479d-b75a-98a7acb1cdda", "title": "What cereal has the most points?", "displayDate": "Yesterday", "is_unread": false, "image_url": null, "icon_url": null, "subtitle": null, "cta_label": null, "cta_action": null } ], "pagination": { "next_cursor": "eyJVc2VySWQiOi..." }, "unread_count": 3}Field Definitions — EpisodeSummary
Section titled “Field Definitions — EpisodeSummary”| Field | Type | Description |
|---|---|---|
id | string | Unique episode identifier (UUID) |
title | string | Episode title — first user message, explicitly set title, or generated title. Truncated to 120 characters. |
displayDate | string | Relative timestamp: “Just now”, “5 minutes ago”, “3 hours ago”, “Yesterday”, “3 days ago”, “Jan 15” |
is_unread | bool | true if the episode was created by the episode builder and has not been opened via GET /episodes/{id} |
image_url | string | null | Hero image URL for BFF card (set via episode builder) |
icon_url | string | null | Icon URL for BFF card (set via episode builder) |
subtitle | string | null | Subtitle text for BFF card (set via episode builder) |
cta_label | string | null | CTA button label (set via episode builder) |
cta_action | string | null | CTA deeplink URL (set via episode builder) |
unread_countreflects the total across all pages, not just the current page.unread_only=truefilters to episodes wheresource=episode_builderandread_atis null.- BFF fields (
image_url,icon_url,subtitle,cta_label,cta_action) are null for user-initiated conversations.
GET /episodes/{episode_id}
Section titled “GET /episodes/{episode_id}”Get the full transcript for a specific episode as conversational turns.
Side effect: Marks unread episodes as read (sets read_at timestamp in DynamoDB). This happens as a background task and does not block the response.
Path Parameters
Section titled “Path Parameters”| Parameter | Type | Description |
|---|---|---|
episode_id | string | Episode UUID |
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Number of turns to return (1–100) |
cursor | string | — | Pagination cursor for turn pagination |
Response — 200 OK
Section titled “Response — 200 OK”{ "id": "968aed30-a619-4ac5-a00d-f4a3a581e9cd", "turns": [ { "user_query": "Show me today's deals", "ai_response": [ { "event_type": "chunk", "content": "Here are some great deals for you today!" }, { "event_type": "data_loaded", "data": { "type": "offer_list", "key": {"ids": [{"id": "offer_001"}, {"id": "offer_002"}]}, "items": [{"offerId": "offer_001"}, {"offerId": "offer_002"}] } } ] } ], "pagination": { "next_cursor": null }}Field Definitions — Turn
Section titled “Field Definitions — Turn”| Field | Type | Description |
|---|---|---|
user_query | string | The user’s message text |
ai_response | AIResponseEvent[] | Ordered list of AI response events |
Field Definitions — AIResponseEvent
Section titled “Field Definitions — AIResponseEvent”| Field | Type | Description |
|---|---|---|
event_type | string | Event discriminator: "response", "chunk", "data_loaded", "episode", "completed" |
response_id | string | null | OpenAI response ID (present on "response" events) |
content | string | null | Text content (present on "chunk" events) |
data | object | null | Structured component data (present on "data_loaded" events) |
Data Loaded Event Types
Section titled “Data Loaded Event Types”data.type | Description | Key Fields |
|---|---|---|
product_card | Product card component | data.key.ids[].id = FIDO product ID, data.items[] = product objects |
offer_list | Offer list component | data.key.ids[].id = offer ID |
- Reasoning blocks (internal LLM thinking) are stripped from the transcript.
- Prompt-suggestion components are stripped from the transcript.
- Chat history prefixes on malformed user messages are cleaned.
GET /episodes/unread/count
Section titled “GET /episodes/unread/count”Get the count of unread episodes for the user.
Response — 200 OK
Section titled “Response — 200 OK”{ "unread_count": 3}Field Definitions
Section titled “Field Definitions”| Field | Type | Description |
|---|---|---|
unread_count | int | Number of episodes where source=episode_builder and read_at is null |
- Only episodes created via the episode builder are counted. User-initiated conversations are never “unread”.
- An episode transitions to “read” when its transcript is fetched via GET
/episodes/{episode_id}.
GET /episodes/unread
Section titled “GET /episodes/unread”List unread episodes with their full transcripts. Unlike GET /episodes/{episode_id}, this endpoint does not mark episodes as read.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 10 | Number of episodes to return (1–20) |
cursor | string | — | Pagination cursor |
Response — 200 OK
Section titled “Response — 200 OK”{ "episodes": [ { "id": "968aed30-a619-4ac5-a00d-f4a3a581e9cd", "title": "Daily Deals Alert", "displayDate": "3 hours ago", "turns": [ { "user_query": "Show me today deals", "ai_response": [ {"event_type": "chunk", "content": "We found some great deals!"}, {"event_type": "data_loaded", "data": {"type": "offer_list", "key": {"ids": [{"id": "offer_001"}]}}} ] } ] } ], "pagination": { "next_cursor": null }, "unread_count": 3}POST /episodes
Section titled “POST /episodes”Episode Builder. Create an episode with pre-built assistant content and UI components. Used by external systems (e.g., marketing, recommendation engines) to programmatically create rich episodes that appear in the user’s conversation history.
Request Body
Section titled “Request Body”{ "assistant_text": "We found some great deals for you today!", "components": [ { "type": "offer-list", "props": { "offerIds": ["offer_001", "offer_002"], "title": "Top Deals" } } ], "title": "Daily Deals Alert", "user_query": "Show me today deals", "image_url": "https://cdn.fetchrewards.com/hero-deals.png", "icon_url": "https://cdn.fetchrewards.com/icon-deals.png", "subtitle": "3 new offers near you", "cta_label": "See Deals", "cta_action": "deeplink://offers/daily"}Field Definitions — CreateEpisodeRequest
Section titled “Field Definitions — CreateEpisodeRequest”| Field | Type | Required | Description |
|---|---|---|---|
assistant_text | string | yes | Pre-written assistant message text. Must be non-empty. |
components | ComponentRequest[] | yes | Components to include. Must have at least one. |
title | string | no | Episode title. Defaults to user_query or truncated assistant_text. |
user_query | string | no | Synthetic user message stored as the first turn. Defaults to empty string. |
image_url | string | no | Hero image URL for BFF card rendering |
icon_url | string | no | Icon URL for BFF card rendering |
subtitle | string | no | Subtitle text for BFF card |
cta_label | string | no | CTA button label (rover-agent defaults to “View”) |
cta_action | string | no | CTA deeplink URL (rover-agent defaults to deeplink://episode/{id}) |
Field Definitions — ComponentRequest
Section titled “Field Definitions — ComponentRequest”| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | Component type: "product-card" or "offer-list" |
props | object | yes | Component-specific properties (see below) |
Component Props — product-card
Section titled “Component Props — product-card”| Field | Type | Required | Description |
|---|---|---|---|
products | object[] | yes | Non-empty array of product objects |
products[].fidoId | string | yes | FIDO product identifier |
products[].title | string | no | Product title |
products[].price | string | no | Product price |
Component Props — offer-list
Section titled “Component Props — offer-list”| Field | Type | Required | Description |
|---|---|---|---|
offerIds | string[] | yes | Non-empty array of offer IDs |
title | string | no | Offer list heading |
Response — 201 Created
Section titled “Response — 201 Created”{ "episode_id": "968aed30-a619-4ac5-a00d-f4a3a581e9cd"}Error — 400 Bad Request
Section titled “Error — 400 Bad Request”Component validation failure:
{ "detail": "product-card requires 'products' array with at least one item"}Error — 422 Unprocessable Entity
Section titled “Error — 422 Unprocessable Entity”Request validation failure (Pydantic):
{ "detail": [ { "type": "string_too_short", "loc": ["body", "assistant_text"], "msg": "String should have at least 1 character" } ]}- Episodes created by the builder have
source=episode_buildermetadata, which is used for unread tracking. - Components are stored as
```jsonblocks in the assistant message, allowing_extract_text_and_components()to parse them intodata_loadedevents at retrieval time. - BFF fields (
image_url, etc.) are optional — rover-agent’s BFF endpoint applies defaults when they’re absent.
Error Responses
Section titled “Error Responses”503 Service Unavailable
Section titled “503 Service Unavailable”History infrastructure (DynamoDB) is not configured:
{ "detail": "History storage is not configured for this environment"}When: Running in default (local) environment without DynamoDB.
500 Internal Server Error
Section titled “500 Internal Server Error”Unexpected storage or processing error:
{ "detail": "Failed to fetch episodes: <error details>"}Pagination
Section titled “Pagination”All list endpoints use cursor-based pagination:
- First request: omit
cursorparameter. - Check
pagination.next_cursorin the response. - If non-null, pass it as
cursoron the next request. - When
next_cursorisnull, there are no more pages.
Cursors are opaque base64-encoded strings. Do not parse or construct them.
Unread Lifecycle
Section titled “Unread Lifecycle”- Episode created via POST /episodes →
source=episode_builder,read_at=null→is_unread=true - User opens transcript via GET
/episodes/{id}→read_atset to current time →is_unread=false - User-initiated conversations →
sourceis notepisode_builder→ alwaysis_unread=false
DynamoDB Schema
Section titled “DynamoDB Schema”Table: {env}-consumer-agent-episodes
| Key | Type | Description |
|---|---|---|
UserId (PK) | string | User identifier |
EpisodeId (SK) | string | Episode UUID |
CreatedAt | string | ISO-8601 creation timestamp |
LastActivityAt | string | ISO-8601 last activity timestamp (GSI sort key) |
Title | string | Episode title |
TitleStatus | string | "pending", "generated", "set" |
Source | string | "chat" or "episode_builder" |
ReadAt | string | null | ISO-8601 timestamp when first opened (null = unread) |
ImageUrl | string | null | Hero image URL |
IconUrl | string | null | Icon URL |
Subtitle | string | null | Subtitle text |
CtaLabel | string | null | CTA button label |
CtaAction | string | null | CTA deeplink |
Metadata | map | Arbitrary metadata |
expireAt | number | null | TTL for auto-deletion |