Skip to content

API — Episodes

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:

  1. Organically — via the chat streaming endpoint, as users interact with the assistant.
  2. Programmatically — via the Episode Builder (POST /episodes), used by external systems to create episodes with pre-built content and components.

Base path: /episodes


All endpoints require a userId header identifying the user.

Required Headers:

  • userId — User identifier (string)

MethodPathDescription
GET/episodesList episodes
GET/episodes/{episode_id}Get episode transcript
GET/episodes/unread/countGet unread episode count
GET/episodes/unreadList unread episodes with transcripts
POST/episodesCreate episode (Episode Builder)

List the user’s episodes sorted by most recent activity.

ParameterTypeDefaultDescription
limitintconfig defaultNumber of episodes to return (1–100)
cursorstringBase64-encoded pagination cursor from previous response
unread_onlyboolfalseWhen true, return only unread episodes
{
"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
}
FieldTypeDescription
idstringUnique episode identifier (UUID)
titlestringEpisode title — first user message, explicitly set title, or generated title. Truncated to 120 characters.
displayDatestringRelative timestamp: “Just now”, “5 minutes ago”, “3 hours ago”, “Yesterday”, “3 days ago”, “Jan 15”
is_unreadbooltrue if the episode was created by the episode builder and has not been opened via GET /episodes/{id}
image_urlstring | nullHero image URL for BFF card (set via episode builder)
icon_urlstring | nullIcon URL for BFF card (set via episode builder)
subtitlestring | nullSubtitle text for BFF card (set via episode builder)
cta_labelstring | nullCTA button label (set via episode builder)
cta_actionstring | nullCTA deeplink URL (set via episode builder)
  • unread_count reflects the total across all pages, not just the current page.
  • unread_only=true filters to episodes where source=episode_builder and read_at is null.
  • BFF fields (image_url, icon_url, subtitle, cta_label, cta_action) are null for user-initiated conversations.

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.

ParameterTypeDescription
episode_idstringEpisode UUID
ParameterTypeDefaultDescription
limitint20Number of turns to return (1–100)
cursorstringPagination cursor for turn pagination
{
"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
}
}
FieldTypeDescription
user_querystringThe user’s message text
ai_responseAIResponseEvent[]Ordered list of AI response events
FieldTypeDescription
event_typestringEvent discriminator: "response", "chunk", "data_loaded", "episode", "completed"
response_idstring | nullOpenAI response ID (present on "response" events)
contentstring | nullText content (present on "chunk" events)
dataobject | nullStructured component data (present on "data_loaded" events)
data.typeDescriptionKey Fields
product_cardProduct card componentdata.key.ids[].id = FIDO product ID, data.items[] = product objects
offer_listOffer list componentdata.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 the count of unread episodes for the user.

{
"unread_count": 3
}
FieldTypeDescription
unread_countintNumber 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}.

List unread episodes with their full transcripts. Unlike GET /episodes/{episode_id}, this endpoint does not mark episodes as read.

ParameterTypeDefaultDescription
limitint10Number of episodes to return (1–20)
cursorstringPagination cursor
{
"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
}

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.

{
"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”
FieldTypeRequiredDescription
assistant_textstringyesPre-written assistant message text. Must be non-empty.
componentsComponentRequest[]yesComponents to include. Must have at least one.
titlestringnoEpisode title. Defaults to user_query or truncated assistant_text.
user_querystringnoSynthetic user message stored as the first turn. Defaults to empty string.
image_urlstringnoHero image URL for BFF card rendering
icon_urlstringnoIcon URL for BFF card rendering
subtitlestringnoSubtitle text for BFF card
cta_labelstringnoCTA button label (rover-agent defaults to “View”)
cta_actionstringnoCTA deeplink URL (rover-agent defaults to deeplink://episode/{id})
FieldTypeRequiredDescription
typestringyesComponent type: "product-card" or "offer-list"
propsobjectyesComponent-specific properties (see below)
FieldTypeRequiredDescription
productsobject[]yesNon-empty array of product objects
products[].fidoIdstringyesFIDO product identifier
products[].titlestringnoProduct title
products[].pricestringnoProduct price
FieldTypeRequiredDescription
offerIdsstring[]yesNon-empty array of offer IDs
titlestringnoOffer list heading
{
"episode_id": "968aed30-a619-4ac5-a00d-f4a3a581e9cd"
}

Component validation failure:

{
"detail": "product-card requires 'products' array with at least one item"
}

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_builder metadata, which is used for unread tracking.
  • Components are stored as ```json blocks in the assistant message, allowing _extract_text_and_components() to parse them into data_loaded events at retrieval time.
  • BFF fields (image_url, etc.) are optional — rover-agent’s BFF endpoint applies defaults when they’re absent.

History infrastructure (DynamoDB) is not configured:

{
"detail": "History storage is not configured for this environment"
}

When: Running in default (local) environment without DynamoDB.

Unexpected storage or processing error:

{
"detail": "Failed to fetch episodes: <error details>"
}

All list endpoints use cursor-based pagination:

  1. First request: omit cursor parameter.
  2. Check pagination.next_cursor in the response.
  3. If non-null, pass it as cursor on the next request.
  4. When next_cursor is null, there are no more pages.

Cursors are opaque base64-encoded strings. Do not parse or construct them.


  1. Episode created via POST /episodessource=episode_builder, read_at=nullis_unread=true
  2. User opens transcript via GET /episodes/{id}read_at set to current time → is_unread=false
  3. User-initiated conversationssource is not episode_builder → always is_unread=false

Table: {env}-consumer-agent-episodes

KeyTypeDescription
UserId (PK)stringUser identifier
EpisodeId (SK)stringEpisode UUID
CreatedAtstringISO-8601 creation timestamp
LastActivityAtstringISO-8601 last activity timestamp (GSI sort key)
TitlestringEpisode title
TitleStatusstring"pending", "generated", "set"
Sourcestring"chat" or "episode_builder"
ReadAtstring | nullISO-8601 timestamp when first opened (null = unread)
ImageUrlstring | nullHero image URL
IconUrlstring | nullIcon URL
Subtitlestring | nullSubtitle text
CtaLabelstring | nullCTA button label
CtaActionstring | nullCTA deeplink
MetadatamapArbitrary metadata
expireAtnumber | nullTTL for auto-deletion