Build a social app
03 · Posts

One primitive, three flavours.

Posts ride a single primitive — set `kind` to text, link, image, or story. Bodies are limited to 10 000 chars after trim. The same endpoint serves a status update, a link share, an image post, or a story (covered in stage 7) — and the same row can be a draft or scheduled for later.


1

Text post

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "actor_id": "<alice-actor-uuid>",
    "kind": "text",
    "body": "Just shipped the Glow style guide v0.4 — color tokens cleaned up, type ramp tightened. Designers, send feedback 🌱"
  }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/posts
response.json
{
  "id": "85a00e9c-8b11-4cfc-947a-c7214ca65f46",
  "community_id": "4d5f5c9e-b4ee-4401-9275-283feb66c178",
  "actor_id": "d6ee4836-1645-41b0-a1c3-c7e22ad01934",
  "kind": "text",
  "title": null,
  "body": "Just shipped the Glow style guide v0.4 — ...",
  "url": null,
  "attributes": {},
  "visibility": "public",
  "status": "published",
  "reaction_counts": {},
  "comment_count": 0,
  "source_post_id": null,
  "repost_count": 0,
  "quote_count": 0,
  "edited_at": null,
  "edit_count": 0,
  "view_count": 0,
  "expires_at": null,
  "pinned": false,
  "scheduled_for": null,
  "created_at": "2026-05-02T19:29:27.762Z",
  "updated_at": "2026-05-02T19:29:27.762Z"
}

2

Link post

kind: "link" with a url and an optional title + body blurb. Your client renders the unfurl card; Social just stores the metadata.

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "actor_id": "<bob-actor-uuid>",
    "kind": "link",
    "title": "Database schema migrations without downtime",
    "body": "A pattern we use at Glow for zero-downtime schema changes. Uses partial indexes + a 3-step rollout.",
    "url": "https://example.com/blog/db-migrations"
  }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/posts

3

Image post

Social doesn't store the binary — your media pipeline does (R2, S3, your CDN). Pass the URL on attributes.media_url + an alt for accessibility. The kind tag is what your client switches on to choose the renderer.

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "actor_id": "<carol-actor-uuid>",
    "kind": "image",
    "title": "Mercado Benito Juárez at golden hour",
    "body": "A study in warm light + cobalt tilework. Oaxaca, this morning.",
    "attributes": {
      "media_url": "https://images.unsplash.com/photo-mercado.jpg",
      "alt": "A market stall under amber light"
    }
  }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/posts

4

Visibility

visibility is public by default. Other values:

  • followers — visible to author + their followers.
  • close_friends — visible to author + members of the author's close-friends list. Stories use this heavily; covered in stage 7.
  • private — visible to author only.

5

Drafts + scheduled posts

status at create time defaults to published, but two other values change the lifecycle:

  • draft — saved, invisible to everyone but the author, listed via GET /actors/:actorId/drafts. Promote with PATCH { "status": "published" }. Details in the drafts guide.
  • scheduled — pass scheduled_for (ISO timestamp, at least 30 seconds in the future; required with this status). The post stays invisible to everyone except the author until a per-minute scheduler promotes it to published once now() >= scheduled_for. List what's queued with GET /actors/:actorId/scheduled-posts (soonest-to-publish first); reschedule with PATCH { "scheduled_for": ... } (only valid while still scheduled — a 422 otherwise); publish early with PATCH { "status": "published" }.
bash
# Schedule a post for tomorrow morning
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "actor_id": "<alice-actor-uuid>",
    "kind": "text",
    "body": "Glow v1.0 is live!",
    "status": "scheduled",
    "scheduled_for": "2026-05-03T09:00:00Z"
  }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/posts

# What's queued?
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<community-uuid>/actors/<alice-actor-uuid>/scheduled-posts"

6

List + filter by author

bash
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<community-uuid>/posts?author_id=<carol-actor-uuid>&limit=20"

Drop author_id to list every post in the community (still cursor-paginated). For an end-user's home tab, you almost certainly want the feed endpoint in stage 6, not this raw list.


7

Edit + soft-delete

Content edits (body / title) are allowed within the community's edit window (default 30 minutes, configurable via edit_window_minutes) — afterwards, 422 EDIT_WINDOW_EXPIRED. Each edit snapshots the previous version; see the edit history guide.

bash
# PATCH — partial update; missing fields are left alone.
curl -X PATCH -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{ "body": "Updated copy with a new hashtag #cdmx #goldenhour" }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/posts/<post-uuid>

# DELETE — soft-delete (status flips to "removed"; rows persist for moderation audit)
curl -X DELETE -H "Authorization: Bearer pcft_live_..." \
  https://social.productcraft.co/v1/communities/<community-uuid>/posts/<post-uuid>

8

View counts (batch)

Every post carries a view_count. Record impressions from your client in batches — debounce on your side (e.g. once per viewport-dwell) and flush up to 200 post ids per call. The endpoint returns 204; ids that no longer exist are silently skipped so a deleted post mid-batch doesn't fail the rest.

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{ "post_ids": ["<post-uuid-1>", "<post-uuid-2>", "<post-uuid-3>"] }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/posts/views

(Story views are different — they're per-viewer, deduplicated, and power the author's viewer list; see stage 7.)