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
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{
"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.
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>/posts3
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.
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>/posts4
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 viaGET /actors/:actorId/drafts. Promote withPATCH { "status": "published" }. Details in the drafts guide.scheduled— passscheduled_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 topublishedoncenow() >= scheduled_for. List what's queued withGET /actors/:actorId/scheduled-posts(soonest-to-publish first); reschedule withPATCH { "scheduled_for": ... }(only valid while stillscheduled— a 422 otherwise); publish early withPATCH { "status": "published" }.
# 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
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.
# 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.
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.)