Build a social app
02 · Profiles

Project your end-users as actors.

Every social interaction in Social keys off an `actor` — your end-user's projection into a community. Create one with the id you already use on your side; Social returns its own UUID and you key off that from then on.


1

Upsert an actor

POST /v1/communities/:c/actors is idempotent on (community, external_id). The first call creates; the second with the same external_idreturns the same row with created: false.

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "external_id": "user_alice",
    "display_name": "Alice Lin",
    "avatar_url": "https://i.pravatar.cc/300?img=47",
    "metadata": {
      "bio": "Brand & product designer based in Lisbon ✦",
      "location": "Lisbon, Portugal"
    }
  }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/actors
response.json
{
  "actor": {
    "id": "d6ee4836-1645-41b0-a1c3-c7e22ad01934",
    "community_id": "4d5f5c9e-b4ee-4401-9275-283feb66c178",
    "external_id": "user_alice",
    "display_name": "Alice Lin",
    "avatar_url": "https://i.pravatar.cc/300?img=47",
    "metadata": {
      "bio": "Brand & product designer based in Lisbon ✦",
      "location": "Lisbon, Portugal"
    },
    "status": "active",
    "created_at": "2026-05-02T19:28:58.294Z",
    "updated_at": "2026-05-02T19:28:58.294Z"
  },
  "created": true
}

Importing an existing user base? POST /actors/batch takes up to 200 upserts per call — one HTTP call per 200 users instead of one per user. Each item runs the exact single-upsert path and reports its own result, so one invalid row doesn't fail the batch:

bash
curl -X POST -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "actors": [
      { "external_id": "user_alice", "display_name": "Alice Lin" },
      { "external_id": "user_bob",   "display_name": "Bob Park" }
    ]
  }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/actors/batch

# → { "data": [ { "index": 0, "success": true, "actor": {...}, "created": true }, ... ],
#     "succeeded": 2, "failed": 0 }

2

Idempotency in practice

Repeating the call returns created: false with the same actor.id. The shape is replace-style — fields you omit will be overwritten by the new body, including metadata. Always include every field you want to keep on each upsert call.


3

List actors

Cursor-paginated, newest first. Default page size 50 (max 200). Pass ?cursor=<next_cursor> while has_more is true:

bash
curl -H "Authorization: Bearer pcft_live_..." \
  "https://social.productcraft.co/v1/communities/<community-uuid>/actors?limit=10"
response.json
{
  "data": [
    { "id": "76054baa-...", "external_id": "user_carol", "display_name": "Carol Diaz", ... },
    { "id": "a54d4499-...", "external_id": "user_bob",   "display_name": "Bob Park",   ... },
    { "id": "d6ee4836-...", "external_id": "user_alice", "display_name": "Alice Lin",  ... }
  ],
  "pagination": { "next_cursor": null, "has_more": false }
}

4

Resolve an external id → actor

On the wire, every Social call below this stage takes an social actor.id (a UUID). When you only have your customer-side external_id and need to resolve it, use the lookup helper:

bash
curl -H "Authorization: Bearer pcft_live_..." \
  https://social.productcraft.co/v1/communities/<community-uuid>/actors/by-external/user_carol

# returns the actor row, or 404

Resolving many at once (a migration backfill, a sync job)? POST /actors/by-external/batch takes { "external_ids": [...] } — up to 200 per call, POST because external_ids can be 256 chars — and returns the actors found plus a missing[] array for ids with no actor yet. The UUID mirror is GET /actors?ids=<uuid,uuid,…> (max 100).


5

Edit profile fields

PATCH /actors/:actorId takes a partial body and updates only the fields present:

bash
curl -X PATCH -H "Authorization: Bearer pcft_live_..." \
  -H "content-type: application/json" \
  -d '{
    "metadata": {
      "bio": "Travel photographer. Currently: Oaxaca.",
      "location": "Oaxaca, MX"
    }
  }' \
  https://social.productcraft.co/v1/communities/<community-uuid>/actors/<actor-uuid>

Mental model: in your app, an end-user has a row in your users table. Mirror that into Social once at signup time — just call POST /actorswith the user's id as external_id. From then on, your backend keeps user.id actor.id mapped however you like (a column on your users table works fine).

Store the renderable profile — display name, avatar, a bio in metadata — on the actor and keep it fresh on profile edits. That's what makes the rest of the API self-sufficient: followers / following / mutes / blocks lists take ?expand=<field> (see stage 5) and return these fields nested on each edge, so social screens render from one Social call with no join back to your own tables. The id mapping is for writes (acting as a user); reads shouldn't need it.