Bookmarks.
Save a post for later. Bookmarks are private — only the bookmarker sees them. No notifications, no counters; just a small per-actor save list with cursor pagination.
1
Save a post
Toggle-on. Idempotent — re-saving the same post returns the existing row with { created: false } instead of erroring. Visibility-aware: if the actor can't see the post, you get a 404 Post not found matching the opacity rule used everywhere else in social.
curl -X POST \
-H "Authorization: Bearer pcft_live_..." \
-H "Content-Type: application/json" \
-d '{"actor_id":"<actorId>"}' \
"https://social.productcraft.co/v1/communities/<communityId>/posts/<postId>/bookmark"{
"bookmark": {
"actor_id": "aaaaaaaa-...",
"post_id": "bbbbbbbb-...",
"community_id": "cccccccc-...",
"created_at": "2026-05-11T19:55:00.000Z"
},
"created": true
}2
Remove a bookmark
Toggle-off. Idempotent 204 either way — calling DELETE on a post that wasn't bookmarked still 204s. The body still carries actor_id so the request shape mirrors the POST.
curl -X DELETE \
-H "Authorization: Bearer pcft_live_..." \
-H "Content-Type: application/json" \
-d '{"actor_id":"<actorId>"}' \
"https://social.productcraft.co/v1/communities/<communityId>/posts/<postId>/bookmark"3
List my bookmarks
Per-actor, cursor-paginated newest-first. Same pagination envelope as the rest of social — { data, pagination: { next_cursor, has_more } }.
Bookmarks against posts whose visibility has narrowed since (or which have been deleted) stay in the list — we don't cross-check post visibility on read of bookmarks. The customer's UI can hydrate post-by-post if it wants the live state.
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<communityId>/actors/<actorId>/bookmarks?limit=20"{
"data": [
{ "actor_id": "...", "post_id": "p-1", "community_id": "...", "created_at": "2026-05-11T19:55:00.000Z" },
{ "actor_id": "...", "post_id": "p-2", "community_id": "...", "created_at": "2026-05-10T08:00:00.000Z" }
],
"pagination": { "next_cursor": null, "has_more": false }
}UX
UX patterns
- Optimistic UI. The toggle is fast and idempotent — flip the icon locally, fire the API call, and reconcile only on error. The duplicate POST returning
created: falseis your signal that someone else (the same user on another device, usually) already bookmarked the post. - Bookmark count is intentionally absent. Bookmarks are private by design — no global popularity signal, nothing for the post author to see. If you want public “saved” counts, that's a separate feature (X-style; out of scope here).
- Scope the list endpoint to the right user. The PAK lane has no end-user principal — your backend is responsible for ensuring the
actorIdin the path matches the end-user behind the request before exposing the response. Returning user A's bookmarks to user B leaks reading habits.
Permissions
PAK scopes
- POST requires
social.createonpcft:agora:post/<postId>. - DELETE requires
social.deleteon the same URN. - GET requires
social.listonpcft:agora:community/<communityId>.