Follow, mute, block, restrict.
Four edge kinds, one consistent shape. Follow shows up in your feed; mute hides someone's content from you without telling them; block is bidirectional and tears down anything in between; restrict quietly pre-moderates someone without unfollowing them.
1
Follow
# Alice follows Bob
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "src_actor_id": "<alice>", "dst_actor_id": "<bob>" }' \
https://social.productcraft.co/v1/communities/<c>/follows
# Unfollow
curl -X DELETE -H "Authorization: Bearer pcft_live_..." \
https://social.productcraft.co/v1/communities/<c>/follows/<alice>/<bob>Importing an existing graph? POST /follows/batch takes up to 200 follows per call and returns a per-item result — a blocked pair fails its item with a 409 while the rest of the batch proceeds. Pass suppress_notifications: true so long-standing followers don't get "X followed you" notifications for edges that existed before you migrated (follower counts still update):
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{
"suppress_notifications": true,
"follows": [
{ "src_actor_id": "<alice>", "dst_actor_id": "<bob>" },
{ "src_actor_id": "<alice>", "dst_actor_id": "<carol>" }
]
}' \
https://social.productcraft.co/v1/communities/<c>/follows/batch
# → { "data": [ { "index": 0, "success": true, "edge": {...}, "created": true }, ... ],
# "succeeded": 2, "failed": 0 }2
Followers + following lists
# Who follows Carol? — with each follower's profile nested on the edge
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/actors/<carol>/followers?limit=50&expand=src_actor"
# Who does Alice follow?
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/actors/<alice>/following?limit=50&expand=dst_actor"Both responses are cursor-paginated edges. To render an avatar grid, pass expand=<field> — the full actor row (display name, avatar, metadata — the profile you stored in stage 2) is nested under that field on every edge, so a followers screen renders from this one call. No per-follower actor fetches, no joining against your own tables:
{
"data": [
{
"id": "7d0e...", "kind": "follow",
"src_actor_id": "d6ee...", "dst_actor_id": "<carol>", ...,
"src_actor": {
"id": "d6ee...", "external_id": "user_alice",
"display_name": "Alice Lin",
"avatar_url": "https://i.pravatar.cc/300?img=47",
"metadata": { "bio": "Brand & product designer ✦" },
"status": "active", ...
}
}
],
"pagination": { "next_cursor": null, "has_more": false }
}expand names the response field the actor is nested under: src_actor on the followers list, dst_actor on following / mutes / blocks / restricts. Without it the response is byte-identical to the plain edge shape. If you need actor rows for an arbitrary set of ids instead (e.g. hydrating ids you cached), batch-fetch with GET /actors?ids=<uuid,uuid,…> (max 100).
3
Mute
Mute is one-directional and silent. Alice mutes Bob — Bob sees nothing, but Alice stops seeing Bob's posts in her feed.
# Mute / unmute
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "src_actor_id": "<alice>", "dst_actor_id": "<bob>" }' \
https://social.productcraft.co/v1/communities/<c>/mutes
curl -X DELETE -H "Authorization: Bearer pcft_live_..." \
https://social.productcraft.co/v1/communities/<c>/mutes/<alice>/<bob>
# List actors Alice has muted
curl -H "Authorization: Bearer pcft_live_..." \
https://social.productcraft.co/v1/communities/<c>/actors/<alice>/mutes?limit=504
Block (and what it tears down)
Blocking is bidirectional and aggressive — creating a block from Bob → Alice:
- Drops any existing follow / mute between them (in both directions).
- Refuses any subsequent follow / mute / reaction between the pair with
409until the block is lifted. - Hides each one's content from the other's feed and profile.
# Block / unblock
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "src_actor_id": "<bob>", "dst_actor_id": "<alice>" }' \
https://social.productcraft.co/v1/communities/<c>/blocks
curl -X DELETE -H "Authorization: Bearer pcft_live_..." \
https://social.productcraft.co/v1/communities/<c>/blocks/<bob>/<alice>
# Trying to follow Bob during a block returns:
# {
# "statusCode": 409,
# "message": "Cannot create this edge — one or both actors have a block in place"
# }5
Restrict (quiet pre-moderation)
Restrict is the Instagram-style middle ground between ignoring someone and blocking them. One-directional and silent — the restricted actor sees nothing change and can still see your content. What actually happens when Alice restricts Bob:
- Comment pre-moderation. Bob's new comments on Alice's posts land as
status="pending_approval"— invisible to everyone but Bob and Alice until Alice approves viaPOST /comments/:id/approve(author-only; covered in stage 4's comment lifecycle). - Notification requests slice. Notifications triggered by Bob are written with
suppressed: true— they skip Alice's default inbox and surface under?status=requests(stage 8). - DM requests folder. Conversations with Bob move out of Alice's default inbox into the
requestsslice of the conversation list (stage 10).
Creating a restrict on a blocked pair returns 409; restricting yourself is 422.
# Restrict / unrestrict
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "content-type: application/json" \
-d '{ "src_actor_id": "<alice>", "dst_actor_id": "<bob>" }' \
https://social.productcraft.co/v1/communities/<c>/restricts
curl -X DELETE -H "Authorization: Bearer pcft_live_..." \
https://social.productcraft.co/v1/communities/<c>/restricts/<alice>/<bob>
# List actors Alice has restricted
curl -H "Authorization: Bearer pcft_live_..." \
https://social.productcraft.co/v1/communities/<c>/actors/<alice>/restricts?limit=506
Edge listings, in summary
Five lists, all cursor-paginated, all under /actors/:actorId/<list>, all expandable:
followers— who follows this actor (expand=src_actor).following— who this actor follows (expand=dst_actor).mutes— who this actor has muted (expand=dst_actor).blocks— who this actor has blocked (expand=dst_actor).restricts— who this actor has restricted (expand=dst_actor).restricts— who this actor has restricted.
All edge DELETEs are idempotent — removing an edge that doesn't exist still returns 204 (missing actors are a 404). To mute keywords rather than people, see muted terms.