Build a social app
07 · Stories

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.

bash
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>/posts

2

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.

bash
# 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=50

3

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.

bash
curl -H "Authorization: Bearer pcft_live_..." \
  https://agora.productcraft.co/v1/communities/<c>/actors/<bob>/story-tray
response.json
{
  "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).

bash
# 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.

bash
# 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-results

6

Highlights

Pin a story to keep it past its expiry. The pinned set for an actor surfaces as their highlights:

bash
# 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