Ephemeral posts that act like everything else.
Stories ride on `kind: story` with an `expires_at`. Visibility, polls, view tracking, close-friends lists, and the Instagram-style author tray are layered on top — same surface as the rest of Agora.
1
Post a story
A story is a post with kind: "story" and an expires_at ISO timestamp (typically 24h out). Visibility, attributes, body — everything else is the same as in stage 3.
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{
"actor_id": "<carol>",
"kind": "story",
"body": "Sunset behind Templo de Santo Domingo 🌅",
"attributes": { "media_url": "https://images.unsplash.com/photo-sunset.jpg" },
"expires_at": "2026-05-03T19:35:22.864Z",
"visibility": "public"
}' \
https://agora.productcraft.co/v1/communities/<c>/posts2
Close-friends visibility
Set visibility: "close_friends" on a story to limit it to the author plus members of their close-friends list. Manage the list with two endpoints:
Mental model: close-friends visibility tightens the audience on top of the follow graph — it does not bypass it. A close-friends-only story still only enters a viewer's home feed if that viewer already follows the author. To make a close-friends story visible to someone who doesn't follow the author yet, surface it through the /actors/:actorId/story-tray on the author's profile / story-tray UI rather than relying on the home feed.
# Add Bob to Alice's close friends
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "owner_actor_id": "<alice>", "member_actor_id": "<bob>" }' \
https://agora.productcraft.co/v1/communities/<c>/close-friends
# Remove
curl -X DELETE -H "Authorization: Bearer pcft_live_..." \
https://agora.productcraft.co/v1/communities/<c>/close-friends/<alice>/<bob>
# Read Alice's close-friends list
curl -H "Authorization: Bearer pcft_live_..." \
https://agora.productcraft.co/v1/communities/<c>/actors/<alice>/close-friends?limit=503
The author tray
Instagram-style horizontal tray. One row per author who has unexpired stories visible to the requester, ordered by recency, with an unviewed flag.
curl -H "Authorization: Bearer pcft_live_..." \
https://agora.productcraft.co/v1/communities/<c>/actors/<bob>/story-tray{
"data": [
{
"actor_id": "76054baa-...",
"actor_external_id": "user_carol",
"actor_display_name": "Carol Diaz",
"actor_avatar_url": "https://i.pravatar.cc/300?img=23",
"latest_story_created_at": "2026-05-02T19:35:23.238Z",
"story_count": 1,
"has_unviewed": true
}
]
}4
View tracking
Record a view when the requester actually opens the story. Self-views, blocked-pair views, and views on invisible / expired / removed posts are all silently suppressed (the endpoint returns 204 in every case to keep the surface opaque).
# Record a view
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "viewer_actor_id": "<bob>" }' \
https://agora.productcraft.co/v1/communities/<c>/posts/<story-uuid>/views
# Author lists viewers (only the author can see the list — pass requester_id)
curl -H "Authorization: Bearer pcft_live_..." \
"https://agora.productcraft.co/v1/communities/<c>/posts/<story-uuid>/viewers?requester_id=<carol>&limit=50"5
Polls
Polls live in post.attributes.poll. One vote per (actor, post); revoting overwrites.
# Create a story with a poll
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{
"actor_id": "<carol>",
"kind": "story",
"body": "Best pic from today?",
"attributes": {
"media_url": "https://images.unsplash.com/photo-grid.jpg",
"poll": { "question": "Pick your favorite", "options": ["Mercado", "Sunset", "Rooftop"] }
},
"expires_at": "2026-05-03T19:35:22.864Z"
}' \
https://agora.productcraft.co/v1/communities/<c>/posts
# Vote (option_index is 0-based)
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "actor_id": "<bob>", "option_index": 1 }' \
https://agora.productcraft.co/v1/communities/<c>/posts/<post-uuid>/votes
# Aggregate results
curl -H "Authorization: Bearer pcft_live_..." \
https://agora.productcraft.co/v1/communities/<c>/posts/<post-uuid>/poll-results6
Highlights
Pin a story to keep it past its expiry. The pinned set for an actor surfaces as their highlights:
# Pin
curl -X PATCH -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "pinned": true }' \
https://agora.productcraft.co/v1/communities/<c>/posts/<post-uuid>
# List Carol's highlights
curl -H "Authorization: Bearer pcft_live_..." \
https://agora.productcraft.co/v1/communities/<c>/actors/<carol>/highlights?limit=50