{"openapi":"3.0.0","paths":{"/v1/workspaces/{workspaceId}/communities":{"get":{"operationId":"CommunityController_list","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"ProductCraft workspace UUID owning the community.","schema":{"example":"11111111-1111-1111-1111-111111111111","type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size (default 50, max 200).","schema":{"example":50,"type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from a prior page's `pagination.next_cursor`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated communities.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommunityListResponseDto"}}}},"403":{"description":"Caller lacks `social.list`, or the workspace has not enabled the `social` service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List communities in a workspace, newest-first. Cursor-paginated as of task 008 / G13 (previously a bare array).","tags":["social-communities"]},"post":{"operationId":"CommunityController_create","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"ProductCraft workspace UUID owning the community.","schema":{"example":"11111111-1111-1111-1111-111111111111","type":"string"}},{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Replays return the original response with `Idempotent-Replay: true`; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCommunityDto"}}}},"responses":{"201":{"description":"Community created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommunityResponseDto"}}}},"400":{"description":"Validation failure (slug shape, display_name length, malformed Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"401":{"description":"`app_id` was supplied but no bearer/cookie was forwarded for the upstream Auth fetch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller lacks `social.create` or workspace has not enabled the `social` service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"`app_id` supplied but the Auth app was not found in this workspace (opaque cross-tenant 404).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Community slug already taken in this workspace, the supplied `app_id` is already connected to a community, or `IDEMPOTENCY_KEY_REUSE` (same Idempotency-Key, different body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Create an social community. `app_id` is optional — when supplied Social connects the community to an Auth app and refuses if the app belongs to a different workspace (cross-tenant gate) or is already connected. Omit `app_id` for a standalone community (no Auth integration; the customer manages actor identity). Pass an `Idempotency-Key` header to make retries safe.","tags":["social-communities"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}":{"get":{"operationId":"CommunityController_get","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"ProductCraft workspace UUID owning the community.","schema":{"example":"11111111-1111-1111-1111-111111111111","type":"string"}},{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"responses":{"200":{"description":"Community found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommunityResponseDto"}}}},"403":{"description":"Caller lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace (opaque cross-tenant 404).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get one community by id.","tags":["social-communities"]},"patch":{"operationId":"CommunityController_update","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"ProductCraft workspace UUID owning the community.","schema":{"example":"11111111-1111-1111-1111-111111111111","type":"string"}},{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCommunityDto"}}}},"responses":{"200":{"description":"Community updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommunityResponseDto"}}}},"400":{"description":"Validation failure or invalid status value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update community display name, description, settings, or status. Status transitions are unrestricted (active ↔ suspended; archived is terminal in v1).","tags":["social-communities"]},"delete":{"operationId":"CommunityController_delete","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"ProductCraft workspace UUID owning the community.","schema":{"example":"11111111-1111-1111-1111-111111111111","type":"string"}},{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"responses":{"204":{"description":"Community deleted."},"403":{"description":"Caller lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Delete a community. Cascades to every actor, post, comment, edge, flag, and notification it owns. Not reversible.","tags":["social-communities"]}},"/v1/communities/{communityId}/actors/batch":{"post":{"operationId":"ActorController_batchUpsert","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchUpsertActorsDto"}}}},"responses":{"201":{"description":"Per-item results, request order. `succeeded` + `failed` summarise the batch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchActorsResponseDto"}}}},"400":{"description":"Envelope validation failure: `actors` missing, empty, over 200 items, or an item shape invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create` for this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in the PAK's workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Bulk actor upsert (max 200 per call) — the import lane. Each item runs the exact single-upsert path; results come back per item with partial success (`201` even when some items fail — inspect `success` / `error` per entry). No wrapping transaction.","tags":["social-actors"]}},"/v1/communities/{communityId}/actors/by-external/batch":{"post":{"operationId":"ActorController_batchGetByExternal","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchExternalIdsDto"}}}},"responses":{"200":{"description":"Actors found (request order, de-duplicated) plus the `missing[]` external_ids.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorsByExternalBatchResponseDto"}}}},"400":{"description":"`external_ids` missing, empty, over 200 items, or an entry exceeding 256 chars.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in the PAK's workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Batch-resolve external_ids to actors (max 200 per call). POST-only — external_ids are up to 256 chars and can't ride a querystring. Unknown ids come back in `missing[]` instead of failing the call.","tags":["social-actors"]}},"/v1/communities/{communityId}/actors":{"post":{"operationId":"ActorController_upsert","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertActorDto"}}}},"responses":{"201":{"description":"Created (`created: true`) or upserted (`created: false`); the actor row is returned in both cases.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertActorResponseDto"}}}},"400":{"description":"Validation failure (external_id, display_name, avatar_url length).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create` for this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in the PAK's workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Upsert an actor by external_id. Creates on first call, updates on subsequent calls with the same external_id. Idempotent on `(community, external_id)`. Note: omitted fields on a follow-up call are overwritten with the new body — pass every field you want to keep on each call.","tags":["social-actors"]},"get":{"operationId":"ActorController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Page size; capped at 200. Ignored when `ids` is passed."},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque cursor from the previous page's `pagination.next_cursor`. Ignored when `ids` is passed."},{"name":"ids","required":false,"in":"query","schema":{"type":"string"},"description":"Comma-separated actor UUIDs (max 100, de-duplicated) to batch-fetch. Returns matching actors in request order; unknown ids are silently omitted."}],"responses":{"200":{"description":"Page of actors with `next_cursor` + `has_more` (batch-fetch responses always report `has_more: false`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorPageResponseDto"}}}},"400":{"description":"Invalid cursor (`INVALID_CURSOR`) — legacy raw-id cursors are rejected; restart from the first page and follow `pagination.next_cursor`. Or invalid `ids` (`INVALID_ID_LIST` — empty / non-UUID entry; `TOO_MANY_IDS` — over 100).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in the PAK's workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List actors in a community. 50 per page by default (max 200) — paginate with the cursor returned in `pagination.next_cursor` while `pagination.has_more` is true. Pass `?ids=<uuid,uuid,…>` (max 100) to batch-fetch specific actors instead — one hydration call for a screenful of ids; unknown ids are silently omitted and `pagination` is the exhausted shape (`next_cursor: null`).","tags":["social-actors"]}},"/v1/communities/{communityId}/actors/by-external/{externalId}":{"get":{"operationId":"ActorController_getByExternal","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"externalId","required":true,"in":"path","description":"Customer-supplied identifier for the user (the `external_id` you upserted with).","schema":{"example":"user_abc123","type":"string"}}],"responses":{"200":{"description":"Actor row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No actor with that external_id in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Look up an actor by the customer-supplied external_id.","tags":["social-actors"]}},"/v1/communities/{communityId}/actors/{actorId}":{"get":{"operationId":"ActorController_getById","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (the id returned by upsert).","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}}],"responses":{"200":{"description":"Actor row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorResponseDto"}}}},"404":{"description":"Actor not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get an actor by its UUID.","tags":["social-actors"]},"patch":{"operationId":"ActorController_update","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateActorDto"}}}},"responses":{"200":{"description":"Actor updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorResponseDto"}}}},"400":{"description":"Validation failure or invalid status value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Actor not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update actor profile fields. Pass only the fields you want to change.","tags":["social-actors"]},"delete":{"operationId":"ActorController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}}],"responses":{"204":{"description":"Actor deleted."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Actor not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Delete an actor. Hard delete — cascades to every post, comment, edge, flag, and notification it owns.","tags":["social-actors"]}},"/v1/communities/{communityId}/posts":{"post":{"operationId":"PostController_create","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostDto"}}}},"responses":{"201":{"description":"Post created. Defaults: `kind=text`, `visibility=public`, `status=published`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponseDto"}}}},"400":{"description":"Validation failure (missing actor_id; body/title/url all empty; bad URL; bad ISO timestamp; malformed Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`, or actor is suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, actor, or quote source post not found in the PAK's workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"`IDEMPOTENCY_KEY_REUSE` — the supplied Idempotency-Key was already used with a different request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Quote rules: `CANNOT_QUOTE_SELF` (kind='quote' with your own post as source) or `CANNOT_QUOTE_PRIVATE` (source post is private).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Create a post on behalf of an actor. Pass an `Idempotency-Key` header to make retries safe — replays return the original response with `Idempotent-Replay: true` for 24h.","tags":["Post"]},"get":{"operationId":"PostController_list","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque cursor from `pagination.next_cursor`."},{"name":"author_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Limit results to this actor."}],"responses":{"200":{"description":"Page of posts (only `published`, non-expired, public-visibility unless filtered by author).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List posts. Pass `author_id` to filter to a single author. 50/page (max 200), cursor-paginated.","tags":["Post"]}},"/v1/communities/{communityId}/posts/{postId}":{"get":{"description":"Without `actor_id`, only `public` posts return. With one, `followers` / `close_friends` / the requester's own `private` posts surface per the visibility matrix. Returns 404 (opacity) when the requester is not allowed to read the post — never 403.","operationId":"PostController_getById","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Social actor UUID of the viewer. Surfaces non-public posts they are entitled to see."},{"name":"requester_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Post.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post not found, removed, expired, or not visible to the requester.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Get a post by id. Visibility-aware: pass `actor_id` for non-public posts.","tags":["Post"]},"patch":{"operationId":"PostController_update","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePostDto"}}}},"responses":{"200":{"description":"Post updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponseDto"}}}},"400":{"description":"Validation failure or invalid status / visibility / timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"`EDIT_WINDOW_EXPIRED` (content edit after the community's edit window), or `scheduled_for` passed while the post is not `status='scheduled'`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Update post fields. `pinned` is the highlights toggle for stories; status transitions to `hidden`/`removed` should usually go through the moderation lane.","tags":["Post"]},"delete":{"operationId":"PostController_delete","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}}],"responses":{"204":{"description":"Post soft-deleted."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Soft-delete a post. The row is marked `removed`; viewers see 404. Comments and reactions are kept for moderation audit.","tags":["Post"]}},"/v1/communities/{communityId}/posts/{postId}/revisions":{"get":{"description":"Each entry is a snapshot of body/title/attributes from BEFORE the edit landed. The post row itself carries the current state plus `edited_at` + `edit_count`. Drafts are not snapshotted — only published posts incur revisions.","operationId":"PostController_listRevisions","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of revisions, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostRevisionListResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List a post's edit history. Cursor-paginated by revision_n desc (newest edit first).","tags":["Post"]}},"/v1/communities/{communityId}/posts/{postId}/quotes":{"get":{"description":"Cursor-paginated by `(created_at DESC, id DESC)`. Each entry is the full quote post row — body, attributes, the quoter's actor_id, the source_post_id back-reference. Filters to status=published.","operationId":"PostController_listQuotes","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"Source post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of quote posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or source post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List the quote posts of a source post (X-style \"show quotes\" link).","tags":["Post"]}},"/v1/communities/{communityId}/posts/impressions":{"post":{"operationId":"PostController_recordImpressions","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordViewsDto"}}}},"responses":{"204":{"description":"Impressions recorded (or no-op if every id was missing)."},"400":{"description":"Empty `post_ids`, more than 200 ids, or a non-UUID in the list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.view` on the community URN.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Batch-increment impression (`view_count`) counters on a list of posts. Client-side debounced; missing post ids are silently skipped (so a deleted post mid-batch doesn’t fail the rest). Renamed from `POST /posts/views` (task 008 / G14) to disambiguate from the story-side per-post `/posts/:postId/views` route.","tags":["Post"]}},"/v1/communities/{communityId}/actors/{actorId}/drafts":{"get":{"description":"Drafts are visible only via this endpoint — they're stripped from public feeds, lists, and `getById`. The PAK lane has no end-user principal, so the customer's backend is responsible for ensuring `actorId` matches the end-user behind the request before exposing the response.","operationId":"DraftController_listDrafts","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID whose drafts to list.","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque cursor from `pagination.next_cursor`."}],"responses":{"200":{"description":"Page of draft posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actor's drafts (status='draft'). Cursor-paginated newest-first.","tags":["social-drafts"]}},"/v1/communities/{communityId}/actors/{actorId}/scheduled-posts":{"get":{"description":"Scheduled posts are invisible to everyone except the author until the per-minute PostSchedulerCron promotes them. Same end-user authorization rules as drafts: the PAK lane trusts the customer-backend to gate the caller.","operationId":"DraftController_listScheduledPosts","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID whose scheduled posts to list.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of scheduled posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actor's scheduled posts (status='scheduled'), ordered soonest-to-publish first. Cursor-paginated.","tags":["social-drafts"]}},"/v1/communities/{communityId}/actors/{actorId}/archived":{"get":{"description":"Archived posts keep their engagement (reaction / comment counts intact) but fall out of every public surface, and `getById` treats them as author-only. Restore one with PATCH `{ status: 'published' }`. Same end-user authorization rules as drafts: the PAK lane trusts the customer-backend to gate the caller.","operationId":"DraftController_listArchived","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID whose archived posts to list.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of archived posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actor's archived posts (status='archived'), newest-first. Cursor-paginated.","tags":["social-drafts"]}},"/v1/communities/{communityId}/posts/{postId}/repost":{"post":{"description":"Visibility is inherited from the source. The reposter must be allowed to read the source (else 404). Self-repost 422; private-source repost 422. Bumps `source.repost_count` and the reposter's `post_count`. Fires a `repost` notification to the source author (subject to per-kind preferences from task 021).","operationId":"RepostController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Source post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRepostDto"}}}},"responses":{"201":{"description":"Repost created (or returned via idempotent retry).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRepostResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / source / actor not found, or source invisible to the actor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"CANNOT_REPOST_SELF (reposting your own post) or CANNOT_REPOST_PRIVATE (source is visibility='private').","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Repost a source post on behalf of an actor. Idempotent — returns the existing repost row with `created: false` on retry.","tags":["social-reposts"]},"delete":{"operationId":"RepostController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Source post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"actor_id","required":true,"in":"query","description":"Acting actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeprecatedActorIdBodyDto"}}}},"responses":{"204":{"description":"Repost soft-deleted (or was never present)."},"400":{"description":"Validation failure (missing/invalid actor_id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / source / actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Undo my repost of a source post. Idempotent — 204 either way. Pass the actor via `?actor_id=`.","tags":["social-reposts"]}},"/v1/communities/{communityId}/posts/{postId}/reposts":{"get":{"operationId":"RepostController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Source post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of repost rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepostListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or source not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actors who have reposted a source post. Cursor-paginated newest-first. Useful for \"X reposted this\" UI.","tags":["social-reposts"]}},"/v1/communities/{communityId}/assets":{"post":{"operationId":"AssetController_upload","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Makes retries safe.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["owner_actor_id"],"properties":{"file":{"type":"string","format":"binary","description":"PNG / JPEG / WEBP, ≤10 MB — image path (multipart) only."},"owner_actor_id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["image","video"],"default":"image","description":"Pass 'video' (JSON body) for the presigned-PUT flow."},"content_type":{"type":"string","enum":["video/mp4","video/quicktime","video/webm"],"description":"Video path: declared container type."},"byte_size":{"type":"integer","minimum":1,"maximum":536870912,"description":"Video path: declared size in bytes (≤512 MB)."}}}},"application/json":{"schema":{"type":"object","required":["owner_actor_id"],"properties":{"file":{"type":"string","format":"binary","description":"PNG / JPEG / WEBP, ≤10 MB — image path (multipart) only."},"owner_actor_id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["image","video"],"default":"image","description":"Pass 'video' (JSON body) for the presigned-PUT flow."},"content_type":{"type":"string","enum":["video/mp4","video/quicktime","video/webm"],"description":"Video path: declared container type."},"byte_size":{"type":"integer","minimum":1,"maximum":536870912,"description":"Video path: declared size in bytes (≤512 MB)."}}}}}},"responses":{"201":{"description":"Image stored and ready, or video created in `uploading` with `upload` instructions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadAssetResponseDto"}}}},"400":{"description":"Missing/oversize/wrong-type file or declaration, or the bytes do not match the declared image type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.assets.write`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or owner actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"The image could not be decoded/processed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"429":{"description":"`TOO_MANY_ACTIVE_UPLOADS` — the community already has the max videos uploading/processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Upload media. Images (multipart): processed synchronously, returns ready. Videos (JSON, kind:'video' + declared content_type/byte_size): returns an `uploading` asset with a presigned PUT — upload the bytes, then POST /assets/:id/complete.","tags":["social-assets"]}},"/v1/communities/{communityId}/assets/{assetId}/complete":{"post":{"operationId":"AssetController_complete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"assetId","required":true,"in":"path","description":"Video asset UUID (status `uploading`).","schema":{"example":"eeeeeeee-0000-0000-0000-000000000001","type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Makes retries safe.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Finalized (`processing`) — or the current status on replay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadAssetResponseDto"}}}},"403":{"description":"PAK lacks `social.assets.write`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Asset not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"`ASSET_UPLOAD_INCOMPLETE` (no object / size mismatch — still retryable), `ASSET_TOO_LARGE` or `ASSET_INVALID_CONTAINER` (upload failed + object deleted), or `ASSET_NOT_VIDEO`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Finalize a presigned-PUT video upload. Verifies the stored object (existence, size, container magic bytes) and enqueues the transcode. Replaying on a finalized asset returns its current status without re-enqueueing.","tags":["social-assets"]}},"/v1/communities/{communityId}/assets/{assetId}":{"get":{"operationId":"AssetController_get","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The asset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}},"403":{"description":"PAK lacks `social.assets.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Asset not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Fetch one asset with freshly-minted presigned rendition URLs.","tags":["social-assets"]},"delete":{"operationId":"AssetController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Asset deleted."},"403":{"description":"PAK lacks `social.assets.write`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Asset not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Delete an asset — removes every stored rendition object, then the row.","tags":["social-assets"]}},"/v1/communities/{communityId}/actors/{actorId}/assets":{"get":{"operationId":"AssetController_listByActor","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Owner actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of the actor's assets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetListResponseDto"}}}},"403":{"description":"PAK lacks `social.assets.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List an actor's uploaded assets — the \"your uploads\" surface + GDPR export walk. 50/page (max 200), cursor-paginated newest-first.","tags":["social-assets"]}},"/v1/communities/{communityId}/counters":{"get":{"operationId":"CounterController_getBatchCounters","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actor_ids","required":true,"in":"query","schema":{"type":"string"},"description":"Comma-separated actor UUIDs (max 100, de-duplicated)."}],"responses":{"200":{"description":"Counters for each actor found plus the `missing[]` ids.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchCountersResponseDto"}}}},"400":{"description":"`actor_ids` missing or empty, a non-UUID entry (`INVALID_ID_LIST`), or over 100 ids (`TOO_MANY_IDS`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in the PAK's workspace. (Unknown actors land in `missing[]`, not a 404.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Batch counters (max 100 actors per call) — render a member list with follower badges from ONE call instead of one per actor. Unknown actor_ids come back in `missing[]` instead of failing the read.","tags":["social-counters"]}},"/v1/communities/{communityId}/actors/{actorId}/counters":{"get":{"operationId":"CounterController_getCounters","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (returned from POST /actors).","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}}],"responses":{"200":{"description":"Counters for the actor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorCountersResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not found (or belongs to a different community / workspace).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Denormalised counters for one actor (followers, following, posts, comments, blocks).","tags":["social-counters"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/analytics":{"get":{"operationId":"AnalyticsController_getCommunityAnalytics","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"ProductCraft workspace UUID.","schema":{"example":"11111111-1111-1111-1111-111111111111","type":"string"}},{"name":"communityId","required":true,"in":"path","description":"Social community UUID owned by the workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"responses":{"200":{"description":"Computed analytics rollup.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommunityAnalyticsResponseDto"}}}},"403":{"description":"Caller lacks `social.analytics.read`, or the workspace has not enabled the `social` service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace (opaque cross-tenant 404).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Community-level analytics rollup: total active actors, published posts, published comments, open flags, and last-7-day windows for each.","tags":["social-analytics"]}},"/v1/communities/{communityId}/follows/batch":{"post":{"operationId":"EdgeController_batchFollows","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchFollowsDto"}}}},"responses":{"201":{"description":"Per-item results, request order. `succeeded` + `failed` summarise the batch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchFollowsResponseDto"}}}},"400":{"description":"Envelope validation failure: `follows` missing, empty, over 200 items, or an item shape invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in the PAK's workspace. (Unknown actors fail per item, not the whole call.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Bulk follow creation (max 200 per call) — the graph-import lane. Each item runs the exact single-follow path (block checks, counters, idempotency), so a blocked pair fails ITS item with a 409 in `error` while the rest of the batch proceeds. `201` even with failures — inspect per-item results. Set `suppress_notifications: true` when importing an existing graph.","tags":["social-edges"]}},"/v1/communities/{communityId}/follows":{"post":{"operationId":"EdgeController_follow","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorEdgeDto"}}}},"responses":{"201":{"description":"Follow edge created / exists (`status: followed`), or a follow request created (`status: pending`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowResultDto"}}}},"400":{"description":"Missing / invalid src_actor_id / dst_actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or one of the actors not found in the PAK's workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A block exists between the pair (in either direction).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot follow yourself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Follow another actor. Idempotent. If the destination is private (task 014), a follow request is created instead and the response is `{ status: \"pending\", edge: null }`; approve/decline it via the follow-request routes. Refused if either side has a block.","tags":["social-edges"]}},"/v1/communities/{communityId}/follow-requests/{dstActorId}/{srcActorId}/approve":{"post":{"operationId":"EdgeController_approveFollowRequest","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"dstActorId","required":true,"in":"path","description":"The private actor being followed (approver).","schema":{"type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"The requesting actor.","schema":{"type":"string"}}],"responses":{"201":{"description":"Request approved; follow edge created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEdgeResponseDto"}}}},"404":{"description":"No pending request for this pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Approve a pending follow request (task 014). The destination (private) actor approves the requesting actor; a follow edge is created.","tags":["social-edges"]}},"/v1/communities/{communityId}/follow-requests/{dstActorId}/{srcActorId}":{"delete":{"operationId":"EdgeController_declineFollowRequest","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"dstActorId","required":true,"in":"path","description":"The private actor (decliner).","schema":{"type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"The requesting actor.","schema":{"type":"string"}}],"responses":{"204":{"description":"Request declined."},"404":{"description":"No pending request for this pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Decline a pending follow request (task 014). Deletes the request; no edge, no notification.","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/follow-requests":{"get":{"operationId":"EdgeController_listIncomingRequests","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"The private actor whose incoming requests to list.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of incoming follow requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRequestListResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List incoming follow requests for a (private) actor, newest-first (task 014).","tags":["social-edges"]}},"/v1/communities/{communityId}/follows/{srcActorId}/{dstActorId}":{"delete":{"operationId":"EdgeController_unfollow","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"Follower actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"dstActorId","required":true,"in":"path","description":"Followee actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000002","type":"string"}}],"responses":{"204":{"description":"Unfollowed (or no edge to remove)."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unfollow. Idempotent — 204 even if the follow didn't exist.","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/following":{"get":{"operationId":"EdgeController_listFollowing","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"expand","required":false,"in":"query","schema":{"type":"string","enum":["dst_actor"]},"description":"Stripe-style expansion. Pass the response field to expand (supported here: `dst_actor`) and the full object is nested under that field on every item. Omit for the unexpanded (default) shape. Unsupported values return 400 `INVALID_EXPAND`."}],"responses":{"200":{"description":"Page of follow edges.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundEdgeListResponseDto"}}}},"400":{"description":"Invalid cursor (`INVALID_CURSOR`) — legacy raw-id cursors are rejected; restart from the first page and follow `pagination.next_cursor`. Or unsupported `expand` value (`INVALID_EXPAND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actors this actor follows. Pass `?expand=dst_actor` to nest each followee's full actor row on the edge (no N+1 hydration).","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/followers":{"get":{"operationId":"EdgeController_listFollowers","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"expand","required":false,"in":"query","schema":{"type":"string","enum":["src_actor"]},"description":"Stripe-style expansion. Pass the response field to expand (supported here: `src_actor`) and the full object is nested under that field on every item. Omit for the unexpanded (default) shape. Unsupported values return 400 `INVALID_EXPAND`."}],"responses":{"200":{"description":"Page of follow edges.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowerEdgeListResponseDto"}}}},"400":{"description":"Invalid cursor (`INVALID_CURSOR`) — legacy raw-id cursors are rejected; restart from the first page and follow `pagination.next_cursor`. Or unsupported `expand` value (`INVALID_EXPAND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actors who follow this actor. Pass `?expand=src_actor` to nest each follower's full actor row on the edge — a followers screen renders from this one call.","tags":["social-edges"]}},"/v1/communities/{communityId}/mutes":{"post":{"operationId":"EdgeController_mute","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorEdgeDto"}}}},"responses":{"201":{"description":"Mute edge created (or returned via idempotent retry).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEdgeResponseDto"}}}},"400":{"description":"Missing / invalid src_actor_id / dst_actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actors not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block exists between the pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot mute yourself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Mute another actor. One-directional and silent — the muted actor isn't notified. Hides their content from the muter's feed.","tags":["social-edges"]}},"/v1/communities/{communityId}/mutes/{srcActorId}/{dstActorId}":{"delete":{"operationId":"EdgeController_unmute","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"Muter actor UUID.","schema":{"type":"string"}},{"name":"dstActorId","required":true,"in":"path","description":"Mutee actor UUID.","schema":{"type":"string"}}],"responses":{"204":{"description":"Unmuted (or no edge to remove)."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unmute. Idempotent — 204 either way.","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/mutes":{"get":{"operationId":"EdgeController_listMutes","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"expand","required":false,"in":"query","schema":{"type":"string","enum":["dst_actor"]},"description":"Stripe-style expansion. Pass the response field to expand (supported here: `dst_actor`) and the full object is nested under that field on every item. Omit for the unexpanded (default) shape. Unsupported values return 400 `INVALID_EXPAND`."}],"responses":{"200":{"description":"Page of mute edges.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundEdgeListResponseDto"}}}},"400":{"description":"Invalid cursor (`INVALID_CURSOR`) — legacy raw-id cursors are rejected; restart from the first page and follow `pagination.next_cursor`. Or unsupported `expand` value (`INVALID_EXPAND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List actors this actor has muted. Pass `?expand=dst_actor` to nest each muted actor's full row on the edge.","tags":["social-edges"]}},"/v1/communities/{communityId}/hides":{"post":{"operationId":"EdgeController_hidePost","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HidePostDto"}}}},"responses":{"201":{"description":"Post hidden.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HideResultDto"}}}},"404":{"description":"Community, actor, or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Hide a post from the actor's feeds (\"not interested\"). Idempotent. A hard filter — the post drops out of home + discover for that actor only.","tags":["social-edges"]}},"/v1/communities/{communityId}/hides/{srcActorId}/{postId}":{"delete":{"operationId":"EdgeController_unhidePost","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"The actor who hid the post.","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"The hidden post UUID.","schema":{"type":"string"}}],"responses":{"204":{"description":"Un-hidden (or no-op)."}},"security":[{"bearer":[]}],"summary":"Un-hide a post. Idempotent — 204 either way.","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/hides":{"get":{"operationId":"EdgeController_listHides","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of hidden posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HideListResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the posts an actor has hidden, newest-first.","tags":["social-edges"]}},"/v1/communities/{communityId}/dismissed-suggestions":{"post":{"operationId":"EdgeController_dismissSuggestion","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorEdgeDto"}}}},"responses":{"201":{"description":"Suggestion dismissed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEdgeResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot dismiss yourself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Dismiss an actor from your follow suggestions (\"don't suggest\"). Idempotent. The actor never re-appears in your suggestions.","tags":["social-edges"]}},"/v1/communities/{communityId}/dismissed-suggestions/{srcActorId}/{dstActorId}":{"delete":{"operationId":"EdgeController_undismissSuggestion","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"The dismissing actor UUID.","schema":{"type":"string"}},{"name":"dstActorId","required":true,"in":"path","description":"The dismissed actor UUID.","schema":{"type":"string"}}],"responses":{"204":{"description":"Un-dismissed (or no-op)."}},"security":[{"bearer":[]}],"summary":"Un-dismiss a suggestion. Idempotent — 204 either way. The actor becomes eligible for suggestions again.","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/dismissed-suggestions":{"get":{"operationId":"EdgeController_listDismissedSuggestions","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"expand","required":false,"in":"query","schema":{"type":"string","enum":["dst_actor"]},"description":"Stripe-style expansion. Pass the response field to expand (supported here: `dst_actor`) and the full object is nested under that field on every item. Omit for the unexpanded (default) shape. Unsupported values return 400 `INVALID_EXPAND`."}],"responses":{"200":{"description":"Page of dismissed-suggestion edges.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundEdgeListResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List actors this actor has dismissed from suggestions. Pass `?expand=dst_actor` to nest each actor's full row.","tags":["social-edges"]}},"/v1/communities/{communityId}/blocks":{"post":{"operationId":"EdgeController_block","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorEdgeDto"}}}},"responses":{"201":{"description":"Block edge created (or returned via idempotent retry); follows + mutes between the pair are torn down in the same transaction.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEdgeResponseDto"}}}},"400":{"description":"Missing / invalid src_actor_id / dst_actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actors not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot block yourself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Block another actor. Removes any existing follows/mutes between the two in either direction (one transaction).","tags":["social-edges"]}},"/v1/communities/{communityId}/blocks/{srcActorId}/{dstActorId}":{"delete":{"operationId":"EdgeController_unblock","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"Blocker actor UUID.","schema":{"type":"string"}},{"name":"dstActorId","required":true,"in":"path","description":"Blockee actor UUID.","schema":{"type":"string"}}],"responses":{"204":{"description":"Unblocked (or no edge to remove)."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unblock. Idempotent — 204 either way. Does NOT restore prior follows / mutes (the block tear-down is one-way).","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/blocks":{"get":{"operationId":"EdgeController_listBlocks","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"expand","required":false,"in":"query","schema":{"type":"string","enum":["dst_actor"]},"description":"Stripe-style expansion. Pass the response field to expand (supported here: `dst_actor`) and the full object is nested under that field on every item. Omit for the unexpanded (default) shape. Unsupported values return 400 `INVALID_EXPAND`."}],"responses":{"200":{"description":"Page of block edges.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundEdgeListResponseDto"}}}},"400":{"description":"Invalid cursor (`INVALID_CURSOR`) — legacy raw-id cursors are rejected; restart from the first page and follow `pagination.next_cursor`. Or unsupported `expand` value (`INVALID_EXPAND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List actors this actor has blocked. Pass `?expand=dst_actor` to nest each blocked actor's full row on the edge.","tags":["social-edges"]}},"/v1/communities/{communityId}/restricts":{"post":{"operationId":"EdgeController_restrict","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorEdgeDto"}}}},"responses":{"201":{"description":"Restrict edge created (or returned via idempotent retry).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEdgeResponseDto"}}}},"400":{"description":"Missing / invalid src_actor_id / dst_actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actors not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block exists between the pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot restrict yourself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Restrict another actor (Instagram-style). One-directional. Restricted actors can still see content, but new comments by them are pre-moderated as `pending_approval` until the post-author approves.","tags":["social-edges"]}},"/v1/communities/{communityId}/restricts/{srcActorId}/{dstActorId}":{"delete":{"operationId":"EdgeController_unrestrict","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"Restrictor actor UUID.","schema":{"type":"string"}},{"name":"dstActorId","required":true,"in":"path","description":"Restricted actor UUID.","schema":{"type":"string"}}],"responses":{"204":{"description":"Unrestricted (or no edge to remove)."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unrestrict. Idempotent — 204 either way.","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{actorId}/restricts":{"get":{"operationId":"EdgeController_listRestricts","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"expand","required":false,"in":"query","schema":{"type":"string","enum":["dst_actor"]},"description":"Stripe-style expansion. Pass the response field to expand (supported here: `dst_actor`) and the full object is nested under that field on every item. Omit for the unexpanded (default) shape. Unsupported values return 400 `INVALID_EXPAND`."}],"responses":{"200":{"description":"Page of restrict edges.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundEdgeListResponseDto"}}}},"400":{"description":"Invalid cursor (`INVALID_CURSOR`) — legacy raw-id cursors are rejected; restart from the first page and follow `pagination.next_cursor`. Or unsupported `expand` value (`INVALID_EXPAND`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List actors this actor has restricted. Pass `?expand=dst_actor` to nest each restricted actor's full row on the edge.","tags":["social-edges"]}},"/v1/communities/{communityId}/actors/{srcActorId}/relationships":{"get":{"description":"One query resolves the seven pairwise flags for a whole profile header or member list — no paging of full edge lists, and `blocked_by` is visible without attempting a write and handling the 409. Unknown or cross-community ids resolve to all-false flags; the id list is de-duplicated preserving request order. Bounded input (max 100 ids), so the response has no pagination envelope.","operationId":"EdgeController_relationships","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"Source actor UUID — the perspective all flags are computed from.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"with","required":true,"in":"query","schema":{"type":"string"},"description":"Comma-separated destination actor UUIDs (1-100)."}],"responses":{"200":{"description":"One relationship entry per requested id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelationshipListResponseDto"}}}},"400":{"description":"Missing `with`, more than 100 ids, or a non-UUID value in the list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or source actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Batched pairwise relationship lookup: does this actor follow / mute / block / restrict each of up to 100 others (and is it followed / blocked back)?","tags":["social-edges"]}},"/v1/communities/{communityId}/connections":{"post":{"operationId":"ConnectionController_request","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionRequestDto"}}}},"responses":{"201":{"description":"`pending` (request created / already open) or `connected` (both edges in place).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionResultDto"}}}},"400":{"description":"Missing / invalid src_actor_id / dst_actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A block exists between the pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot connect with yourself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Request a connection (mutual follow). Runs the standard follow path: a private destination yields `pending` (accept via the accept route); a public destination connects immediately — the reverse edge is created in the same call. Idempotent; refused on blocks (409) and self-connection (422) exactly like a follow.","tags":["connections"]}},"/v1/communities/{communityId}/connections/{dstActorId}/{srcActorId}/accept":{"post":{"operationId":"ConnectionController_accept","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"dstActorId","required":true,"in":"path","schema":{"type":"string"}},{"name":"srcActorId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"Connected — both follow edges exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionResultDto"}}}},"400":{"description":"Malformed actor id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community/actor not found, or no request to accept and src does not follow dst.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Accept `srcActorId`'s connection request as `dstActorId`: approves the follow request AND creates the reverse follow edge, so both directions exist afterwards. Convergent on retry — if the request was already consumed by a partially-completed accept (src already follows dst) the reverse edge is still ensured.","tags":["connections"]}},"/v1/communities/{communityId}/connections/{actorId}/{otherActorId}":{"delete":{"operationId":"ConnectionController_sever","parameters":[{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","schema":{"type":"string"}},{"name":"otherActorId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Severed (or nothing to sever)."},"400":{"description":"Malformed actor id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Sever a connection COMPLETELY: deletes the follow edges in both directions and withdraws any pending requests either way. There is no one-sided removal — this is the connections invariant. 204 whether or not anything existed.","tags":["connections"]}},"/v1/communities/{communityId}/hashtags":{"get":{"operationId":"HashtagController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"order","required":false,"in":"query","schema":{"type":"string","enum":["recent","trending"],"default":"recent"},"description":"`recent` (default) = newest tags first; `trending` = most posts in the window."},{"name":"window","required":false,"in":"query","schema":{"type":"string","enum":["24h","7d"],"default":"24h"},"description":"Trending window (only meaningful with `order=trending`)."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of hashtags.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagListResponseDto"}}}},"400":{"description":"Invalid order / window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List hashtags in this community. Default order is `first_used_at` desc; `order=trending` ranks by windowed post count (task 023) — pass `window=24h|7d`. `window_post_count` is populated only when trending.","tags":["social-hashtags"]}},"/v1/communities/{communityId}/hashtags/{tag}":{"get":{"operationId":"HashtagController_getOne","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"tag","required":true,"in":"path","description":"Lowercase tag (without `#`).","schema":{"example":"storymode","type":"string"}}],"responses":{"200":{"description":"Hashtag detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagDetailResponseDto"}}}},"400":{"description":"Tag fails the lowercase + alphanum + underscore + 1..32 rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or tag not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get a single hashtag with its current published post count.","tags":["social-hashtags"]}},"/v1/communities/{communityId}/search/hashtags":{"get":{"description":"Backed by a `text_pattern_ops` partial index — sub-100ms for any prefix length up to communities with millions of tags. Results are alphabetically ordered.","operationId":"HashtagController_autocomplete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"q","required":true,"in":"query","schema":{"type":"string"},"description":"Lowercase prefix (1..32 chars)."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Matching hashtags.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagAutocompleteResponseDto"}}}},"400":{"description":"q is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Prefix-search hashtags for autocomplete.","tags":["social-hashtags","social-search"]}},"/v1/communities/{communityId}/actors/{actorId}/hashtag-follows":{"post":{"operationId":"HashtagFollowController_follow","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID who is following.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateHashtagFollowDto"}}}},"responses":{"201":{"description":"Follow created (or refreshed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagFollowResponseDto"}}}},"400":{"description":"Tag fails the lowercase + alphanum + underscore + 1..32 rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, actor, or hashtag not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Follow a hashtag on behalf of an actor. Idempotent — re-follow refreshes the followed_at timestamp but doesn't error.","tags":["social-hashtag-follows"]},"get":{"operationId":"HashtagFollowController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of followed hashtags.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagFollowListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the hashtags an actor follows. Cursor-paginated newest-first by followed_at.","tags":["social-hashtag-follows"]}},"/v1/communities/{communityId}/actors/{actorId}/hashtag-follows/{tag}":{"delete":{"operationId":"HashtagFollowController_unfollow","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"tag","required":true,"in":"path","description":"Lowercase tag.","schema":{"example":"storymode","type":"string"}}],"responses":{"204":{"description":"Unfollowed (or was never followed)."},"400":{"description":"Tag malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unfollow a hashtag. Idempotent — 204 either way.","tags":["social-hashtag-follows"]}},"/v1/communities/{communityId}/posts/{postId}/insights":{"get":{"operationId":"InsightsController_postInsights","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"Window start (YYYY-MM-DD)."},{"name":"to","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"Window end (YYYY-MM-DD)."}],"responses":{"200":{"description":"Post insights.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostInsightsResponseDto"}}}},"400":{"description":"Invalid window (inverted or > 366 days).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.analytics.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Aggregate insights for a post: total views, unique reach, engagement counts, and a daily views/reach series over ?from/?to (default trailing 30 days).","tags":["social-insights"]}},"/v1/communities/{communityId}/actors/{actorId}/insights":{"get":{"operationId":"InsightsController_actorInsights","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","schema":{"type":"string","format":"date"}},{"name":"to","required":false,"in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Actor insights.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorInsightsResponseDto"}}}},"400":{"description":"Invalid window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.analytics.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Actor not found in this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"An actor's follower time series: live follower_count plus a nightly-snapshot daily series over ?from/?to (default trailing 30 days).","tags":["social-insights"]}},"/v1/communities/{communityId}/posts/{postId}/tags/{actorId}/approve":{"post":{"description":"The tagged actor accepts being tagged; the tag flips `pending → approved` and the post starts surfacing on their `/tagged` listing. 403 if the acting actor is not the tagged actor. 409 if the tag was already removed.","operationId":"PostTagController_approve","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"Post UUID.","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Tagged actor UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActingActorDto"}}}},"responses":{"201":{"description":"Tag approved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostTagResponseDto"}}}},"403":{"description":"Acting actor is not the tagged actor."},"404":{"description":"Post or tag not found."},"409":{"description":"Tag already removed."}},"security":[{"bearer":[]}],"summary":"Approve a pending tag (tagged actor only).","tags":["social-tags"]}},"/v1/communities/{communityId}/posts/{postId}/tags/{actorId}/remove":{"post":{"description":"Flips the tag to `removed`. Removable by either the tagged actor or the post author (403 otherwise). Idempotent on an already-removed tag.","operationId":"PostTagController_remove","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","description":"Post UUID.","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Tagged actor UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActingActorDto"}}}},"responses":{"201":{"description":"Tag removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostTagResponseDto"}}}},"403":{"description":"Acting actor is neither the tagged actor nor the post author."},"404":{"description":"Post or tag not found."}},"security":[{"bearer":[]}],"summary":"Remove a tag (tagged actor or post author).","tags":["social-tags"]}},"/v1/communities/{communityId}/actors/{actorId}/tagged":{"get":{"description":"Approved tags only, on published + unexpired posts, newest-tag-first. Visibility- and block-aware: pass the viewer via `requester_id` to surface followers / close-friends posts they may read; without it only public posts appear. Pages may be short (post-visibility filtering) but never skip or duplicate.","operationId":"PostTagController_listTagged","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","description":"The tagged actor UUID.","schema":{"type":"string"}},{"name":"requester_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor UUID for visibility resolution."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of tagged posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaggedPostsResponse"}}}},"404":{"description":"Community or actor not found."}},"security":[{"bearer":[]}],"summary":"List posts an actor is tagged in.","tags":["social-tags"]}},"/v1/communities/{communityId}/posts/{postId}/comments":{"post":{"operationId":"CommentController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Replays return the original response with `Idempotent-Replay: true`; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCommentDto"}}}},"responses":{"201":{"description":"Comment created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponseDto"}}}},"400":{"description":"Validation failure (missing actor_id, empty body, body too long, malformed Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, post, or parent comment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block exists between commenter and post author, depth exceeds community.settings.max_comment_depth, or `IDEMPOTENCY_KEY_REUSE` (same Idempotency-Key, different body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Add a comment on a post. Pass `parent_id` to reply; the depth is computed and capped by the community config. Pass an `Idempotency-Key` header to make retries safe.","tags":["social-comments"]},"get":{"operationId":"CommentController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"parent_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"List replies of this parent comment instead of top-level."},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor UUID — surfaces author_only replies they're entitled to see."},{"name":"requester_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of comments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List comments on a post. Default returns top-level comments newest-first; pass `parent_id` to expand a thread (oldest-first). Pass `actor_id` so DM-style `author_only` replies surface only to the post-author and the comment-author pair.","tags":["social-comments"]}},"/v1/communities/{communityId}/posts/{postId}/comments/{commentId}":{"get":{"operationId":"CommentController_getById","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Social comment UUID.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor UUID."},{"name":"requester_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Comment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Comment not found, removed, or not visible to the requester.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get a single comment by id.","tags":["social-comments"]},"patch":{"operationId":"CommentController_update","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Social comment UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCommentDto"}}}},"responses":{"200":{"description":"Comment updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponseDto"}}}},"400":{"description":"Validation failure or invalid status / visibility.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Comment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update a comment's body, status, or visibility.","tags":["social-comments"]},"delete":{"operationId":"CommentController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Social comment UUID.","schema":{"type":"string"}}],"responses":{"204":{"description":"Comment soft-deleted."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Comment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Soft-delete a comment.","tags":["social-comments"]}},"/v1/communities/{communityId}/posts/{postId}/comments/{commentId}/approve":{"post":{"operationId":"CommentController_approve","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Social comment UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveCommentDto"}}}},"responses":{"200":{"description":"Comment approved (now published).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponseDto"}}}},"400":{"description":"Missing or invalid approver_actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not the post-author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Comment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Comment is in a status that cannot be approved (hidden / removed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Approve a pending_approval comment (restricted-actor pre-moderation). Reserved to the post-author. No-op if already published.","tags":["social-comments"]}},"/v1/communities/{communityId}/posts/{postId}/comments/{commentId}/pin":{"post":{"description":"Pins a published top-level comment so it leads the top-level listing. Reserved to the post author (403 otherwise). 409 on a reply, a non-published comment, or when the per-post pin cap (community.settings.max_pinned_comments, default 3) is reached. Idempotent.","operationId":"CommentController_pin","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Social comment UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PinCommentDto"}}}},"responses":{"201":{"description":"Comment pinned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponseDto"}}}},"403":{"description":"Caller is not the post-author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post or comment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Reply / non-published comment, or pin cap reached (`PIN_CAP_REACHED`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Pin a top-level comment (post-author only).","tags":["social-comments"]},"delete":{"description":"Clears the pin; the comment returns to its chronological position. Reserved to the post author (403). Idempotent. Pass the acting actor via the request body.","operationId":"CommentController_unpin","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID the comments thread under.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Social comment UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PinCommentDto"}}}},"responses":{"200":{"description":"Comment unpinned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentResponseDto"}}}},"403":{"description":"Caller is not the post-author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post or comment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unpin a comment (post-author only).","tags":["social-comments"]}},"/v1/communities/{communityId}/posts/{postId}/comments/{commentId}/reactions":{"post":{"operationId":"CommentReactionController_add","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Parent post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Comment UUID.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCommentReactionDto"}}}},"responses":{"201":{"description":"Reaction added; response carries live `reaction_counts`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCommentReactionResponseDto"}}}},"400":{"description":"Validation failure (missing actor_id; empty / overlong type).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / post / comment / actor not found, or comment is invisible to the actor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block exists between actor and comment author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"React to a comment. Idempotent: re-posting the same `(actor, type)` returns the existing row with `created: false`. Multiple reactions per `(actor, comment)` are allowed (different `type` values count separately).","tags":["social-comment-reactions"]},"get":{"operationId":"CommentReactionController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Parent post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Comment UUID.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of reactions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentReactionListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / post / comment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List reactions on a comment, newest-first. Cursor encodes `(created_at, actor_id, type)` so multi-reaction-from-same-actor stays stable.","tags":["social-comment-reactions"]}},"/v1/communities/{communityId}/posts/{postId}/comments/{commentId}/reactions/{srcActorId}/{type}":{"delete":{"operationId":"CommentReactionController_remove","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Parent post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"commentId","required":true,"in":"path","description":"Comment UUID.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"Reactor actor UUID.","schema":{"type":"string"}},{"name":"type","required":true,"in":"path","description":"Reaction type label (matches the value passed at create time).","schema":{"example":"like","type":"string"}}],"responses":{"200":{"description":"Reaction removed (or already absent); response carries live `reaction_counts`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentReactionCountsResponseDto"}}}},"400":{"description":"Missing path parameter `type`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / post / comment / actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unreact. Idempotent: returns 200 with current `reaction_counts` even if there was nothing to remove.","tags":["social-comment-reactions"]}},"/v1/communities/{communityId}/actors/{actorId}/muted-terms":{"post":{"operationId":"MutedTermController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMutedTermDto"}}}},"responses":{"201":{"description":"Muted term applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutedTermResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Add (or refresh) a muted term for the actor. Re-posting the same `term` updates the scope + expires_at.","tags":["social-muted-terms"]},"get":{"operationId":"MutedTermController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","description":"Page size (default 50, max 200).","schema":{"example":50,"type":"string"}},{"name":"cursor","required":false,"in":"query","description":"Opaque cursor from a prior page's `pagination.next_cursor`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Active muted terms, paginated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutedTermListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actor's currently-active muted terms, newest-first. Cursor-paginated (task 008 / G13 — previously an unbounded bare array).","tags":["social-muted-terms"]}},"/v1/communities/{communityId}/actors/{actorId}/muted-terms/{term}":{"delete":{"operationId":"MutedTermController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"term","required":true,"in":"path","description":"URL-encoded term.","schema":{"type":"string"}}],"responses":{"204":{"description":"Muted term removed."},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Remove a muted term. Idempotent — 204 either way.","tags":["social-muted-terms"]}},"/v1/communities/{communityId}/actors/{actorId}/feed":{"get":{"operationId":"FeedController_getFeed","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Viewer social actor UUID — feed is candidate-pooled to their follow-graph.","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque cursor; not interchangeable between `ranked` and `chronological` modes."},{"name":"order","required":false,"in":"query","schema":{"type":"string","enum":["ranked","chronological"],"default":"ranked"}}],"responses":{"200":{"description":"Page of posts (only `published`, non-expired, visibility-filtered).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedResponseDto"}}}},"400":{"description":"INVALID_ORDER (unknown `order` value) or INVALID_CURSOR (cursor shape mismatch).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Feed for an actor: their own posts + posts from actors they follow, with blocks excluded. Default order is `ranked` (recency + engagement + follow-graph signals); pass `order=chronological` to get the time-ordered feed. 50/page (max 200). The cursor shape differs between orders — passing a chronological cursor with `order=ranked` returns 400 INVALID_CURSOR.","tags":["social-feed"]}},"/v1/communities/{communityId}/discover-feed":{"get":{"operationId":"FeedController_getDiscoverFeed","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor UUID."},{"name":"viewer_actor_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque ranked cursor; encodes (score, id)."},{"name":"kind","required":false,"in":"query","schema":{"type":"string"},"description":"Optional post-kind filter (task 023) — e.g. `image`, `link`. Restricts discovery to that kind."}],"responses":{"200":{"description":"Page of posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or viewer actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Discover / explore feed (task 019). Ranked surface over every public post in the community — Instagram Explore / X \"For You without follow constraint\". Optional `actor_id` filters the viewer's blocks and applies a small self-boost; without one, the result is the global discovery slice.","tags":["social-feed"]}},"/v1/communities/{communityId}/lists/{listId}/feed":{"get":{"operationId":"FeedController_getListFeed","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"listId","required":true,"in":"path","description":"List UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer — required for visibility checks + block-filter."},{"name":"viewer_actor_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque cursor; differs in shape between ranked and chronological."},{"name":"order","required":false,"in":"query","schema":{"type":"string","enum":["ranked","chronological"],"default":"chronological"}}],"responses":{"200":{"description":"Page of posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedResponseDto"}}}},"400":{"description":"Missing/invalid actor_id, INVALID_ORDER, or INVALID_CURSOR.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"List not found, or private and viewer is not owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List-scoped feed (task 018). Returns posts authored by members of the list, filtered by what the viewer can see. Default order is `chronological`; pass `order=ranked` for the same engagement-blended scoring used by the actor feed.","tags":["social-feed"]}},"/v1/communities/{communityId}/lists":{"post":{"operationId":"ActorListController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Replays return the original response with `Idempotent-Replay: true`; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateActorListDto"}}}},"responses":{"201":{"description":"List created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorListResponseDto"}}}},"400":{"description":"Validation failure (including malformed Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`, or owner is suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or owner not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"`IDEMPOTENCY_KEY_REUSE` — the supplied Idempotency-Key was already used with a different request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Create a new list. Default visibility `private`. List creation has no natural key (an owner can hold many same-named lists), so pass an `Idempotency-Key` header to make retries safe.","tags":["social-lists"]},"get":{"operationId":"ActorListController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor UUID — surfaces the viewer's own private lists alongside the public catalog."},{"name":"viewer_actor_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"owner_actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter to lists owned by this actor."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of lists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorListListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List discoverable lists in this community. Public lists are visible to anyone; private lists surface only when `actor_id` is their owner.","tags":["social-lists"]}},"/v1/communities/{communityId}/lists/{listId}":{"get":{"operationId":"ActorListController_getById","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"listId","required":true,"in":"path","description":"List UUID.","schema":{"type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor UUID — required to see a private list (must equal owner)."},{"name":"viewer_actor_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorListResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"List not found, or private and viewer is not owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get one list by id.","tags":["social-lists"]},"patch":{"operationId":"ActorListController_update","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"listId","required":true,"in":"path","description":"List UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateActorListDto"}}}},"responses":{"200":{"description":"List updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorListResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not the owner, or PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"List not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update list name / description / visibility. Owner-only.","tags":["social-lists"]},"delete":{"operationId":"ActorListController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"listId","required":true,"in":"path","description":"List UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","description":"Caller social actor UUID (must equal the list owner).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteActorListDto"}}}},"responses":{"204":{"description":"List deleted."},"400":{"description":"Missing/invalid actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"List not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Delete a list. Owner-only. Pass the caller via `?actor_id=`.","tags":["social-lists"]}},"/v1/communities/{communityId}/lists/{listId}/members":{"post":{"operationId":"ActorListController_addMember","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"listId","required":true,"in":"path","description":"List UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddListMemberDto"}}}},"responses":{"201":{"description":"Member added (or returned via idempotent retry).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorListMemberResponseDto"}}}},"403":{"description":"Caller is not the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"List or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A block exists between owner and member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Add a member to a list. Owner-only.","tags":["social-lists"]},"get":{"operationId":"ActorListController_listMembers","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"listId","required":true,"in":"path","description":"List UUID.","schema":{"type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor UUID."},{"name":"viewer_actor_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of members.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorListMemberListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"List not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List members of a list. Public lists are visible to anyone; private lists surface only when `actor_id` is the owner.","tags":["social-lists"]}},"/v1/communities/{communityId}/lists/{listId}/members/{actorId}":{"delete":{"operationId":"ActorListController_removeMember","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"listId","required":true,"in":"path","description":"List UUID.","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Member — social actor UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","description":"Caller social actor UUID (must equal the list owner).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveListMemberDto"}}}},"responses":{"204":{"description":"Member removed."},"400":{"description":"Missing/invalid actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"List not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Remove a member from a list. Owner-only. Idempotent — 204 either way. Pass the caller via `?actor_id=`.","tags":["social-lists"]}},"/v1/communities/{communityId}/flags":{"post":{"operationId":"FlagController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFlagDto"}}}},"responses":{"201":{"description":"Flag created (or de-duplicated to existing open flag). `auto_hidden` reports whether the auto-hide threshold tripped.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFlagResponseDto"}}}},"400":{"description":"Validation failure (bad reporter / target / reason).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.flag` for this community, or reporter actor is suspended/deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, reporter actor, or target object not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The target post/comment is already removed; nothing to flag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot flag your own content.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"File a moderation report against a post or comment. Idempotent per (reporter, target) while open.","tags":["social-flags"]},"get":{"operationId":"FlagController_listMine","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"reporter_actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"The reporter whose flags to list."},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["open","dismissed","actioned"]}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of the reporter's flags.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagListResponseDto"}}}},"400":{"description":"Missing reporter_actor_id or invalid status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List a reporter's own flags (task 017). Requires `reporter_actor_id` — the customer lane sees only that reporter's reports (\"was my report reviewed?\"), never other reporters' flags. Cursor-paginated newest-first.","tags":["social-flags"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/flags":{"get":{"operationId":"ModerationAdminController_listFlags","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of flags with next-cursor and has-more.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagListResponseDto"}}}},"400":{"description":"Invalid status filter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller lacks `social.moderation.queue.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List flags in this community. Filter by ?status=open|dismissed|actioned. 50/page (max 200), cursor-paginated.","tags":["social-moderation-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/flags/{flagId}/actions":{"post":{"operationId":"ModerationAdminController_act","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"flagId","required":true,"in":"path","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Replays return the original response with `Idempotent-Replay: true`; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModerationActionDto"}}}},"responses":{"201":{"description":"Action recorded; target status updated where applicable. Returns the updated flag and the audit row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActOnFlagResponseDto"}}}},"400":{"description":"Invalid action or notes, or malformed Idempotency-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller lacks `social.moderate`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or flag not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"FLAG_ALREADY_CLOSED — the flag is already actioned/dismissed and the requested action is punitive (hide/remove/warn/ban); reversals (unhide/restore/dismiss) stay legal on a closed flag. Also `IDEMPOTENCY_KEY_REUSE` (same Idempotency-Key, different body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Apply a moderation action to a flag. Records an audit entry and updates the target post/comment status. Pass an `Idempotency-Key` header so a retried request records ONE audit entry, not two.","tags":["social-moderation-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/actions":{"post":{"operationId":"ModerationAdminController_actDirect","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string used to deduplicate retries within a 24h window. Replays return the original response; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectModerationActionDto"}}}},"responses":{"201":{"description":"Action recorded (flag_id=null audit row); side-effect applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModerationActionResponseDto"}}}},"400":{"description":"Invalid target_kind / action / notes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller lacks `social.moderate`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or target not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Apply a moderation action directly, without a preceding flag (task 017). Acts on any target — post/comment (status), direct_message (moderator soft-delete), or actor (ban=suspend / restore=reinstate). Writes a flag_id=null audit row.","tags":["social-moderation-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/audit":{"get":{"operationId":"ModerationAdminController_listAudit","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of audit entries with next-cursor and has-more.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditListResponseDto"}}}},"403":{"description":"Caller lacks `social.audit.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Append-only audit of moderator actions in this community. 50/page (max 200), cursor-paginated.","tags":["social-moderation-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/actors/{actorId}/shadow-ban":{"post":{"operationId":"ModerationAdminController_shadowBan","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Replays return the original response with `Idempotent-Replay: true`; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyShadowBanDto"}}}},"responses":{"200":{"description":"Shadow ban applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModerationActorResponseDto"}}}},"400":{"description":"Invalid duration_hours or reason, or malformed Idempotency-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller lacks `social.moderate`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"`IDEMPOTENCY_KEY_REUSE` — the supplied Idempotency-Key was already used with a different request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Apply a shadow ban to an actor. The actor sees their own content normally; everyone else sees nothing from them. Pass `duration_hours` for a timed ban or omit for indefinite. Pass an `Idempotency-Key` header so a retried timed ban doesn't restart the clock.","tags":["social-moderation-admin"]},"delete":{"operationId":"ModerationAdminController_unshadow","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Shadow ban cleared.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModerationActorResponseDto"}}}},"403":{"description":"Caller lacks `social.moderate`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Remove a shadow ban. No-op if not currently shadow-banned.","tags":["social-moderation-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/shadow-banned":{"get":{"operationId":"ModerationAdminController_listShadowBanned","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of shadow-banned actors.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModerationActorListResponseDto"}}}},"403":{"description":"Caller lacks `social.audit.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List currently shadow-banned actors in this community. 50/page (max 200), cursor-paginated newest-ban first.","tags":["social-moderation-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/blocked-terms":{"post":{"operationId":"ModerationAdminController_addBlockedTerm","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBlockedTermDto"}}}},"responses":{"201":{"description":"Blocked term added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockedTermResponseDto"}}}},"400":{"description":"Empty or over-length term.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller lacks `social.config.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Add a hidden word to the community lexicon. A comment whose body contains the term is routed to `action`: `hide` → lands hidden; `pending_approval` → lands in the approve lane. Re-adding a term updates its action. Case-insensitive.","tags":["social-moderation-admin"]},"get":{"operationId":"ModerationAdminController_listBlockedTerms","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of blocked terms.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockedTermListResponseDto"}}}},"403":{"description":"Caller lacks `social.config.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List the community hidden-words lexicon, newest-first. Cursor-paginated (50/page, max 200).","tags":["social-moderation-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/blocked-terms/{term}":{"delete":{"operationId":"ModerationAdminController_removeBlockedTerm","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"term","required":true,"in":"path","description":"The blocked term to remove (case-insensitive).","schema":{"type":"string"}}],"responses":{"204":{"description":"Removed (or no-op)."},"403":{"description":"Caller lacks `social.config.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Remove a hidden word. Idempotent — 204 whether or not it existed.","tags":["social-moderation-admin"]}},"/v1/communities/{communityId}/conversations/unread-count":{"get":{"operationId":"ConversationController_unreadCount","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"The viewer actor UUID."}],"responses":{"200":{"description":"Unread count.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnreadCountResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Global unread message count for the caller across every conversation in this community.","tags":["social-conversations"]}},"/v1/communities/{communityId}/conversations":{"post":{"operationId":"ConversationController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Mostly useful for `kind=\"group\"` (direct conversations are get-or-create by participant pair anyway). Replays return the original response with `Idempotent-Replay: true`; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConversationDto"}}}},"responses":{"201":{"description":"Conversation created or (1-on-1 only) returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponseDto"}}}},"400":{"description":"Validation failure (missing recipient for direct, missing actor_ids for group, two equal actor ids, malformed Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`, or a participant is suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A block exists, the group exceeds `community.settings.max_group_conversation_size`, or `IDEMPOTENCY_KEY_REUSE` (same Idempotency-Key, different body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Open a conversation. Default `kind=\"direct\"` is the naturally-idempotent 1-on-1 path (get-or-create by participant pair). Pass `kind=\"group\"` with `actor_ids` for a multi-participant chat — group creates always insert a new row, so pass an `Idempotency-Key` header to make group-create retries safe.","tags":["social-conversations"]},"get":{"operationId":"ConversationController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"The viewer actor UUID."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["inbox","requests","all"],"default":"inbox"},"description":"Filter slice. `inbox` (default) excludes conversations whose other participant the viewer has restricted; `requests` shows only those; `all` merges both."}],"responses":{"200":{"description":"Page of conversations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the caller's conversations in this community, most-recent-activity first.","tags":["social-conversations"]}},"/v1/communities/{communityId}/conversations/{conversationId}/members":{"post":{"operationId":"ConversationController_addMember","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddMemberDto"}}}},"responses":{"201":{"description":"Member added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not an admin, or PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Already a member, group full, or block exists with an existing participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Add a member to a group conversation. Admin-only. Refuses if the new member has a block with any existing participant.","tags":["social-conversations"]}},"/v1/communities/{communityId}/conversations/{conversationId}/members/{actorId}":{"patch":{"operationId":"ConversationController_updateMember","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Target participant — social actor UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMemberDto"}}}},"responses":{"200":{"description":"Participant updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponseDto"}}}},"403":{"description":"Not an admin (role) or not the target (self-only fields).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation or participant not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Attempting to demote the last admin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update a participant (task 025). Field-level authz: `role` is admin-only; `muted` / `muted_until` / `pinned` are self-only (the caller must be the path actor). The canonical home for participant state.","tags":["social-conversations"]},"delete":{"operationId":"ConversationController_removeMember","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Target participant — social actor UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","description":"Acting actor UUID (equal to the target for self-leave).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveMemberDto"}}}},"responses":{"204":{"description":"Participant removed."},"400":{"description":"Missing/invalid actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not an admin and not the target.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation or participant not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Attempting to remove the last admin (or self-leave when last admin in a non-empty group).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Remove a participant. Admin-only unless the acting `?actor_id=` equals the target (self-leave).","tags":["social-conversations"]}},"/v1/communities/{communityId}/conversations/{conversationId}":{"get":{"operationId":"ConversationController_getById","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"The viewer actor UUID — must be a participant."}],"responses":{"200":{"description":"Conversation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation not found or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Get a single conversation by id.","tags":["social-conversations"]},"patch":{"operationId":"ConversationController_update","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConversationDto"}}}},"responses":{"200":{"description":"Conversation after update (with the caller's unread_count).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Not an admin (rename), or PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation not found or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Rename on a non-group conversation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Conversation-level update (task 025). Pass `name` to rename a group (admin-only) — the canonical rename home. Participant state (muted / muted_until / pinned) lives on PATCH /:id/members/:actorId.","tags":["social-conversations"]},"delete":{"operationId":"ConversationController_leave","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","description":"The leaving participant — social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaveConversationDto"}}}},"responses":{"204":{"description":"Caller removed from the conversation."},"400":{"description":"Missing/invalid actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation not found or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Leave the conversation. Drops the caller's participant row; the conversation is hard-deleted when the last participant leaves. Pass the actor via `?actor_id=`.","tags":["social-conversations"]}},"/v1/communities/{communityId}/conversations/{conversationId}/messages":{"post":{"operationId":"ConversationController_send","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. Replays return the original response with `Idempotent-Replay: true`; same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageDto"}}}},"responses":{"201":{"description":"Message sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageResponseDto"}}}},"400":{"description":"Validation failure (empty body for text, body too long, malformed Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation, reply target, or sender not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A block exists between the sender and another participant, or `IDEMPOTENCY_KEY_REUSE` (same Idempotency-Key, different body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Send a message in the conversation. Pass an `Idempotency-Key` header to make retries safe — a retried send never delivers the message twice.","tags":["social-conversations"]},"get":{"operationId":"ConversationController_listMessages","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"The viewer actor UUID — must be a participant."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}},{"name":"since","required":false,"in":"query","schema":{"type":"string"},"description":"Polling cursor — encoded last-seen message id; returns newer messages in ascending order."}],"responses":{"200":{"description":"Page of messages.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation not found or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List messages in the conversation. Default newest-first cursor. Pass `since` (encoded cursor of the last-seen message) to poll for newer messages instead.","tags":["social-conversations"]}},"/v1/communities/{communityId}/conversations/{conversationId}/messages/{messageId}":{"get":{"operationId":"ConversationController_getMessage","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"messageId","required":true,"in":"path","description":"Message UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"The viewer actor UUID — must be a participant."}],"responses":{"200":{"description":"The message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageResponseDto"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation/message not found, or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Fetch a single message (task 024) — resolves a reply target without paging the whole thread. Participant-gated.","tags":["social-conversations"]},"patch":{"operationId":"ConversationController_editMessage","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"messageId","required":true,"in":"path","description":"Direct message UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMessageDto"}}}},"responses":{"200":{"description":"Message updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageResponseDto"}}}},"400":{"description":"Validation failure (empty body, body too long).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not the sender, or PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation or message not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Edit window expired (5 minutes after send).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Edit a message body. Allowed within 5 minutes of send and only by the sender.","tags":["social-conversations"]},"delete":{"operationId":"ConversationController_deleteMessage","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"messageId","required":true,"in":"path","description":"Direct message UUID.","schema":{"type":"string"}},{"name":"actor_id","required":true,"in":"query","description":"The sender — must match the message's sender_id.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteMessageDto"}}}},"responses":{"204":{"description":"Message soft-deleted."},"400":{"description":"Missing/invalid actor_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Caller is not the sender, or PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation or message not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Soft-delete a message. Reserved to the sender; the row stays so the conversation timeline doesn't orphan replies. Reads return body `\"(deleted)\"`. Pass the sender via `?actor_id=`.","tags":["social-conversations"]}},"/v1/communities/{communityId}/conversations/{conversationId}/read":{"post":{"operationId":"ConversationController_markRead","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkConversationReadDto"}}}},"responses":{"204":{"description":"Read marker updated."},"400":{"description":"Validation failure (read_through not ISO).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation not found or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Mark messages as read up to a given timestamp (default: now()). Sets the caller's `last_read_at`.","tags":["social-conversations"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/conversations":{"get":{"operationId":"ConversationModerationController_listConversations","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of conversations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationAdminListResponseDto"}}}},"403":{"description":"Caller lacks `social.audit.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List every conversation in the community, most-recent-activity first. Moderation-only.","tags":["social-conversations-admin"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/moderation/conversations/{conversationId}/messages":{"get":{"operationId":"ConversationModerationController_listMessages","parameters":[{"name":"workspaceId","required":true,"in":"path","schema":{"type":"string"}},{"name":"communityId","required":true,"in":"path","schema":{"type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of messages.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageAdminListResponseDto"}}}},"403":{"description":"Caller lacks `social.audit.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or conversation not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"List every message in a conversation. Moderation-only.","tags":["social-conversations-admin"]}},"/v1/communities/{communityId}/conversations/{conversationId}/messages/{messageId}/reactions":{"post":{"operationId":"DirectMessageReactionController_add","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"example":"11111111-2222-3333-4444-555555555555","type":"string"}},{"name":"messageId","required":true,"in":"path","description":"Direct message UUID.","schema":{"example":"mmmmmmmm-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDirectMessageReactionDto"}}}},"responses":{"201":{"description":"Reaction applied (created or pre-existing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageReactionCreatedResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`, or reactor is suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation, message, or actor not found, or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block exists between reactor and sender.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Add a reaction to a direct message. Idempotent — re-posting the same (actor, type) is a no-op.","tags":["social-dm-reactions"]},"get":{"operationId":"DirectMessageReactionController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"example":"11111111-2222-3333-4444-555555555555","type":"string"}},{"name":"messageId","required":true,"in":"path","description":"Direct message UUID.","schema":{"example":"mmmmmmmm-0000-0000-0000-000000000001","type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"The viewer — must be a participant."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of reactions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageReactionListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation or message not found, or caller not a participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the reactions on a direct message. Cursor-paginated, newest-first.","tags":["social-dm-reactions"]}},"/v1/communities/{communityId}/conversations/{conversationId}/messages/{messageId}/reactions/{actorId}/{type}":{"delete":{"operationId":"DirectMessageReactionController_remove","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"conversationId","required":true,"in":"path","description":"Conversation UUID.","schema":{"example":"11111111-2222-3333-4444-555555555555","type":"string"}},{"name":"messageId","required":true,"in":"path","description":"Direct message UUID.","schema":{"example":"mmmmmmmm-0000-0000-0000-000000000001","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Reactor actor UUID.","schema":{"type":"string"}},{"name":"type","required":true,"in":"path","description":"Reaction type to remove.","schema":{"type":"string"}}],"responses":{"200":{"description":"Reaction removed (or already absent).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectMessageReactionRemovedResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Conversation, message, or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Remove the (actor, type) reaction. Idempotent — calling on a missing reaction returns the current counts.","tags":["social-dm-reactions"]}},"/v1/communities/{communityId}/actors/{actorId}/notifications":{"get":{"description":"Reverse-chronological inbox. Pass `unread_only=true` to filter to unread rows only. Cursor encodes `(created_at, id)` and round-trips through the response. Default page size 50, max 200. Suppression of self-actions and blocked-actor pairs happens at write time, so every row returned here has already passed those filters.","operationId":"NotificationController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (returned from POST /actors).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Items per page (1..200, default 50)."},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque base64url cursor returned by the prior page."},{"name":"unread_only","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"When `true`, returns only rows where `read_at IS NULL`."},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["inbox","requests","all"],"default":"inbox"},"description":"Filter slice. `inbox` (default) hides suppressed rows; `requests` shows only suppressed rows (notifications from actors the viewer has restricted); `all` merges both."}],"responses":{"200":{"description":"Page of notifications.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationListResponse"}}}},"403":{"description":"PAK lacks `social.notify.read` for this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found (also returned for cross-workspace probes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List notifications for an actor (newest first).","tags":["social-notifications"]}},"/v1/communities/{communityId}/actors/{actorId}/notifications/unread-count":{"get":{"description":"Cheap badge endpoint backed by a partial index — safe to poll at the cadence of an in-product notification bell.","operationId":"NotificationController_unreadCount","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (returned from POST /actors).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"responses":{"200":{"description":"Count of unread rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}},"403":{"description":"PAK lacks `social.notify.read` for this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Count unread notifications for an actor.","tags":["social-notifications"]}},"/v1/communities/{communityId}/actors/{actorId}/notifications/{notificationId}":{"patch":{"description":"Idempotent: marking an already-read row read again is a no-op 204. Returns 404 if the notification belongs to a different actor.","operationId":"NotificationController_markRead","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (returned from POST /actors).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"notificationId","required":true,"in":"path","description":"Notification UUID returned from the list endpoint.","schema":{"example":"11111111-1111-1111-1111-111111111111","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationDto"}}}},"responses":{"204":{"description":"Read state updated."},"400":{"description":"Body validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.notify.update` for this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Notification not found, or belongs to a different actor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Mark a single notification as read or unread.","tags":["social-notifications"]}},"/v1/communities/{communityId}/actors/{actorId}/notifications/read-all":{"post":{"description":"Single bulk write. Optional `before` ISO timestamp scopes to rows older than that — useful for \"mark all older than today as read\" UX. Returns the number of rows that flipped.","operationId":"NotificationController_markAllRead","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (returned from POST /actors).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkAllReadDto"}}}},"responses":{"200":{"description":"Bulk update applied; `updated` is the row count.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkAllReadResponse"}}}},"400":{"description":"`before` is not a valid ISO timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.notify.update` for this community.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Mark every unread notification for the actor as read.","tags":["social-notifications"]}},"/v1/communities/{communityId}/actors/{actorId}/notification-prefs":{"get":{"description":"Returns one row per known NotificationKind so the client UI can render every toggle without a separate catalogue lookup. Synthesised rows carry an epoch `updated_at`; explicit rows carry the real timestamp.","operationId":"NotificationPrefController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (returned from POST /actors).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"responses":{"200":{"description":"Per-kind preferences.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationPrefListResponseDto"}}}},"403":{"description":"PAK lacks `social.notify.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actor's per-kind notification preferences. Synthesises `enabled=true` defaults for kinds the actor has not explicitly toggled.","tags":["social-notification-preferences"]}},"/v1/communities/{communityId}/actors/{actorId}/notification-prefs/{kind}":{"patch":{"description":"Idempotent upsert. Pass `{ enabled: false }` to mute, `{ enabled: true }` to re-enable. Cache is invalidated on write so the next NotificationService.tryEmit picks up the change immediately. Returns the resulting preference row.","operationId":"NotificationPrefController_set","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"kind","required":true,"in":"path","description":"NotificationKind label.","schema":{"enum":["follow","reaction","comment_on_post","reply_to_comment","mention","vote","repost","quote_post","dm_new_message","dm_added_to_group","dm_role_changed","dm_reaction","tag","follow_request","follow_request_approved"],"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationPrefDto"}}}},"responses":{"200":{"description":"Preference upserted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationPrefResponseDto"}}}},"400":{"description":"Body validation failed, or `kind` is not a known NotificationKind.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.notify.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Set a single notification-kind preference for the actor.","tags":["social-notification-preferences"]}},"/v1/communities/{communityId}/actors/{actorId}/suggested-follows":{"get":{"description":"Friend-of-a-friend signal first (people the requester's followees also follow), with a cold-start fallback to top actors in the community by follower count when FoF underfills the limit. Excludes self, existing follows / mutes / blocks, and actors who have blocked the requester. Cached 1h per (actor, limit).","operationId":"SuggestionController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID for whom to compute suggestions.","schema":{"example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Items to return; capped at 50."}],"responses":{"200":{"description":"Ordered candidates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestedFollowsListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Suggested follows for an actor.","tags":["social-suggestions"]}},"/v1/communities/{communityId}/posts/{postId}/bookmark":{"post":{"description":"Visibility-aware: the actor must be allowed to read the post. If they can't, returns 404 (opaque), matching the read-path opacity rule used everywhere else in Social.","operationId":"BookmarkController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBookmarkDto"}}}},"responses":{"201":{"description":"Bookmark created (or returned via idempotent retry).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookmarkToggleResponseDto"}}}},"400":{"description":"Validation failure (missing/invalid actor_id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, post, or actor not found, or the post is invisible to the actor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Bookmark a post on behalf of an actor. Idempotent — returns the existing row with `created: false` on retry.","tags":["social-bookmarks"]},"delete":{"operationId":"BookmarkController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"actor_id","required":true,"in":"query","description":"Acting actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeprecatedActorIdBodyDto"}}}},"responses":{"204":{"description":"Bookmark removed (or was already absent)."},"400":{"description":"Validation failure (missing/invalid actor_id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, post, or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Remove a bookmark on behalf of an actor. Idempotent — 204 either way. Pass the actor via `?actor_id=`.","tags":["social-bookmarks"]}},"/v1/communities/{communityId}/actors/{actorId}/bookmarks":{"get":{"description":"Bookmarks are private — only the bookmarker should see their own list. The PAK lane has no end-user principal; the customer's backend must scope this call to the right end-user before exposing the response. 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.","operationId":"BookmarkController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque cursor from `pagination.next_cursor`."}],"responses":{"200":{"description":"Page of bookmarks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookmarkListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actor's bookmarks. Cursor-paginated newest-first.","tags":["social-bookmarks"]}},"/v1/communities/{communityId}/collections":{"post":{"operationId":"CollectionController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client string; replays return the original response.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCollectionDto"}}}},"responses":{"201":{"description":"Collection created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`, owner suspended, or cover asset not owned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, owner, or cover asset not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cover asset is not ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Create a collection. Default kind `bookmark`, visibility `private`. Pass an Idempotency-Key so retries are safe.","tags":["social-collections"]},"get":{"operationId":"CollectionController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer actor — surfaces their own private collections."},{"name":"owner_actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"}},{"name":"kind","required":false,"in":"query","schema":{"type":"string","enum":["bookmark","highlight"]}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of collections.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionListResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List collections. Public ones are visible to anyone; private ones surface only to their owner via actor_id. Filter by owner_actor_id / kind.","tags":["social-collections"]}},"/v1/communities/{communityId}/collections/{collectionId}":{"get":{"operationId":"CollectionController_getById","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"collectionId","required":true,"in":"path","schema":{"type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The collection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionResponseDto"}}}},"404":{"description":"Not found (or private and not the owner).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Fetch one collection. Private collections resolve only for their owner (via actor_id).","tags":["social-collections"]},"patch":{"operationId":"CollectionController_update","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"collectionId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCollectionDto"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionResponseDto"}}}},"403":{"description":"Caller is not the owner, or PAK lacks `social.update`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cover asset is not ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update a collection (owner only) — rename, cover, visibility, reorder position.","tags":["social-collections"]},"delete":{"operationId":"CollectionController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"collectionId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteWithCallerDto"}}}},"responses":{"204":{"description":"Deleted."},"403":{"description":"Caller is not the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Delete a collection (owner only). Items cascade; the posts themselves are untouched.","tags":["social-collections"]}},"/v1/communities/{communityId}/collections/{collectionId}/items":{"post":{"operationId":"CollectionController_addItem","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"collectionId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddItemDto"}}}},"responses":{"201":{"description":"Item added (or already present).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionItemResponseDto"}}}},"403":{"description":"Caller is not the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Collection or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Add a post to a collection (owner only). Idempotent per (collection, post).","tags":["social-collections"]},"get":{"operationId":"CollectionController_listItems","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"collectionId","required":true,"in":"path","schema":{"type":"string"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionItemListResponseDto"}}}},"404":{"description":"Not found (or private and not the owner).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List a collection's posts in curator order. Highlight collections bypass story expiry; bookmark collections drop expired posts. 50/page (max 200).","tags":["social-collections"]}},"/v1/communities/{communityId}/collections/{collectionId}/items/{postId}":{"patch":{"operationId":"CollectionController_reorderItem","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"collectionId","required":true,"in":"path","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderItemDto"}}}},"responses":{"200":{"description":"Item repositioned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionItemResponseDto"}}}},"403":{"description":"Caller is not the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Collection or item not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Reorder a post within a collection (owner only).","tags":["social-collections"]},"delete":{"operationId":"CollectionController_removeItem","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"collectionId","required":true,"in":"path","schema":{"type":"string"}},{"name":"postId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteWithCallerDto"}}}},"responses":{"204":{"description":"Item removed."},"403":{"description":"Caller is not the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Remove a post from a collection (owner only).","tags":["social-collections"]}},"/v1/communities/{communityId}/actors/{actorId}/mentions":{"get":{"description":"Cursor-paginated, newest-first. Interleaves post + comment mentions in a single feed; the `source` field discriminates which surface each row came from. The `post_id` field is always set so the client can deep-link, even for comment mentions.","operationId":"MentionController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID whose mentions to list.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque cursor from `pagination.next_cursor`."}],"responses":{"200":{"description":"Page of mention entries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MentionListResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List every place this actor has been @-mentioned (posts + comments).","tags":["social-mentions"]}},"/v1/communities/{communityId}/search/actors":{"get":{"description":"Postgres FTS over a generated tsvector column (display_name A + external_id B + bio C, `simple` locale). Returns up to `limit` (default 20, max 50) hits ordered by `ts_rank`. Pass `actor_id` to filter out actors the requester has muted or blocked, and actors who have blocked the requester. Trigram fuzzy matching is intentionally out of scope — that's a follow-up.","operationId":"SearchController_searchActors","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"q","required":true,"in":"query","schema":{"type":"string","minLength":1,"maxLength":120},"description":"Free-text query (1..120 chars)."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Max hits to return."},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer social actor UUID — filters out blocked / muted actors."},{"name":"requester_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor from a prior response. Valid only for the same `q` (task 022)."}],"responses":{"200":{"description":"Ranked actor hits, cursor-paginated by (rank, id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorSearchResponseDto"}}}},"400":{"description":"q missing or out of bounds (1..120 chars).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found (or belongs to a different community).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Search actors by display name, external_id (username), and bio.","tags":["social-search"]}},"/v1/communities/{communityId}/search/posts":{"get":{"description":"Postgres FTS over a generated tsvector column on the post table (title A + body B, simple locale). `order='relevance'` (default) ranks by ts_rank; `order='recent'` keeps the FTS filter but orders by created_at desc. Filters out drafts / removed / hidden / expired posts. Pass `actor_id` to drop posts authored by anyone the requester has muted / blocked, or who has blocked the requester. Without `actor_id`, only posts with `visibility='public'` surface.","operationId":"SearchController_searchPosts","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"q","required":true,"in":"query","schema":{"type":"string","minLength":1,"maxLength":120}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}},{"name":"order","required":false,"in":"query","schema":{"type":"string","enum":["relevance","recent"],"default":"relevance"}},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Viewer social actor UUID."},{"name":"requester_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor from a prior response. Valid only for the same `(q, order)` pair (task 022)."}],"responses":{"200":{"description":"Ranked post hits, cursor-paginated by (rank, id) for relevance or (created_at, id) for recent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostSearchResponseDto"}}}},"400":{"description":"q missing / overlong, or invalid `order` value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Search post content (title + body) by full-text.","tags":["social-search"]}},"/v1/communities/{communityId}/actors/{actorId}/story-tray":{"get":{"description":"Includes the requester themselves and every actor they follow. Ordered by latest story timestamp. Capped at 200 entries (no cursor in v1; tray is bounded by follow-graph in normal usage). Returns `has_unviewed=true` when the requester has not recorded a `view` edge against the author's most recent story.","operationId":"StoryController_getTray","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID (returned from POST /actors).","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"responses":{"200":{"description":"Story tray entries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryTrayResponse"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Story tray for an actor — one entry per author with at least one unexpired or pinned story.","tags":["social-stories"]}},"/v1/communities/{communityId}/actors/{actorId}/highlights":{"get":{"description":"Pinned posts appear here regardless of their `expires_at`. Visibility still applies — pass `actor_id` to surface non-public pinned posts the requester is allowed to see. Without that param, only `public` pinned rows are returned (legacy-safe default). Cursor-paginated on `(created_at DESC, id DESC)`.","operationId":"StoryController_getHighlights","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Social actor UUID whose highlights to list.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Items per page."},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Cursor from the prior page."},{"name":"actor_id","required":false,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Requester social actor UUID. Without it, only public pinned posts surface."},{"name":"requester_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of highlights.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HighlightsResponse"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the actor's pinned posts (highlights).","tags":["social-stories"]}},"/v1/communities/{communityId}/posts/{postId}/views":{"post":{"description":"Idempotent on `(community, viewer, post)`. 204 on success or no-op duplicate. 422 on self-view (you cannot view your own story). 404 if the post is hidden by visibility, expired, removed, or if a block exists between viewer and author. Does NOT fire a notification — the author sees views via the viewers endpoint instead.","operationId":"StoryController_recordView","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Story (post) UUID.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordViewDto"}}}},"responses":{"204":{"description":"View recorded (or already existed)."},"400":{"description":"Body validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.view`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / post / viewer not found, or the post is invisible to the viewer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot view your own story.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Record a view of a post (idempotent).","tags":["social-stories"]}},"/v1/communities/{communityId}/posts/{postId}/viewers":{"get":{"description":"Only the post author can list viewers. The PAK passes `actor_id`; if that resolves to an actor whose id != post.actor_id, returns 403. Viewers are ordered by view timestamp, newest-first.","operationId":"StoryController_listViewers","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Story (post) UUID.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Requester social actor UUID — must resolve to the post author."},{"name":"requester_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Cursor from the prior page."}],"responses":{"200":{"description":"Page of viewers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryViewersResponse"}}}},"400":{"description":"Missing/invalid actor_id (author-only endpoint).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list` OR requester is not the post author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List viewers of a post (author-only).","tags":["social-stories"]}},"/v1/communities/{communityId}/close-friends":{"post":{"description":"Idempotent on `(community, owner, member)`. 422 on self-add. 409 if a block exists between the pair in either direction. Visibility-`close_friends` posts authored by `owner` become visible to `member` after this call.","operationId":"StoryController_addCloseFriend","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCloseFriendDto"}}}},"responses":{"204":{"description":"Member added (or already present)."},"400":{"description":"Body validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.close-friend.manage`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / owner / member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block in place between owner and member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Cannot add yourself as a close friend.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Add a member to an actor's close-friends list.","tags":["social-stories"]}},"/v1/communities/{communityId}/close-friends/{ownerActorId}/{memberActorId}":{"delete":{"description":"Idempotent: 204 whether or not the edge exists. After removal, `close_friends`-visibility posts by `owner` no longer surface to `member`.","operationId":"StoryController_removeCloseFriend","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"ownerActorId","required":true,"in":"path","description":"Owner social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"memberActorId","required":true,"in":"path","description":"Member social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000002","type":"string"}}],"responses":{"204":{"description":"Removed (or was not in the list)."},"403":{"description":"PAK lacks `social.close-friend.manage`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Remove a member from an actor's close-friends list.","tags":["social-stories"]}},"/v1/communities/{communityId}/actors/{actorId}/close-friends":{"get":{"description":"Returns the actors on `externalId`'s close-friends list, with denormalised actor metadata. Cursor paginated.","operationId":"StoryController_listCloseFriends","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Owner social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of close friends.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloseFriendsListResponse"}}}},"400":{"description":"Invalid cursor (`INVALID_CURSOR`) — legacy raw-id cursors are rejected; restart from the first page and follow `pagination.next_cursor`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.close-friend.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the close-friends members of an actor.","tags":["social-stories"]}},"/v1/communities/{communityId}/posts/{postId}/votes":{"post":{"description":"Last-vote-wins: an actor changing their vote replaces the prior one. 422 if the post has no poll, if `option_index` is out of range, or on self-vote of own poll. 409 if a block exists between voter and post author. Fires a `vote` notification to the post author.","operationId":"StoryController_castVote","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Post UUID containing the poll.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoteDto"}}}},"responses":{"204":{"description":"Vote recorded or replaced."},"400":{"description":"Body validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.vote`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community / post / voter not found, or post invisible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block in place between voter and poll author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Self-vote / no poll / option_index out of range / `POLL_CLOSED` (past the poll `closes_at`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Cast or change a vote on a poll.","tags":["social-stories"]}},"/v1/communities/{communityId}/posts/{postId}/poll-results":{"get":{"description":"Returns counts per option, zero-filling options that received no votes, plus `total_votes`. The `question` and option `label` strings are echoed from `post.attributes.poll`.","operationId":"StoryController_getPollResults","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Post UUID containing the poll.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}}],"responses":{"200":{"description":"Aggregated poll results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollResultsResponse"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"422":{"description":"Post does not contain a poll.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Aggregate poll results for a post.","tags":["social-stories"]}},"/v1/communities/{communityId}/posts/{postId}/votes/{actorId}":{"get":{"description":"Returns the actor's chosen `option_index` and when it was cast. 404 when the post is gone or the actor has not voted. Naturally idempotent — no Idempotency-Key.","operationId":"StoryController_getVote","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Post UUID containing the poll.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Voter social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000002","type":"string"}}],"responses":{"200":{"description":"The actor's vote.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollVoteResponse"}}}},"403":{"description":"PAK lacks `social.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post not found, or the actor has not voted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Read an actor's vote on a poll.","tags":["social-stories"]},"delete":{"description":"Deletes the vote edge. Idempotent — 204 whether or not a vote existed. No tally decrement: results are computed from surviving votes.","operationId":"StoryController_deleteVote","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Post UUID containing the poll.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Voter social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000002","type":"string"}}],"responses":{"204":{"description":"Vote retracted (or no-op)."},"403":{"description":"PAK lacks `social.vote`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Retract an actor's vote on a poll.","tags":["social-stories"]}},"/v1/communities/{communityId}/posts/{postId}/voters":{"get":{"description":"Only the poll author can list voters. Pass the author via `actor_id`; a requester whose id != post.actor_id gets 403. Each entry carries the voter and their `option_index`. Optionally filter to a single option with `option`. Ordered newest-first.","operationId":"StoryController_listVoters","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Post UUID containing the poll.","schema":{"example":"bbbbbbbb-0000-0000-0000-000000000001","type":"string"}},{"name":"actor_id","required":true,"in":"query","schema":{"type":"string","format":"uuid"},"description":"Requester social actor UUID — must resolve to the poll author."},{"name":"option","required":false,"in":"query","schema":{"type":"integer","minimum":0},"description":"Filter to voters who chose this option index."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Cursor from the prior page."}],"responses":{"200":{"description":"Page of voters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PollVotersResponse"}}}},"400":{"description":"Missing/invalid actor_id (author-only endpoint).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list` OR requester is not the poll author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List voters of a poll (author-only).","tags":["social-stories"]}},"/v1/communities/{communityId}/posts/{postId}/reactions":{"post":{"operationId":"PostReactionController_react","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReactionDto"}}}},"responses":{"201":{"description":"Reaction added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReactionResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community, post, or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Block exists between actor and post author.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"React to a post. Idempotent: re-posting the same (actor, type) returns the existing edge with `created: false`.","tags":["social-post-reactions"]},"get":{"operationId":"PostReactionController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","type":"string"}},{"name":"type","required":false,"in":"query","schema":{"type":"string","maxLength":64},"description":"Filter to a single reaction type label."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of reactions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostReactionListResponseDto"}}}},"400":{"description":"Overlong `type` filter (max 64 chars).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or post not found (or post removed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List reactions on a post, newest-first. Cursor encodes `(created_at, actor_id, type)` so multi-reaction-from-same-actor stays stable. Pass `type` to narrow to one reaction type.","tags":["social-post-reactions"]}},"/v1/communities/{communityId}/posts/{postId}/reactions/{srcActorId}/{type}":{"delete":{"operationId":"PostReactionController_unreact","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"postId","required":true,"in":"path","description":"Social post UUID.","schema":{"type":"string"}},{"name":"srcActorId","required":true,"in":"path","description":"Reactor actor UUID.","schema":{"type":"string"}},{"name":"type","required":true,"in":"path","description":"Reaction type label.","schema":{"type":"string"}}],"responses":{"200":{"description":"Reaction removed (or already absent).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReactionCountsResponseDto"}}}},"400":{"description":"Missing path parameter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.delete`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Unreact. Idempotent: returns 200 with current `reaction_counts` even if there was nothing to remove.","tags":["social-post-reactions"]}},"/v1/communities/{communityId}/actors/{actorId}/post-state":{"post":{"description":"Read-over-POST (the id list is too large for a query string) — no side effects, no Idempotency-Key needed. Renders a whole feed page's \"liked by you\" / saved / reposted / voted state with one request on top of the feed call.","operationId":"ViewerStateController_postState","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID. Must belong to the PAK's workspace.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"Viewer actor UUID — the perspective all state is computed from.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostStateRequestDto"}}}},"responses":{"200":{"description":"One state entry per requested post id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostStateResponseDto"}}}},"400":{"description":"Empty `post_ids`, more than 200 ids, or a non-UUID in the list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or actor not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Batch viewer-state hydration: the actor's reactions, bookmark, repost, and poll-vote state for up to 200 posts in one call.","tags":["social-viewer-state"]}},"/v1/communities/{communityId}/hashtags/{tag}/posts":{"get":{"operationId":"HashtagFeedController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Community UUID.","schema":{"type":"string"}},{"name":"tag","required":true,"in":"path","description":"Lowercase tag (without `#`).","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","schema":{"type":"string","enum":["chronological","ranked"],"default":"chronological"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"},"description":"Chronological pages on (created_at, id); ranked on (score, id). A cursor is not portable across orders."}],"responses":{"200":{"description":"Page of posts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HashtagPostListResponseDto"}}}},"400":{"description":"Invalid tag or order value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.list`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community or tag not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List posts using a hashtag. `order=chronological` (default) or `order=ranked` (task 023) — ranked reuses the discover recency+engagement scoring, paginated on a (score, id) cursor. Same wire shape as the regular post list.","tags":["social-hashtag-posts"]}},"/v1/communities/{communityId}/webhooks":{"post":{"operationId":"WebhookController_create","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. The signing secret is returned exactly once; a replay returns the original response (including the secret) with `Idempotent-Replay: true`. Same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookDto"}}}},"responses":{"201":{"description":"Webhook created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedWebhookResponseDto"}}}},"400":{"description":"Validation failure (bad url, empty or unknown event_types — the message names the valid set; malformed Idempotency-Key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"PAK lacks `social.create`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Webhook cap reached (10 per community), or `IDEMPOTENCY_KEY_REUSE` (same Idempotency-Key, different body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Create a community webhook. Multiple subscribers per community are supported (cap 10). Returns the signing secret ONCE — store it. Pass an `Idempotency-Key` header so a retried create replays the SAME secret instead of minting a duplicate subscriber.","tags":["social-webhooks"]},"get":{"operationId":"WebhookController_list","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}}],"responses":{"200":{"description":"All webhooks on the community (no pagination — capped at 10 rows).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponseDto"}}}},"404":{"description":"Community not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List the community's webhooks — disabled rows included, so an auto-disabled subscriber stays discoverable.","tags":["social-webhooks"]}},"/v1/communities/{communityId}/webhooks/{webhookId}":{"get":{"operationId":"WebhookController_getById","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"webhookId","required":true,"in":"path","description":"Webhook UUID.","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponseDto"}}}},"404":{"description":"Webhook not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Read a single webhook by id.","tags":["social-webhooks"]},"patch":{"operationId":"WebhookController_update","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"webhookId","required":true,"in":"path","description":"Webhook UUID.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookDto"}}}},"responses":{"200":{"description":"Webhook updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponseDto"}}}},"400":{"description":"Validation failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Webhook not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update webhook url, subscribed event types, or disabled state. `{ \"disabled\": false }` is the re-enable path after an auto-disable — no secret rotation required.","tags":["social-webhooks"]},"delete":{"operationId":"WebhookController_delete","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"webhookId","required":true,"in":"path","description":"Webhook UUID.","schema":{"type":"string"}}],"responses":{"204":{"description":"Webhook deleted."},"404":{"description":"Webhook not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Delete the webhook.","tags":["social-webhooks"]}},"/v1/communities/{communityId}/webhooks/{webhookId}/test":{"post":{"operationId":"WebhookController_testPing","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"webhookId","required":true,"in":"path","description":"Webhook UUID.","schema":{"type":"string"}}],"responses":{"202":{"description":"Ping enqueued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryJobResponseDto"}}}},"404":{"description":"Webhook not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Webhook is disabled — re-enable it first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Enqueue a `ping` test delivery. Async (202): the dispatch worker signs and POSTs it exactly like a real event — SSRF guard, TLS, HMAC signature and all. Watch the deliveries list for the outcome.","tags":["social-webhooks"]}},"/v1/communities/{communityId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver":{"post":{"operationId":"WebhookController_redeliver","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"webhookId","required":true,"in":"path","description":"Webhook UUID.","schema":{"type":"string"}},{"name":"deliveryId","required":true,"in":"path","description":"Delivery row UUID (from GET …/deliveries).","schema":{"type":"string"}}],"responses":{"202":{"description":"Redelivery enqueued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryJobResponseDto"}}}},"404":{"description":"Webhook or delivery not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Webhook is disabled — re-enable it first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Re-publish a past delivery from its stored request body. Async (202). The payload is byte-identical to the original (same event id), so subscriber-side event-id dedupe treats it as a redelivery.","tags":["social-webhooks"]}},"/v1/communities/{communityId}/webhooks/{webhookId}/rotate-secret":{"post":{"operationId":"WebhookController_rotateSecret","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"webhookId","required":true,"in":"path","description":"Webhook UUID.","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Opaque client-generated string (1–256 chars, `[A-Za-z0-9_\\-:]`) used to deduplicate retries within a 24h window. A replay returns the original response (including the new secret) with `Idempotent-Replay: true`; a retry WITHOUT the header rotates the secret again, invalidating the previous one. Same key + different body returns 409 `IDEMPOTENCY_KEY_REUSE`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"New secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedWebhookResponseDto"}}}},"400":{"description":"Malformed Idempotency-Key header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Webhook not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"`IDEMPOTENCY_KEY_REUSE` — the supplied Idempotency-Key was already used with a different request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Rotate the signing secret. Also clears any auto-disable flag. Returns the new secret ONCE. Pass an `Idempotency-Key` header so a retried rotate replays the SAME new secret instead of rotating twice.","tags":["social-webhooks"]}},"/v1/communities/{communityId}/webhooks/{webhookId}/deliveries":{"get":{"operationId":"WebhookController_deliveries","parameters":[{"name":"communityId","required":true,"in":"path","description":"Social community UUID.","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"webhookId","required":true,"in":"path","description":"Webhook UUID.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of deliveries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryListResponseDto"}}}},"404":{"description":"Webhook not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List delivery attempts (newest first).","tags":["social-webhooks"]}},"/v1/webhook-event-types":{"get":{"operationId":"WebhookEventTypesController_list","parameters":[],"responses":{"200":{"description":"The full event-type catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEventTypesResponseDto"}}}}},"summary":"List the webhook event-type catalog. Static metadata, no auth required. Create/update webhooks reject anything not in this list with a 400.","tags":["social-webhooks"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/actors/{actorId}/comments":{"get":{"operationId":"ExportController_comments","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"Workspace UUID.","schema":{"example":"wwwwwwww-wwww-wwww-wwww-wwwwwwwwwwww","type":"string"}},{"name":"communityId","required":true,"in":"path","description":"Community UUID (must belong to the workspace).","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"The data subject — social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of the actor's comments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorCommentsResponseDto"}}}},"403":{"description":"Caller lacks `social.audit.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Every comment an actor authored, all statuses (published / hidden / removed) — the GDPR-export view. 50/page (max 200), cursor-paginated newest-first.","tags":["social-export"]}},"/v1/workspaces/{workspaceId}/communities/{communityId}/actors/{actorId}/reactions":{"get":{"operationId":"ExportController_reactions","parameters":[{"name":"workspaceId","required":true,"in":"path","description":"Workspace UUID.","schema":{"example":"wwwwwwww-wwww-wwww-wwww-wwwwwwwwwwww","type":"string"}},{"name":"communityId","required":true,"in":"path","description":"Community UUID (must belong to the workspace).","schema":{"example":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":"string"}},{"name":"actorId","required":true,"in":"path","description":"The data subject — social actor UUID.","schema":{"example":"aaaaaaaa-0000-0000-0000-000000000001","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Page of the actor's reactions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActorReactionsResponseDto"}}}},"403":{"description":"Caller lacks `social.audit.read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Community not found in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Every reaction an actor has given — post reactions and comment reactions unified into one stream, discriminated by `target_type`. The GDPR-export view. 50/page (max 200), cursor-paginated newest-first.","tags":["social-export"]}}},"info":{"title":"Social API","description":"Social-as-a-Service backend: actors, posts, comments, edges (follow/mute/block/restrict), feeds, stories, DMs, moderation. Two lanes. Customer lane: your backend calls /v1/communities/:communityId/* with a workspace PAK (`Authorization: Bearer pcft_live_...`) and asserts the acting end-user via the standardized `actor_id` (body field or `?actor_id=` query parameter). Admin lane: /v1/workspaces/:workspaceId/* accepts a PAK bearer, a PlatformUser bearer, or the `auth_token` session cookie, and covers community CRUD, moderation, and analytics.","version":"0.1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"CommunityRankingWeightsDto":{"type":"object","properties":{"recency":{"type":"number","description":"Recency signal weight (exponential decay). Non-negative finite number; anything else falls back to the default.","example":1,"default":1},"engagement":{"type":"number","description":"Engagement signal weight (log-scaled comments + reactions). Non-negative finite number.","example":0.6,"default":0.6},"follow_bias":{"type":"number","description":"Follow-graph bias weight. Non-negative finite number.","example":0.4,"default":0.4},"self_boost":{"type":"number","description":"Self-author boost weight (keeps the viewer's own posts visible). Non-negative finite number.","example":0.2,"default":0.2}}},"CommunityRankingSettingsDto":{"type":"object","properties":{"weights":{"description":"Per-signal weights for the ranked feed. Each key falls back to its default independently.","allOf":[{"$ref":"#/components/schemas/CommunityRankingWeightsDto"}]},"half_life_hours":{"type":"number","description":"Recency half-life in hours for the ranked feed. Strictly positive finite number.","example":24,"default":24},"candidate_window_days":{"type":"number","description":"Ranked-feed candidate window in days — posts older than this are not scored. Strictly positive finite number.","example":30,"default":30}}},"CommunitySettingsDto":{"type":"object","properties":{"ranking":{"description":"Ranked-feed tuning (weights, half-life, candidate window). Read at query time by the actor feed, list feeds, and the discover feed.","allOf":[{"$ref":"#/components/schemas/CommunityRankingSettingsDto"}]},"max_comment_depth":{"type":"number","description":"Maximum comment thread depth (0 = flat). Integer 0..10; floored; out-of-bounds falls back to the default. Replying past the cap returns 409.","example":3,"default":3,"minimum":0,"maximum":10},"auto_hide_flag_threshold":{"type":"number","description":"Distinct-reporter count at which a flagged post/comment is auto-hidden. Integer 0..1000; floored. Read by the moderation flag pipeline.","example":3,"default":3,"minimum":0,"maximum":1000},"edit_window_minutes":{"type":"number","description":"Minutes after publication during which a post body/title may still be edited (`EDIT_WINDOW_EXPIRED` afterwards). Strictly positive finite number. Drafts edit freely.","example":30,"default":30},"max_group_conversation_size":{"type":"number","description":"Maximum participants in a group conversation. Integer 2..200; floored. Exceeding it returns 409 on create / member add.","example":50,"default":50,"minimum":2,"maximum":200},"max_pinned_comments":{"type":"number","description":"Maximum pinned top-level comments per post (task 012). Integer 0..100; floored. Exceeding it returns 409 `PIN_CAP_REACHED`.","example":3,"default":3,"minimum":0,"maximum":100}}},"CommunityResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"workspace_id":{"type":"string","description":"Owning workspace UUID.","example":"11111111-1111-1111-1111-111111111111"},"app_id":{"type":"string","description":"Auth app UUID this community wraps; `null` for standalone communities.","example":"33333333-3333-3333-3333-333333333333","nullable":true},"slug":{"type":"string","description":"URL-friendly community slug.","example":"my-product-community"},"display_name":{"type":"string","description":"Display name.","example":"My Product Community"},"description":{"type":"string","description":"Optional human-readable description.","example":null,"nullable":true},"settings":{"description":"Per-community settings blob. See CommunitySettingsDto for every key the service reads; unrecognized keys are stored and returned unchanged but not consumed.","example":{"max_comment_depth":3},"allOf":[{"$ref":"#/components/schemas/CommunitySettingsDto"}]},"status":{"type":"string","description":"Lifecycle status.","enum":["active","suspended","archived"],"example":"active"},"created_by":{"type":"string","description":"Account UUID of the creator.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"created_at":{"type":"string","description":"ISO timestamp the row was inserted.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp of the last update.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","workspace_id","app_id","slug","display_name","description","settings","status","created_by","created_at","updated_at"]},"PaginationResponseDto":{"type":"object","properties":{"next_cursor":{"type":"string","description":"Opaque base64url cursor for the next page; `null` when no further pages.","example":"eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMVQxMjowMDowMC4wMDBaIiwiaWQiOiJiYmJiIn0","nullable":true},"has_more":{"type":"boolean","description":"`true` when at least one more page exists. Mirrors `next_cursor !== null`; published as a separate field so SDKs can drive a `while` loop without parsing the cursor. A short or empty `data` array does NOT imply the end of the list when a lane post-filters — always page until this is `false`.","example":true}},"required":["next_cursor","has_more"]},"CommunityListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CommunityResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"ErrorResponseDto":{"type":"object","properties":{"statusCode":{"type":"number","description":"HTTP status code echoed in the body.","example":404},"message":{"type":"object","description":"Human-readable error message. May be a class-validator array on 400.","example":"Post not found"},"code":{"type":"string","description":"Stable error code for programmatic handling. Optional — newer routes set it where the message alone is ambiguous.","example":"NOT_FOUND"}},"required":["statusCode","message"]},"CreateCommunityDto":{"type":"object","properties":{"app_id":{"type":"string","description":"Optional Auth App UUID this community wraps. When supplied, the app must be owned by the calling workspace (cross-tenant gate). Omit to create a standalone community with no Auth integration — actor identity is then managed entirely by the customer.","example":"33333333-3333-3333-3333-333333333333"},"slug":{"type":"string","description":"URL-friendly community slug. Lowercase letters, digits, and hyphens (3-64 chars).","example":"my-product-community","minLength":3,"maxLength":64},"display_name":{"type":"string","description":"Display name shown in the console and on responses.","example":"My Product Community","minLength":1,"maxLength":120},"description":{"type":"string","description":"Optional human-readable description (up to 500 chars).","example":"Where our customers post stories and threads.","maxLength":500},"settings":{"description":"Per-community settings blob. Supported keys are typed on CommunitySettingsDto; unrecognized keys are stored verbatim but not consumed.","allOf":[{"$ref":"#/components/schemas/CommunitySettingsDto"}]}},"required":["slug","display_name"]},"UpdateCommunityDto":{"type":"object","properties":{"display_name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","nullable":true,"maxLength":500},"settings":{"description":"Replaces the settings blob. Supported keys are typed on CommunitySettingsDto; unrecognized keys are stored verbatim but not consumed.","allOf":[{"$ref":"#/components/schemas/CommunitySettingsDto"}]},"status":{"type":"string","enum":["active","suspended","archived"]}}},"UpsertActorDto":{"type":"object","properties":{"external_id":{"type":"string","description":"Customer-side identifier (your user id). Idempotency key for upsert; unique per community.","example":"user_abc123","minLength":1,"maxLength":256},"display_name":{"type":"string","description":"Display name shown in feeds / mentions.","example":"Ada Lovelace","nullable":true,"maxLength":120},"avatar_url":{"type":"string","description":"Avatar URL.","example":"https://cdn.example.com/avatars/ada.png","nullable":true,"maxLength":2048},"metadata":{"type":"object","description":"Free-form metadata blob (e.g. plan tier, internal flags).","example":{"tier":"pro"}}},"required":["external_id"]},"BatchUpsertActorsDto":{"type":"object","properties":{"actors":{"description":"Actors to upsert, in order. 1–200 items; each item follows the single-upsert body exactly.","type":"array","items":{"$ref":"#/components/schemas/UpsertActorDto"}}},"required":["actors"]},"ActorResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Social actor UUID.","example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"external_id":{"type":"string","description":"Customer-supplied stable identifier (your user id).","example":"user_abc123"},"display_name":{"type":"string","description":"Display name.","example":"Ada Lovelace","nullable":true},"avatar_url":{"type":"string","description":"Avatar URL.","example":"https://cdn.example.com/avatars/ada.png","nullable":true},"metadata":{"type":"object","description":"Free-form metadata blob.","example":{"tier":"pro"}},"status":{"type":"string","description":"Lifecycle status. `suspended` is moderator-applied via PATCH `status=suspended`; `deleted` is soft-deletion. Shadow-banning is a separate moderation mechanism that does not change `status`.","enum":["active","suspended","deleted"],"example":"active"},"is_private":{"type":"boolean","description":"When true, the actor is private (task 014): effective post visibility is capped at `followers` and following requires an approved follow request.","example":false},"created_at":{"type":"string","description":"ISO timestamp the row was inserted.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp of the last update.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","external_id","display_name","avatar_url","metadata","status","is_private","created_at","updated_at"]},"BatchActorResultDto":{"type":"object","properties":{"index":{"type":"number","description":"Position of this result in the request array.","example":0},"success":{"type":"boolean","description":"`true` when this item was upserted; `false` when it failed (see `error`).","example":true},"actor":{"description":"The upserted actor — present on success.","allOf":[{"$ref":"#/components/schemas/ActorResponseDto"}]},"created":{"type":"boolean","description":"`true` if this item inserted a new row, `false` on an update of an existing external_id. Present on success.","example":true},"error":{"description":"The error this item would have returned as a standalone call — present on failure.","allOf":[{"$ref":"#/components/schemas/ErrorResponseDto"}]}},"required":["index","success"]},"BatchActorsResponseDto":{"type":"object","properties":{"data":{"description":"One result per input item, same order.","type":"array","items":{"$ref":"#/components/schemas/BatchActorResultDto"}},"succeeded":{"type":"number","description":"Count of successful items.","example":199},"failed":{"type":"number","description":"Count of failed items.","example":1}},"required":["data","succeeded","failed"]},"BatchExternalIdsDto":{"type":"object","properties":{"external_ids":{"description":"external_ids to resolve, 1–200 items (POST body — 256-char ids can't ride a querystring). Duplicates are de-duplicated.","example":["user_abc123","user_def456"],"type":"array","items":{"type":"string"}}},"required":["external_ids"]},"ActorsByExternalBatchResponseDto":{"type":"object","properties":{"data":{"description":"Actors found, in request order (after de-duplication).","type":"array","items":{"$ref":"#/components/schemas/ActorResponseDto"}},"missing":{"description":"external_ids with no actor in this community.","example":["user_ghost"],"type":"array","items":{"type":"string"}}},"required":["data","missing"]},"UpsertActorResponseDto":{"type":"object","properties":{"actor":{"$ref":"#/components/schemas/ActorResponseDto"},"created":{"type":"boolean","description":"`true` on first call (row was inserted), `false` on subsequent upserts that updated an existing row.","example":true}},"required":["actor","created"]},"ActorPageResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ActorResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UpdateActorDto":{"type":"object","properties":{"display_name":{"type":"string","nullable":true,"maxLength":120},"avatar_url":{"type":"string","nullable":true,"maxLength":2048},"metadata":{"type":"object"},"status":{"type":"string","enum":["active","suspended","deleted"]},"is_private":{"type":"boolean","description":"Make the actor private (task 014). Caps effective post visibility at `followers`; following requires an approved request. Flipping back to public auto-approves pending requests."}}},"PostSettingsInputDto":{"type":"object","properties":{"comments":{"type":"string","description":"Who may comment. `everyone` (default), `followers`, `mentioned`, or `off` (author only).","enum":["everyone","followers","mentioned","off"]},"hide_reaction_counts":{"type":"boolean","description":"Suppress reaction_counts for everyone except the author. Default false."},"allow_repost":{"type":"boolean","description":"Allow this post to be reposted. Default true."},"allow_quote":{"type":"boolean","description":"Allow this post to be quoted. Default true."}}},"AssetAttachmentInputDto":{"type":"object","properties":{"asset_id":{"type":"string","description":"An uploaded asset UUID (status must be `ready`).","example":"eeeeeeee-0000-0000-0000-000000000001"},"alt_text":{"type":"string","description":"Per-attachment alt text (≤1000 chars).","maxLength":1000}},"required":["asset_id"]},"CreatePostDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Social actor UUID who authors the post (returned from POST /actors).","example":"aaaaaaaa-0000-0000-0000-000000000001"},"kind":{"type":"string","description":"Post kind. Free-form short label; common values: text, link, image, story.","example":"text","maxLength":64},"title":{"type":"string","description":"Optional title (1-240 chars).","example":"Shipping Phase 8","nullable":true,"maxLength":240},"body":{"type":"string","description":"Body markdown / plain text (up to 50000 chars).","example":"Stories are live!","nullable":true,"maxLength":50000},"url":{"type":"string","description":"Optional URL (for link / image kinds).","example":"https://cdn.example.com/img.png","nullable":true,"maxLength":2048},"attributes":{"type":"object","description":"Free-form attributes blob. Polls live here at `attributes.poll`.","example":{"media_url":"https://cdn.example.com/stories/1.jpg"}},"visibility":{"type":"string","description":"Visibility scope. Defaults to `public`.","enum":["public","followers","close_friends","private"],"example":"public"},"expires_at":{"type":"string","description":"ISO-8601 expiry timestamp. Used by stories (24h disappearing).","example":"2026-05-02T12:00:00Z","nullable":true},"status":{"type":"string","description":"Initial status. Defaults to `published`. Pass `draft` to save unpublished; pass `scheduled` with a future `scheduled_for` to defer publication. Scheduled posts are invisible to everyone except the author until the per-minute scheduler promotes them.","enum":["published","draft","scheduled"]},"scheduled_for":{"type":"string","description":"Required when status='scheduled'. ISO timestamp at least 30 seconds in the future. The PostSchedulerCron promotes scheduled posts to published once now() >= scheduled_for.","nullable":true},"source_post_id":{"type":"string","description":"For `kind='quote'` ONLY: the source post being quoted. Reposts use the dedicated POST /posts/:postId/repost route instead. Validation: when `kind='quote'`, both `source_post_id` and a non-empty `body` are required.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"settings":{"description":"Per-post interaction settings (comment policy, hide reaction counts, allow repost/quote). Omitted keys default to the open shape. Enforced server-side.","allOf":[{"$ref":"#/components/schemas/PostSettingsInputDto"}]},"tags":{"description":"Actor UUIDs to tag in the post (task 011). Each lands as a pending tag the actor approves. Non-actors and blocked pairs are skipped. Max 30.","example":["aaaaaaaa-0000-0000-0000-000000000002"],"type":"array","items":{"type":"string"}},"assets":{"description":"Ordered image attachments (task 019). Each references a `ready` asset in this community owned by the author. Max 20.","type":"array","items":{"$ref":"#/components/schemas/AssetAttachmentInputDto"}}},"required":["actor_id"]},"PostSettingsDto":{"type":"object","properties":{"comments":{"type":"string","description":"Who may comment. `everyone` (default), `followers`, `mentioned` (actors mentioned in the post body), or `off` (author only). Enforced server-side on comment create.","enum":["everyone","followers","mentioned","off"],"example":"everyone"},"hide_reaction_counts":{"type":"boolean","description":"When true, `reaction_counts` is suppressed (emitted as `{}`) for everyone except the post author, on every read surface.","example":false},"allow_repost":{"type":"boolean","description":"When false, reposting this post is refused (422 REPOST_NOT_ALLOWED).","example":true},"allow_quote":{"type":"boolean","description":"When false, quoting this post (kind='quote') is refused (422 QUOTE_NOT_ALLOWED).","example":true}},"required":["comments","hide_reaction_counts","allow_repost","allow_quote"]},"AssetAttachmentDto":{"type":"object","properties":{"asset_id":{"type":"string","description":"The attached asset UUID.","example":"eeeeeeee-0000-0000-0000-000000000001"},"alt_text":{"type":"string","description":"Per-attachment alt text for accessibility; null when unset.","nullable":true,"example":"A golden retriever on a beach"}},"required":["asset_id","alt_text"]},"PostResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Post UUID.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"actor_id":{"type":"string","description":"Author actor UUID.","example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},"kind":{"type":"string","description":"Free-form post kind. Common: text, link, image, story, repost, quote.","example":"text"},"title":{"type":"string","description":"Optional title (1-240 chars).","example":"Shipping Phase 8","nullable":true},"body":{"type":"string","description":"Body markdown / plain text (up to 50000 chars).","example":"Stories are live!","nullable":true},"url":{"type":"string","description":"Optional URL.","example":"https://cdn.example.com/img.png","nullable":true},"attributes":{"type":"object","description":"Free-form attributes blob (polls live at `attributes.poll`).","example":{}},"visibility":{"type":"string","description":"Visibility scope.","enum":["public","followers","close_friends","private"],"example":"public"},"status":{"type":"string","description":"Lifecycle status. Public list/feed surfaces only return `published`; `draft`, `scheduled`, and `archived` rows surface exclusively on their author-scoped listings and author reads. `removed` rows are hidden from reads. `archived` keeps engagement but is author-only.","enum":["published","hidden","removed","draft","scheduled","archived"],"example":"published"},"reaction_counts":{"type":"object","description":"Map of reaction type to count, denormalised on the post row for cheap feed reads.","example":{"like":12,"fire":3}},"comment_count":{"type":"number","description":"Comment count, denormalised on the post row.","example":4},"source_post_id":{"type":"string","description":"For kind='repost' / 'quote': the source post UUID.","example":null,"nullable":true},"repost_count":{"type":"number","description":"Denormalised count of currently-published reposts.","example":0},"quote_count":{"type":"number","description":"Denormalised count of currently-published quote posts.","example":0},"edited_at":{"type":"string","description":"ISO timestamp of the most recent edit (body/title/attributes change). Null when never edited.","example":null,"nullable":true},"edit_count":{"type":"number","description":"Number of edits applied; equals the count of post_revision rows.","example":0},"view_count":{"type":"number","description":"Denormalised impression count, bumped via the batch POST /posts/views endpoint.","example":0},"expires_at":{"type":"string","description":"ISO timestamp; `null` = no expiry. Stories typically set this 24h ahead.","example":null,"nullable":true},"pinned":{"type":"boolean","description":"When `true`, surfaces in the author's highlights endpoint regardless of expiry.","example":false},"settings":{"description":"Typed per-post interaction settings (comment policy, count hiding, repost/quote permission). Enforced server-side.","allOf":[{"$ref":"#/components/schemas/PostSettingsDto"}]},"scheduled_for":{"type":"string","description":"ISO publish-at timestamp, set while status='scheduled'. The scheduler cron promotes the post to published and clears this field; a manual PATCH promotion leaves the last value in place unless the caller also passes scheduled_for: null.","example":null,"nullable":true},"assets":{"description":"Ordered image attachments (task 019). Ref-only; resolve renditions via GET /assets/:id.","type":"array","items":{"$ref":"#/components/schemas/AssetAttachmentDto"}},"created_at":{"type":"string","description":"ISO timestamp the row was inserted.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp of the last update.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","actor_id","kind","title","body","url","attributes","visibility","status","reaction_counts","comment_count","source_post_id","repost_count","quote_count","edited_at","edit_count","view_count","expires_at","pinned","settings","scheduled_for","assets","created_at","updated_at"]},"PostListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PostResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UpdatePostDto":{"type":"object","properties":{"title":{"type":"string","description":"Updated title.","nullable":true,"maxLength":240},"body":{"type":"string","description":"Updated body.","nullable":true,"maxLength":50000},"url":{"type":"string","description":"Updated URL.","nullable":true,"maxLength":2048},"attributes":{"type":"object","description":"Replaces the attributes blob entirely."},"visibility":{"type":"string","enum":["public","followers","close_friends","private"]},"status":{"type":"string","description":"Direct status transition. Pass `published` to promote a `draft`/`scheduled` or restore an `archived` post; pass `archived` to hide-without-deleting (engagement kept). Prefer the moderation lane for `hidden`/`removed`.","enum":["published","hidden","removed","draft","scheduled","archived"]},"expires_at":{"type":"string","description":"ISO-8601 expiry timestamp; pass null to clear.","nullable":true},"scheduled_for":{"type":"string","description":"ISO-8601 publish-at timestamp. Updating only valid while the post is `status=\"scheduled\"`. Pass null to clear (typically only the scheduler does this on promotion).","nullable":true},"pinned":{"type":"boolean","description":"Toggle pinned status (used by highlights / stories)."},"settings":{"description":"Partial update of the per-post interaction settings. Only the keys you pass change; the rest keep their current value.","allOf":[{"$ref":"#/components/schemas/PostSettingsInputDto"}]},"tags":{"description":"Additional actor UUIDs to tag (task 011). Additive — creates new pending tags; never resets existing approved/removed tags. Max 30.","example":["aaaaaaaa-0000-0000-0000-000000000002"],"type":"array","items":{"type":"string"}}}},"PostRevisionResponseDto":{"type":"object","properties":{"post_id":{"type":"string","description":"Post UUID this revision belongs to.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"revision_n":{"type":"number","description":"Revision sequence number (1 = first edit).","example":1},"body":{"type":"string","description":"Body text BEFORE this edit landed.","example":"shipping today.","nullable":true},"title":{"type":"string","description":"Title BEFORE this edit landed.","example":"Phase 8","nullable":true},"attributes":{"type":"object","description":"Attributes BEFORE this edit landed.","example":{}},"edited_at":{"type":"string","description":"ISO timestamp the edit landed.","example":"2026-05-01T12:15:00.000Z"}},"required":["post_id","revision_n","body","title","attributes","edited_at"]},"PostRevisionListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PostRevisionResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"RecordViewsDto":{"type":"object","properties":{"post_ids":{"description":"UUIDs of posts that became visible to the viewer in this batch. Capped at 200 to keep the payload sensible — the server will silently skip ids that aren't in the community or have been removed.","example":["bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","cccccccc-cccc-cccc-cccc-cccccccccccc"],"type":"array","items":{"type":"string"}},"viewer_actor_id":{"type":"string","description":"Optional viewer actor UUID (task 021). When present, the impressions count toward the post's unique reach (deduped per viewer/day); omit for anonymous views (which count total views but not reach).","format":"uuid","example":"aaaaaaaa-0000-0000-0000-000000000001"}},"required":["post_ids"]},"DraftListResponseDto":{"type":"object","properties":{"data":{"description":"Draft post rows — status is always 'draft' on this endpoint.","type":"array","items":{"$ref":"#/components/schemas/PostResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateRepostDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Reposting actor UUID — the customer-backend supplies the end-user identity here.","example":"aaaaaaaa-0000-0000-0000-000000000001"}},"required":["actor_id"]},"CreateRepostResponseDto":{"type":"object","properties":{"repost":{"description":"The newly-created (or existing) repost post row.","allOf":[{"$ref":"#/components/schemas/PostResponseDto"}]},"source":{"description":"The source post, with refreshed `repost_count`.","allOf":[{"$ref":"#/components/schemas/PostResponseDto"}]},"created":{"type":"boolean","description":"`true` on first call; `false` on idempotent retry of an existing repost.","example":true}},"required":["repost","source","created"]},"DeprecatedActorIdBodyDto":{"type":"object","properties":{}},"RepostListResponseDto":{"type":"object","properties":{"data":{"description":"Page of repost post rows, each carrying the reposting actor_id.","type":"array","items":{"$ref":"#/components/schemas/PostResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"AssetUploadDto":{"type":"object","properties":{"url":{"type":"string","description":"Presigned PUT URL — upload the raw bytes here.","example":"https://…r2.cloudflarestorage.com/…?X-Amz-Signature=…"},"method":{"type":"string","enum":["PUT"],"example":"PUT"},"headers":{"type":"object","description":"Headers the PUT must carry — they are bound into the signature. Keys arrive lowercase (HTTP header names are case-insensitive).","example":{"content-type":"video/mp4","content-length":"10485760"}},"expires_at":{"type":"string","description":"Signature expiry — finish the PUT before this.","example":"2026-05-01T12:15:00.000Z"}},"required":["url","method","headers","expires_at"]},"UploadAssetResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"eeeeeeee-0000-0000-0000-000000000001"},"status":{"type":"string","enum":["uploading","processing","ready","failed"],"example":"ready"},"upload":{"description":"Present only on video creation — PUT the bytes here, then POST /assets/:id/complete.","allOf":[{"$ref":"#/components/schemas/AssetUploadDto"}]}},"required":["id","status"]},"AssetRenditionDto":{"type":"object","properties":{"name":{"type":"string","description":"Ladder rung — original/medium/thumb for images, original/video/poster for videos.","enum":["original","medium","thumb","video","poster"],"example":"medium"},"url":{"type":"string","description":"Short-lived presigned GET URL (re-minted each read).","example":"https://…r2.cloudflarestorage.com/…?X-Amz-Signature=…"},"content_type":{"type":"string","example":"image/jpeg"},"width":{"type":"number","nullable":true,"description":"Null on the un-probed video original.","example":1280},"height":{"type":"number","nullable":true,"description":"Null on the un-probed video original.","example":960},"byte_size":{"type":"number","example":184320}},"required":["name","url","content_type","width","height","byte_size"]},"AssetResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"eeeeeeee-0000-0000-0000-000000000001"},"community_id":{"type":"string","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"owner_actor_id":{"type":"string","example":"aaaaaaaa-0000-0000-0000-000000000001"},"kind":{"type":"string","enum":["image","video"],"example":"image"},"status":{"type":"string","description":"Videos: uploading → processing → ready | failed. Images never enter 'uploading'.","enum":["uploading","processing","ready","failed"],"example":"ready"},"content_type":{"type":"string","example":"image/jpeg"},"byte_size":{"type":"number","description":"Size of the original upload in bytes.","example":512000},"width":{"type":"number","nullable":true,"example":3024},"height":{"type":"number","nullable":true,"example":4032},"duration_s":{"type":"number","nullable":true,"description":"Output duration in seconds — ready videos only.","example":12.48},"failure_reason":{"type":"string","nullable":true,"description":"Stable code when status='failed' — ASSET_TOO_LARGE, ASSET_INVALID_CONTAINER, ASSET_DURATION_EXCEEDED, ASSET_TRANSCODE_FAILED, ASSET_PROCESSING_FAILED.","example":null},"renditions":{"type":"array","items":{"$ref":"#/components/schemas/AssetRenditionDto"}},"created_at":{"type":"string","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","owner_actor_id","kind","status","content_type","byte_size","width","height","duration_s","failure_reason","renditions","created_at","updated_at"]},"AssetListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"ActorCountersResponseDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Actor UUID.","example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"follower_count":{"type":"number","description":"How many actors follow this actor.","example":42},"following_count":{"type":"number","description":"How many actors this actor follows.","example":17},"post_count":{"type":"number","description":"Published posts authored by this actor.","example":5},"comment_count":{"type":"number","description":"Published comments authored by this actor.","example":12},"blocks_count":{"type":"number","description":"Actors this actor has blocked.","example":0},"updated_at":{"type":"string","description":"ISO timestamp of the last counter update.","example":"2026-05-01T12:00:00.000Z"}},"required":["actor_id","community_id","follower_count","following_count","post_count","comment_count","blocks_count","updated_at"]},"BatchCountersResponseDto":{"type":"object","properties":{"data":{"description":"Counters for each actor found, in `actor_ids` order (after de-duplication).","type":"array","items":{"$ref":"#/components/schemas/ActorCountersResponseDto"}},"missing":{"description":"actor_ids with no actor in this community.","example":[],"type":"array","items":{"type":"string"}}},"required":["data","missing"]},"CommunityAnalyticsResponseDto":{"type":"object","properties":{"community_id":{"type":"string","description":"Community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"total_actors":{"type":"number","description":"Active actor rows count.","example":1234},"total_posts":{"type":"number","description":"Posts where status='published'.","example":5678},"total_comments":{"type":"number","description":"Comments where status='published'.","example":9876},"total_open_flags":{"type":"number","description":"Flags where status='open'.","example":3},"posts_last_7d":{"type":"number","description":"Posts created in the last 7 days (any status).","example":42},"comments_last_7d":{"type":"number","description":"Comments created in the last 7 days (any status).","example":88},"actors_last_7d":{"type":"number","description":"Actors created in the last 7 days.","example":12},"computed_at":{"type":"string","description":"ISO timestamp this rollup was computed at.","example":"2026-05-01T12:00:00.000Z"}},"required":["community_id","total_actors","total_posts","total_comments","total_open_flags","posts_last_7d","comments_last_7d","actors_last_7d","computed_at"]},"ActorEdgeDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Acting social actor UUID (the one performing the follow / mute / block / restrict). Standardized name (task 008 / G11).","example":"aaaaaaaa-0000-0000-0000-000000000001"},"dst_actor_id":{"type":"string","description":"Destination social actor UUID (the one being followed / muted / blocked / restricted).","example":"aaaaaaaa-0000-0000-0000-000000000002"},"payload":{"type":"object","description":"Optional free-form payload stored on the edge (e.g. mute reason)."}},"required":["actor_id","dst_actor_id"]},"BatchFollowsDto":{"type":"object","properties":{"follows":{"description":"Follow edges to create, in order. 1–200 items; each item follows the single-follow body exactly.","type":"array","items":{"$ref":"#/components/schemas/ActorEdgeDto"}},"suppress_notifications":{"type":"boolean","description":"Import lane: when `true`, the per-item `edge.follow.created` fan-out (follow notifications AND webhooks) is skipped for this batch. Follower/following counters still update. Use when importing an existing graph so long-standing followers don't get \"X followed you\" notifications.","default":false,"example":true}},"required":["follows"]},"EdgeResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Edge UUID.","example":"eeeeeeee-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"kind":{"type":"string","description":"Edge kind. The actor-edge routes emit exactly these four; post reactions and bookmarks have their own response shapes.","enum":["follow","mute","block","restrict"],"example":"follow"},"src_actor_id":{"type":"string","description":"Source actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"dst_actor_id":{"type":"string","description":"Destination actor UUID (for actor edges).","example":"aaaaaaaa-0000-0000-0000-000000000002","nullable":true},"dst_post_id":{"type":"string","description":"Destination post UUID (for reaction / save edges).","example":null,"nullable":true},"payload":{"type":"object","description":"Free-form payload stored on the edge (e.g. mute reason).","example":{}},"created_at":{"type":"string","description":"ISO timestamp the edge was created.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","kind","src_actor_id","dst_actor_id","dst_post_id","payload","created_at"]},"BatchFollowResultDto":{"type":"object","properties":{"index":{"type":"number","description":"Position of this result in the request array.","example":0},"success":{"type":"boolean","description":"`true` when this follow was created (or already existed); `false` when it failed (see `error`).","example":true},"edge":{"description":"The follow edge — present on success.","allOf":[{"$ref":"#/components/schemas/EdgeResponseDto"}]},"created":{"type":"boolean","description":"`true` if this item created a new edge, `false` on an idempotent retry of an existing follow. Present on success.","example":true},"error":{"description":"The error this item would have returned as a standalone call (e.g. 409 block in place, 422 self-follow, 404 unknown actor) — present on failure.","allOf":[{"$ref":"#/components/schemas/ErrorResponseDto"}]}},"required":["index","success"]},"BatchFollowsResponseDto":{"type":"object","properties":{"data":{"description":"One result per input item, same order.","type":"array","items":{"$ref":"#/components/schemas/BatchFollowResultDto"}},"succeeded":{"type":"number","description":"Count of successful items.","example":4999},"failed":{"type":"number","description":"Count of failed items.","example":1}},"required":["data","succeeded","failed"]},"FollowResultDto":{"type":"object","properties":{"status":{"type":"string","description":"`followed` = an edge exists; `pending` = a follow request was created (private dst).","enum":["followed","pending"],"example":"followed"},"edge":{"nullable":true,"description":"The follow edge when `status=followed`; null when `pending`.","allOf":[{"$ref":"#/components/schemas/EdgeResponseDto"}]},"created":{"type":"boolean","description":"`true` if the edge / request was created on this call; `false` on idempotent retry.","example":true}},"required":["status","edge","created"]},"CreateEdgeResponseDto":{"type":"object","properties":{"edge":{"$ref":"#/components/schemas/EdgeResponseDto"},"created":{"type":"boolean","description":"`true` if the edge was created on this call; `false` on idempotent retry of an existing edge.","example":true}},"required":["edge","created"]},"FollowRequestEntryDto":{"type":"object","properties":{"request_id":{"type":"string","description":"Follow request UUID.","example":"ffffffff-1111-0000-0000-000000000001"},"src_actor_id":{"type":"string","description":"Requesting actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"src_external_id":{"type":"string","description":"Requesting actor's external_id.","example":"bob"},"src_display_name":{"type":"string","description":"Requesting actor display name.","example":"Bob","nullable":true},"src_avatar_url":{"type":"string","description":"Requesting actor avatar URL.","example":null,"nullable":true},"created_at":{"type":"string","description":"When the request was created.","example":"2026-05-01T12:00:00.000Z"}},"required":["request_id","src_actor_id","src_external_id","src_display_name","src_avatar_url","created_at"]},"FollowRequestListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FollowRequestEntryDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"OutboundEdgeResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Edge UUID.","example":"eeeeeeee-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"kind":{"type":"string","description":"Edge kind. The actor-edge routes emit exactly these four; post reactions and bookmarks have their own response shapes.","enum":["follow","mute","block","restrict"],"example":"follow"},"src_actor_id":{"type":"string","description":"Source actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"dst_actor_id":{"type":"string","description":"Destination actor UUID (for actor edges).","example":"aaaaaaaa-0000-0000-0000-000000000002","nullable":true},"dst_post_id":{"type":"string","description":"Destination post UUID (for reaction / save edges).","example":null,"nullable":true},"payload":{"type":"object","description":"Free-form payload stored on the edge (e.g. mute reason).","example":{}},"created_at":{"type":"string","description":"ISO timestamp the edge was created.","example":"2026-05-01T12:00:00.000Z"},"dst_actor":{"description":"The destination actor's full row. Present only when requested via `?expand=dst_actor`.","allOf":[{"$ref":"#/components/schemas/ActorResponseDto"}]}},"required":["id","community_id","kind","src_actor_id","dst_actor_id","dst_post_id","payload","created_at"]},"OutboundEdgeListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OutboundEdgeResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"FollowerEdgeResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Edge UUID.","example":"eeeeeeee-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"kind":{"type":"string","description":"Edge kind. The actor-edge routes emit exactly these four; post reactions and bookmarks have their own response shapes.","enum":["follow","mute","block","restrict"],"example":"follow"},"src_actor_id":{"type":"string","description":"Source actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"dst_actor_id":{"type":"string","description":"Destination actor UUID (for actor edges).","example":"aaaaaaaa-0000-0000-0000-000000000002","nullable":true},"dst_post_id":{"type":"string","description":"Destination post UUID (for reaction / save edges).","example":null,"nullable":true},"payload":{"type":"object","description":"Free-form payload stored on the edge (e.g. mute reason).","example":{}},"created_at":{"type":"string","description":"ISO timestamp the edge was created.","example":"2026-05-01T12:00:00.000Z"},"src_actor":{"description":"The follower's full actor row. Present only when requested via `?expand=src_actor`.","allOf":[{"$ref":"#/components/schemas/ActorResponseDto"}]}},"required":["id","community_id","kind","src_actor_id","dst_actor_id","dst_post_id","payload","created_at"]},"FollowerEdgeListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FollowerEdgeResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"HidePostDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"The actor hiding the post.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"post_id":{"type":"string","description":"The post to hide.","example":"bbbbbbbb-0000-0000-0000-000000000001"}},"required":["actor_id","post_id"]},"HideResultDto":{"type":"object","properties":{"created":{"type":"boolean","description":"`true` if the hide was created on this call; `false` on idempotent retry.","example":true}},"required":["created"]},"HideEntryDto":{"type":"object","properties":{"post_id":{"type":"string","description":"The hidden post UUID.","example":"bbbbbbbb-0000-0000-0000-000000000001"},"created_at":{"type":"string","description":"When the post was hidden.","example":"2026-05-01T12:00:00.000Z"}},"required":["post_id","created_at"]},"HideListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/HideEntryDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"RelationshipEntryDto":{"type":"object","properties":{"dst_actor_id":{"type":"string","description":"Destination actor UUID this entry describes.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"following":{"type":"boolean","description":"The source actor follows this actor.","example":true},"followed_by":{"type":"boolean","description":"This actor follows the source actor.","example":false},"muting":{"type":"boolean","description":"The source actor has muted this actor.","example":false},"blocking":{"type":"boolean","description":"The source actor has blocked this actor.","example":false},"blocked_by":{"type":"boolean","description":"This actor has blocked the source actor — surface it BEFORE attempting a follow/react write instead of handling the 409.","example":false},"restricting":{"type":"boolean","description":"The source actor has restricted this actor.","example":false},"close_friend":{"type":"boolean","description":"This actor is on the source actor's close-friend list.","example":false},"follow_pending":{"type":"boolean","description":"The source actor has a pending follow request against this (private) actor (task 014).","example":false}},"required":["dst_actor_id","following","followed_by","muting","blocking","blocked_by","restricting","close_friend","follow_pending"]},"RelationshipListResponseDto":{"type":"object","properties":{"data":{"description":"One entry per requested id (de-duplicated, request order). Unknown ids resolve to all-false flags.","type":"array","items":{"$ref":"#/components/schemas/RelationshipEntryDto"}}},"required":["data"]},"ConnectionRequestDto":{"type":"object","properties":{"src_actor_id":{"type":"string","description":"Requesting actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"dst_actor_id":{"type":"string","description":"Actor being invited to connect.","example":"aaaaaaaa-0000-0000-0000-000000000002"}},"required":["src_actor_id","dst_actor_id"]},"ConnectionResultDto":{"type":"object","properties":{"status":{"type":"string","enum":["pending","connected"],"description":"'pending' — a connection request now awaits the destination's accept. 'connected' — both follow edges exist."},"created":{"type":"boolean","description":"False when the call was an idempotent replay."}},"required":["status","created"]},"HashtagResponseDto":{"type":"object","properties":{"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"tag":{"type":"string","description":"Lowercase tag (1..32 chars from [a-z0-9_]).","example":"storymode"},"first_used_at":{"type":"string","description":"ISO timestamp the tag first appeared in this community.","example":"2026-05-01T12:00:00.000Z"},"window_post_count":{"type":"number","description":"Published posts using the tag inside the trending window; non-null only when `order=trending` (task 023).","nullable":true,"example":null}},"required":["community_id","tag","first_used_at","window_post_count"]},"HashtagListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/HashtagResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"HashtagDetailResponseDto":{"type":"object","properties":{"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"tag":{"type":"string","description":"Lowercase tag (1..32 chars from [a-z0-9_]).","example":"storymode"},"first_used_at":{"type":"string","description":"ISO timestamp the tag first appeared in this community.","example":"2026-05-01T12:00:00.000Z"},"window_post_count":{"type":"number","description":"Published posts using the tag inside the trending window; non-null only when `order=trending` (task 023).","nullable":true,"example":null},"post_count":{"type":"number","description":"Number of currently-published posts using the tag.","example":17}},"required":["community_id","tag","first_used_at","window_post_count","post_count"]},"HashtagAutocompleteResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/HashtagResponseDto"}},"pagination":{"nullable":true,"description":"Always `null` — autocomplete returns a single bounded, ranked page (no cursor). Present for envelope consistency (task 008 / G13).","allOf":[{"$ref":"#/components/schemas/PaginationResponseDto"}]}},"required":["data","pagination"]},"CreateHashtagFollowDto":{"type":"object","properties":{"tag":{"type":"string","description":"Lowercase tag (without `#`). Must already exist in the community directory.","example":"storymode","minLength":1,"maxLength":32}},"required":["tag"]},"HashtagFollowResponseDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"tag":{"type":"string","description":"Lowercase tag (without `#`).","example":"storymode"},"followed_at":{"type":"string","description":"ISO timestamp the follow was created.","example":"2026-05-01T12:00:00.000Z"}},"required":["actor_id","community_id","tag","followed_at"]},"HashtagFollowListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/HashtagFollowResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"InsightsDailyBucketDto":{"type":"object","properties":{"day":{"type":"string","description":"ISO date (YYYY-MM-DD).","example":"2026-05-01"},"views":{"type":"number","description":"Total impressions that day (repeats + anonymous).","example":340},"reach":{"type":"number","description":"Distinct viewers that day.","example":210}},"required":["day","views","reach"]},"PostInsightsResponseDto":{"type":"object","properties":{"post_id":{"type":"string","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"community_id":{"type":"string","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"views":{"type":"number","description":"All-time total impressions.","example":4820},"unique_reach":{"type":"number","description":"All-time distinct viewers.","example":3110},"reaction_count":{"type":"number","example":128},"comment_count":{"type":"number","example":34},"repost_count":{"type":"number","example":9},"quote_count":{"type":"number","example":4},"daily":{"type":"array","items":{"$ref":"#/components/schemas/InsightsDailyBucketDto"}}},"required":["post_id","community_id","views","unique_reach","reaction_count","comment_count","repost_count","quote_count","daily"]},"ActorFollowerPointDto":{"type":"object","properties":{"day":{"type":"string","example":"2026-05-01"},"follower_count":{"type":"number","example":1240}},"required":["day","follower_count"]},"ActorInsightsResponseDto":{"type":"object","properties":{"actor_id":{"type":"string","example":"aaaaaaaa-0000-0000-0000-000000000001"},"community_id":{"type":"string","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"follower_count":{"type":"number","description":"Live follower count.","example":1240},"daily":{"type":"array","items":{"$ref":"#/components/schemas/ActorFollowerPointDto"}}},"required":["actor_id","community_id","follower_count","daily"]},"ActingActorDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"The actor performing the action (approve: the tagged actor; remove: the tagged actor or the post author).","example":"aaaaaaaa-0000-0000-0000-000000000002"}},"required":["actor_id"]},"PostTagResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Tag UUID.","example":"ffffffff-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"post_id":{"type":"string","description":"Tagged post UUID.","example":"bbbbbbbb-0000-0000-0000-000000000001"},"tagged_actor_id":{"type":"string","description":"Tagged actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"state":{"type":"string","description":"Lifecycle state.","enum":["pending","approved","removed"],"example":"approved"},"metadata":{"type":"object","description":"Opaque customer-defined metadata blob.","nullable":true,"example":null},"created_at":{"type":"string","description":"ISO timestamp the tag was created.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp of the last state change.","example":"2026-05-01T12:05:00.000Z"}},"required":["id","community_id","post_id","tagged_actor_id","state","metadata","created_at","updated_at"]},"TaggedPostsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PostResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateCommentDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Social actor UUID who authors the comment.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"body":{"type":"string","description":"Comment body (1-10000 chars after trim).","example":"Looks great, shipping today.","minLength":1,"maxLength":10000},"parent_id":{"type":"string","description":"Parent comment UUID for replies. Omit for a top-level comment.","example":"bbbbbbbb-0000-0000-0000-000000000001","nullable":true},"visibility":{"type":"string","description":"Visibility scope. `author_only` = DM-style (post author + comment author only).","enum":["public","author_only"],"example":"public"},"assets":{"description":"Ordered image attachments (task 019). Each references a `ready` asset in this community owned by the author. Max 20.","type":"array","items":{"$ref":"#/components/schemas/AssetAttachmentInputDto"}}},"required":["actor_id","body"]},"CommentResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Comment UUID.","example":"bbbbbbbb-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"post_id":{"type":"string","description":"Post the comment threads under.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"parent_id":{"type":"string","description":"Parent comment UUID for replies; `null` for top-level.","example":null,"nullable":true},"actor_id":{"type":"string","description":"Comment author actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"body":{"type":"string","description":"Comment body.","example":"Looks great, shipping today."},"depth":{"type":"number","description":"Thread depth — 0 for top-level, 1 for first reply, etc. Capped per community.","example":0},"status":{"type":"string","description":"Lifecycle status. `pending_approval` = held by restricted-commenter pre-moderation; list endpoints surface these rows only to the comment author and the post-author (who publishes them via POST :commentId/approve). `removed` rows are hidden from public reads.","enum":["published","hidden","removed","pending_approval"],"example":"published"},"visibility":{"type":"string","description":"`public` (default) is visible to anyone who can read the post; `author_only` is DM-style (post-author + comment-author only).","enum":["public","author_only"],"example":"public"},"reaction_counts":{"type":"object","description":"Map of reaction type to count, denormalised on the comment row in lock-step with the comment-reaction writes.","example":{"like":2}},"pinned":{"type":"boolean","description":"True when the post author has pinned this top-level comment (task 012). Pinned comments lead the top-level listing.","example":false},"pinned_at":{"type":"string","description":"ISO timestamp the comment was pinned; null when not pinned.","example":null,"nullable":true},"assets":{"description":"Ordered image attachments (task 019). Ref-only; resolve renditions via GET /assets/:id.","type":"array","items":{"$ref":"#/components/schemas/AssetAttachmentDto"}},"created_at":{"type":"string","description":"ISO timestamp the row was inserted.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp of the last update.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","post_id","parent_id","actor_id","body","depth","status","visibility","reaction_counts","pinned","pinned_at","assets","created_at","updated_at"]},"CommentListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CommentResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UpdateCommentDto":{"type":"object","properties":{"body":{"type":"string","description":"Updated body.","minLength":1,"maxLength":10000},"status":{"type":"string","description":"Direct status transition. Prefer the moderation lane for `hidden`/`removed`.","enum":["published","hidden","removed"]},"visibility":{"type":"string","enum":["public","author_only"]}}},"ApproveCommentDto":{"type":"object","properties":{"approver_actor_id":{"type":"string","description":"The post-author — must equal the post.actor_id.","example":"aaaaaaaa-0000-0000-0000-000000000001"}},"required":["approver_actor_id"]},"PinCommentDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"The acting actor — must equal the post.actor_id (task 012).","example":"aaaaaaaa-0000-0000-0000-000000000001"}},"required":["actor_id"]},"CreateCommentReactionDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Reactor actor UUID. Standardized name (task 008 / G11).","example":"aaaaaaaa-0000-0000-0000-000000000001"},"type":{"type":"string","description":"Reaction type (free-form short label, e.g. `like`, `fire`, `heart`).","example":"like","minLength":1,"maxLength":64}},"required":["actor_id","type"]},"CommentReactionEntryDto":{"type":"object","properties":{"comment_id":{"type":"string","description":"Comment UUID.","example":"bbbbbbbb-0000-0000-0000-000000000001"},"actor_id":{"type":"string","description":"Reactor actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"type":{"type":"string","description":"Reaction type label.","example":"like"},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["comment_id","actor_id","community_id","type","created_at"]},"AddCommentReactionResponseDto":{"type":"object","properties":{"reaction":{"$ref":"#/components/schemas/CommentReactionEntryDto"},"created":{"type":"boolean","description":"`true` on first call; `false` on idempotent retry.","example":true},"reaction_counts":{"type":"object","description":"Live denormalised counts on the comment after this write.","example":{"like":4}}},"required":["reaction","created","reaction_counts"]},"CommentReactionCountsResponseDto":{"type":"object","properties":{"reaction_counts":{"type":"object","description":"Live denormalised counts after the delete (or current counts if the reaction was already absent).","example":{"like":3}}},"required":["reaction_counts"]},"CommentReactionListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CommentReactionEntryDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateMutedTermDto":{"type":"object","properties":{"term":{"type":"string","description":"Term to mute. 1..200 chars after trim.","minLength":1,"maxLength":200,"example":"spoiler"},"scope":{"type":"string","description":"Default `all`.","enum":["feed","dm","mention","all"]},"action":{"type":"string","description":"What matching does. `mute` (default) or `pending_approval` (task 015).","enum":["mute","pending_approval"]},"expires_at":{"type":"string","description":"Optional expiry (ISO timestamp in the future).","nullable":true}},"required":["term"]},"MutedTermResponseDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Owning actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"term":{"type":"string","description":"Case-insensitive substring.","example":"spoiler"},"scope":{"type":"string","description":"Where the mute applies.","enum":["feed","dm","mention","all"],"example":"all"},"action":{"type":"string","description":"What matching does (task 015). `mute` = feed-filter only; `pending_approval` = hold a stranger's comment on your post for review.","enum":["mute","pending_approval"],"example":"mute"},"expires_at":{"type":"string","description":"ISO timestamp; null = never expires.","example":null,"nullable":true},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["actor_id","community_id","term","scope","action","expires_at","created_at"]},"MutedTermListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MutedTermResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"FeedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PostResponseDto"}},"pagination":{"description":"Cursor encodes `(score, id)` for `ranked` feeds and `(created_at, id)` for `chronological` feeds. Cursors are NOT interchangeable between modes.","allOf":[{"$ref":"#/components/schemas/PaginationResponseDto"}]}},"required":["data","pagination"]},"CreateActorListDto":{"type":"object","properties":{"owner_id":{"type":"string","description":"List owner — social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"name":{"type":"string","description":"Display name (1..200).","minLength":1,"maxLength":200,"example":"Tech news"},"description":{"type":"string","description":"Optional description (0..2000).","maxLength":2000,"nullable":true},"visibility":{"type":"string","description":"Default `private`.","enum":["public","private"],"example":"public"}},"required":["owner_id","name"]},"ActorListResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"List UUID.","example":"llllllll-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"owner_id":{"type":"string","description":"List owner — social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"name":{"type":"string","description":"List display name.","example":"Tech news"},"description":{"type":"string","description":"Optional description.","example":"Curated tech accounts I follow.","nullable":true},"visibility":{"type":"string","description":"public = discoverable; private = owner-only.","enum":["public","private"],"example":"public"},"member_count":{"type":"number","description":"Current member count.","example":12},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","owner_id","name","description","visibility","member_count","created_at","updated_at"]},"ActorListListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ActorListResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UpdateActorListDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","description":"Caller — social actor UUID. Must equal the list owner.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"name":{"type":"string","description":"New name.","minLength":1,"maxLength":200},"description":{"type":"string","description":"New description.","maxLength":2000,"nullable":true},"visibility":{"type":"string","description":"New visibility.","enum":["public","private"]}},"required":["caller_actor_id"]},"DeleteActorListDto":{"type":"object","properties":{}},"AddListMemberDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","description":"Caller — social actor UUID. Must equal the list owner.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"actor_id":{"type":"string","description":"New member — social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"}},"required":["caller_actor_id","actor_id"]},"ActorListMemberResponseDto":{"type":"object","properties":{"list_id":{"type":"string","description":"Owning list UUID.","example":"llllllll-0000-0000-0000-000000000001"},"actor_id":{"type":"string","description":"Member actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"added_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["list_id","actor_id","community_id","added_at"]},"RemoveListMemberDto":{"type":"object","properties":{}},"ActorListMemberListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ActorListMemberResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateFlagDto":{"type":"object","properties":{"reporter_actor_id":{"type":"string","description":"Social actor UUID who is reporting the content.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"target_kind":{"type":"string","description":"What kind of object is being flagged (task 017 adds actor + direct_message). A direct_message flag requires the reporter to be a participant in the conversation.","enum":["post","comment","actor","direct_message"],"example":"post"},"target_id":{"type":"string","description":"UUID of the target (post / comment / actor / message id per target_kind).","example":"pppppppp-pppp-pppp-pppp-pppppppppppp"},"reason":{"type":"string","description":"Optional free-form reason (up to 1000 chars).","example":"Spam — same link posted 6 times in 5 minutes.","maxLength":1000}},"required":["reporter_actor_id","target_kind","target_id"]},"FlagResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Flag UUID.","example":"ffffffff-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"target_kind":{"type":"string","description":"What kind of object is being flagged.","enum":["post","comment","actor","direct_message"],"example":"post"},"target_id":{"type":"string","description":"UUID of the post or comment being flagged.","example":"pppppppp-pppp-pppp-pppp-pppppppppppp"},"reporter_actor_id":{"type":"string","description":"Social actor UUID who reported.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"reason":{"type":"string","description":"Free-text reason; empty string when none provided.","example":"Spam — same link posted 6 times in 5 minutes."},"status":{"type":"string","description":"Lifecycle. `open` is in the queue; admins close to `dismissed` or `actioned`.","enum":["open","dismissed","actioned"],"example":"open"},"created_at":{"type":"string","description":"ISO timestamp the row was inserted.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp of the last update.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","target_kind","target_id","reporter_actor_id","reason","status","created_at","updated_at"]},"CreateFlagResponseDto":{"type":"object","properties":{"flag":{"description":"The flag row (existing open flag on de-duplicated retry).","allOf":[{"$ref":"#/components/schemas/FlagResponseDto"}]},"created":{"type":"boolean","description":"`true` when this call inserted a new flag; `false` when de-duplicated to an existing open flag from the same reporter on the same target.","example":true},"auto_hidden":{"type":"boolean","description":"`true` when this flag pushed the target over the community's `auto_hide_flag_threshold` and the target was auto-hidden as a side effect.","example":false}},"required":["flag","created","auto_hidden"]},"FlagListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FlagResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"ModerationActionDto":{"type":"object","properties":{"action":{"type":"string","description":"Action to apply. `hide`/`remove` transition target status; `unhide`/`restore` reverse them. `warn`/`ban` are audit-only — they record the decision without touching the target; for actor-level enforcement use PATCH `/actors/:actorId` with `status=suspended` or the shadow-ban endpoints on this controller. `dismiss` closes the flag with no target change.","enum":["hide","remove","warn","ban","dismiss","unhide","restore"],"example":"hide"},"notes":{"type":"string","description":"Optional moderator notes attached to the audit row (up to 2000 chars).","example":"Spam confirmed by manual review.","maxLength":2000}},"required":["action"]},"ModerationActionResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Action UUID.","example":"mmmmmmmm-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"flag_id":{"type":"string","description":"Flag UUID this action was applied to.","example":"ffffffff-0000-0000-0000-000000000001","nullable":true},"target_kind":{"type":"string","enum":["post","comment","actor","direct_message"],"example":"post"},"target_id":{"type":"string","description":"UUID of the targeted post / comment.","example":"pppppppp-pppp-pppp-pppp-pppppppppppp"},"moderator_account_id":{"type":"string","description":"PlatformUser account UUID of the moderator.","example":"99999999-0000-0000-0000-000000000001"},"action":{"type":"string","description":"Action applied.","enum":["hide","remove","warn","ban","dismiss","unhide","restore"],"example":"hide"},"notes":{"type":"string","description":"Optional moderator notes.","example":"Spam confirmed.","nullable":true},"created_at":{"type":"string","description":"ISO timestamp the action was recorded.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","flag_id","target_kind","target_id","moderator_account_id","action","notes","created_at"]},"ActOnFlagResponseDto":{"type":"object","properties":{"flag":{"description":"The flag after the action (status flipped to actioned/dismissed for punitive/dismiss actions; reversals re-resolve it too).","allOf":[{"$ref":"#/components/schemas/FlagResponseDto"}]},"moderation_action":{"description":"The audit row this action recorded.","allOf":[{"$ref":"#/components/schemas/ModerationActionResponseDto"}]}},"required":["flag","moderation_action"]},"DirectModerationActionDto":{"type":"object","properties":{"target_kind":{"type":"string","description":"What the action targets (task 017). post/comment → status transition; direct_message → moderator soft-delete; actor → ban=suspend / restore=reinstate.","enum":["post","comment","actor","direct_message"],"example":"direct_message"},"target_id":{"type":"string","description":"The target UUID (post/comment/actor/message id per target_kind).","example":"bbbbbbbb-0000-0000-0000-000000000001"},"action":{"type":"string","description":"Action. hide/remove act on the target; ban/restore suspend/reinstate an actor; warn is audit-only. The audit row has flag_id=null (unsolicited action).","enum":["hide","remove","warn","ban","dismiss","unhide","restore"],"example":"remove"},"notes":{"type":"string","description":"Optional moderator notes (up to 2000 chars).","maxLength":2000}},"required":["target_kind","target_id","action"]},"AuditListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ModerationActionResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"ApplyShadowBanDto":{"type":"object","properties":{"duration_hours":{"type":"number","description":"Hours until auto-restore. Omit for indefinite.","nullable":true,"example":168},"reason":{"type":"string","description":"Moderator note (1..1000 chars).","nullable":true,"maxLength":1000}}},"ModerationActorResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Social actor UUID.","example":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"external_id":{"type":"string","description":"Customer-supplied stable identifier (your user id).","example":"user_abc123"},"display_name":{"type":"string","description":"Display name.","example":"Ada Lovelace","nullable":true},"avatar_url":{"type":"string","description":"Avatar URL.","example":"https://cdn.example.com/avatars/ada.png","nullable":true},"metadata":{"type":"object","description":"Free-form metadata blob.","example":{"tier":"pro"}},"status":{"type":"string","description":"Lifecycle status. `suspended` is moderator-applied via PATCH `status=suspended`; `deleted` is soft-deletion. Shadow-banning is a separate moderation mechanism that does not change `status`.","enum":["active","suspended","deleted"],"example":"active"},"is_private":{"type":"boolean","description":"When true, the actor is private (task 014): effective post visibility is capped at `followers` and following requires an approved follow request.","example":false},"created_at":{"type":"string","description":"ISO timestamp the row was inserted.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp of the last update.","example":"2026-05-01T12:00:00.000Z"},"shadow_banned_at":{"type":"string","description":"ISO timestamp the shadow ban was applied, or null.","nullable":true},"shadow_banned_until":{"type":"string","description":"ISO timestamp the shadow ban auto-expires, or null for indefinite.","nullable":true},"shadow_banned_reason":{"type":"string","description":"Moderator note attached to the ban.","nullable":true}},"required":["id","community_id","external_id","display_name","avatar_url","metadata","status","is_private","created_at","updated_at","shadow_banned_at","shadow_banned_until","shadow_banned_reason"]},"ModerationActorListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ModerationActorResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateBlockedTermDto":{"type":"object","properties":{"term":{"type":"string","description":"The word/phrase to block (case-insensitive substring). Max 128 chars.","example":"spoiler"},"action":{"type":"string","description":"What a match does. Default `hide`.","enum":["hide","pending_approval"],"example":"hide"}},"required":["term"]},"BlockedTermResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Blocked term UUID.","example":"ffffffff-2222-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"term":{"type":"string","description":"The blocked term (lowercased).","example":"spoiler"},"action":{"type":"string","description":"What a match does.","enum":["hide","pending_approval"],"example":"hide"},"created_at":{"type":"string","description":"ISO timestamp the term was added.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","term","action","created_at"]},"BlockedTermListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BlockedTermResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UnreadCountResponseDto":{"type":"object","properties":{"count":{"type":"number","description":"Number of unread incoming messages across every conversation the actor participates in.","example":7}},"required":["count"]},"CreateConversationDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Social actor UUID opening the conversation.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"kind":{"type":"string","description":"Conversation shape. Default `direct`.","enum":["direct","group"],"example":"direct"},"recipient_actor_id":{"type":"string","description":"For 1-on-1 (`kind=\"direct\"`): the other participant — social actor UUID. Required when kind=direct.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"actor_ids":{"description":"For groups (`kind=\"group\"`): initial members (excluding the creator). Required when kind=group. Capped by `community.settings.max_group_conversation_size` (default 50).","example":["aaaaaaaa-0000-0000-0000-000000000002","aaaaaaaa-0000-0000-0000-000000000003"],"type":"array","items":{"type":"string"}},"name":{"type":"string","description":"Optional group display name. Ignored for 1-on-1.","maxLength":200}},"required":["actor_id"]},"ConversationParticipantDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Participant actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"joined_at":{"type":"string","description":"ISO timestamp the actor joined.","example":"2026-05-01T12:00:00.000Z"},"last_read_at":{"type":"string","description":"ISO timestamp of the participant's last read marker; `null` until first /read.","example":null,"nullable":true},"muted":{"type":"boolean","description":"When true, dm_new_message notifications are suppressed indefinitely for this participant.","example":false},"muted_until":{"type":"string","description":"Timed mute (task 024): dm_new_message is suppressed while this is in the future. Additive beside `muted`; a past value is inert. Null when no timed mute.","example":null,"nullable":true},"pinned":{"type":"boolean","description":"When true, the thread is pinned to the top of this actor's inbox (task 024).","example":false},"role":{"type":"string","description":"Role on this conversation. `admin` for groups can add / remove members and rename.","enum":["member","admin"],"example":"member"}},"required":["actor_id","joined_at","last_read_at","muted","muted_until","pinned","role"]},"ConversationResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Conversation UUID.","example":"11111111-2222-3333-4444-555555555555"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"kind":{"type":"string","description":"Conversation shape — 1-on-1 (`direct`) or multi-participant (`group`).","enum":["direct","group"],"example":"direct"},"name":{"type":"string","description":"Group display name; null for 1-on-1.","example":null,"nullable":true},"created_by":{"type":"string","description":"Social actor UUID of the group founder; null for legacy / 1-on-1.","example":null,"nullable":true},"participants":{"description":"Participants, joined-at ascending.","type":"array","items":{"$ref":"#/components/schemas/ConversationParticipantDto"}},"unread_count":{"type":"number","description":"Unread messages for the requesting actor (task 024) — messages from others created after the requester's last_read_at. Embedded on list + single GET for inbox badges.","example":0},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"last_message_at":{"type":"string","description":"ISO timestamp of the most recent message (or conversation create when no messages yet).","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","kind","name","created_by","participants","unread_count","created_at","updated_at","last_message_at"]},"AddMemberDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","description":"Group admin performing the add — social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"actor_id":{"type":"string","description":"The new member — social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000004"},"role":{"type":"string","description":"Default `member`.","enum":["member","admin"],"example":"member"}},"required":["caller_actor_id","actor_id"]},"UpdateMemberDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","description":"The actor performing the change — social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"role":{"type":"string","description":"Target role (admin-only). Omit to leave unchanged.","enum":["member","admin"],"example":"admin"},"muted":{"type":"boolean","description":"Mute the thread indefinitely (self-only — caller must be the path actor). Task 024/025.","example":true},"muted_until":{"type":"string","description":"Timed mute until this ISO timestamp (self-only). Pass null to clear.","nullable":true,"example":"2026-05-02T12:00:00Z"},"pinned":{"type":"boolean","description":"Pin / unpin the thread in the caller's inbox (self-only).","example":true}},"required":["caller_actor_id"]},"RemoveMemberDto":{"type":"object","properties":{}},"ConversationListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ConversationResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UpdateConversationDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"The participant whose row is being updated.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"name":{"type":"string","description":"Rename a group conversation (task 025) — group-only, admin-only. The canonical rename home.","nullable":true,"maxLength":200,"example":"Weekend plans"}},"required":["actor_id"]},"LeaveConversationDto":{"type":"object","properties":{}},"DirectMessageAttachmentDto":{"type":"object","properties":{"kind":{"type":"string","description":"Attachment kind discriminator.","example":"image"},"url":{"type":"string","description":"Attachment URL.","example":"https://cdn.example/attachments/abc.png"}},"required":["kind","url"]},"SendMessageDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"The sender — social actor UUID. Standardized name (task 008 / G11); the persisted `sender_id` is set from it.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"body":{"type":"string","description":"Message body. Required when `kind=\"text\"`. 1..10000 chars after trim.","maxLength":10000,"example":"Hey!"},"kind":{"type":"string","enum":["text","image","video","audio","system"],"example":"text"},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/DirectMessageAttachmentDto"}},"assets":{"description":"Ordered image attachments (task 019). Each references a `ready` asset in this community owned by the sender. Max 20.","type":"array","items":{"$ref":"#/components/schemas/AssetAttachmentInputDto"}},"reply_to_id":{"type":"string","description":"Threaded inline reply target.","nullable":true}},"required":["actor_id"]},"DirectMessageResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Message UUID.","example":"mmmmmmmm-0000-0000-0000-000000000001"},"conversation_id":{"type":"string","description":"Conversation UUID.","example":"11111111-2222-3333-4444-555555555555"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"sender_id":{"type":"string","description":"Sender actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"kind":{"type":"string","enum":["text","image","video","audio","system"],"example":"text"},"body":{"type":"string","description":"Message body. Returns `\"(deleted)\"` when soft-deleted.","example":"Hey there."},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/DirectMessageAttachmentDto"}},"assets":{"description":"Ordered image attachments (task 019). Ref-only; resolve renditions via GET /assets/:id.","type":"array","items":{"$ref":"#/components/schemas/AssetAttachmentDto"}},"reply_to_id":{"type":"string","description":"Threaded inline reply target.","example":null,"nullable":true},"edited_at":{"type":"string","description":"ISO timestamp of the last edit; `null` for never-edited.","example":null,"nullable":true},"deleted_at":{"type":"string","description":"ISO timestamp when soft-deleted; `null` until then.","example":null,"nullable":true},"reaction_counts":{"type":"object","description":"Map of reaction type to count, denormalised on the message row in lock-step with the DM-reaction writes.","example":{"like":1}},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","conversation_id","community_id","sender_id","kind","body","attachments","assets","reply_to_id","edited_at","deleted_at","reaction_counts","created_at"]},"DirectMessageListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DirectMessageResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UpdateMessageDto":{"type":"object","properties":{"body":{"type":"string","description":"Updated body.","minLength":1,"maxLength":10000,"example":"edited"},"actor_id":{"type":"string","description":"The sender — must match the message's sender_id. Standardized name (task 008 / G11).","example":"aaaaaaaa-0000-0000-0000-000000000001"}},"required":["body","actor_id"]},"DeleteMessageDto":{"type":"object","properties":{}},"MarkConversationReadDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"The reader — social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"read_through":{"type":"string","description":"ISO timestamp. Messages with `created_at <= read_through` count as read. Omit to mark every message up to now().","example":"2026-05-01T12:00:00.000Z"}},"required":["actor_id"]},"ParticipantSummaryDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Participant actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"joined_at":{"type":"string","description":"ISO timestamp the actor joined.","example":"2026-05-01T12:00:00.000Z"},"last_read_at":{"type":"string","description":"ISO timestamp of the participant's last read marker.","example":null,"nullable":true},"muted":{"type":"boolean","description":"When true, dm_new_message notifications are suppressed for this participant.","example":false}},"required":["actor_id","joined_at","last_read_at","muted"]},"ConversationAdminResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Conversation UUID.","example":"11111111-2222-3333-4444-555555555555"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"kind":{"type":"string","enum":["direct","group"],"example":"direct"},"participants":{"type":"array","items":{"$ref":"#/components/schemas/ParticipantSummaryDto"}},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"last_message_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","kind","participants","created_at","updated_at","last_message_at"]},"ConversationAdminListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ConversationAdminResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"DirectMessageAdminResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Message UUID.","example":"mmmmmmmm-0000-0000-0000-000000000001"},"conversation_id":{"type":"string","description":"Conversation UUID.","example":"11111111-2222-3333-4444-555555555555"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"sender_id":{"type":"string","description":"Sender actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"kind":{"type":"string","enum":["text","image","video","audio","system"],"example":"text"},"body":{"type":"string","description":"Message body. Returns `\"(deleted)\"` when soft-deleted.","example":"Hey there."},"attachments":{"description":"Attachments.","example":[],"type":"array","items":{"type":"string"}},"assets":{"description":"Ordered image attachments (task 019); ref-only.","example":[],"type":"array","items":{"type":"string"}},"reply_to_id":{"type":"string","description":"Threaded inline reply target.","example":null,"nullable":true},"edited_at":{"type":"string","description":"ISO timestamp of the last edit; `null` for never-edited.","example":null,"nullable":true},"deleted_at":{"type":"string","description":"ISO timestamp when soft-deleted; `null` until then.","example":null,"nullable":true},"reaction_counts":{"type":"object","description":"Map of reaction type to count, denormalised on the message row in lock-step with the DM-reaction writes.","example":{"like":1}},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","conversation_id","community_id","sender_id","kind","body","attachments","assets","reply_to_id","edited_at","deleted_at","reaction_counts","created_at"]},"DirectMessageAdminListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DirectMessageAdminResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateDirectMessageReactionDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"The reactor — social actor UUID. Standardized name (task 008 / G11).","example":"aaaaaaaa-0000-0000-0000-000000000001"},"type":{"type":"string","description":"Reaction type (emoji or label, 1..64 chars).","minLength":1,"maxLength":64,"example":"heart"}},"required":["actor_id","type"]},"DirectMessageReactionResponseDto":{"type":"object","properties":{"message_id":{"type":"string","description":"Reacted message UUID.","example":"mmmmmmmm-0000-0000-0000-000000000001"},"actor_id":{"type":"string","description":"Reactor actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"type":{"type":"string","description":"Reaction type (emoji or label, 1..64 chars).","example":"heart"},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["message_id","actor_id","community_id","type","created_at"]},"DirectMessageReactionCreatedResponseDto":{"type":"object","properties":{"reaction":{"$ref":"#/components/schemas/DirectMessageReactionResponseDto"},"created":{"type":"boolean","description":"True when this call inserted the row; false on idempotent retry.","example":true},"reaction_counts":{"type":"object","description":"Denormalised map of reaction type → count after the change.","example":{"heart":3,"laugh":1}}},"required":["reaction","created","reaction_counts"]},"DirectMessageReactionRemovedResponseDto":{"type":"object","properties":{"reaction_counts":{"type":"object","description":"Denormalised map of reaction type → count after the change.","example":{"heart":2}}},"required":["reaction_counts"]},"DirectMessageReactionListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DirectMessageReactionResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"NotificationResponse":{"type":"object","properties":{"id":{"type":"string","description":"Notification UUID.","example":"11111111-1111-1111-1111-111111111111"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"recipient_actor_id":{"type":"string","description":"Actor whose inbox this row lives in.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"kind":{"type":"string","description":"What happened. Content kinds (`follow`, `reaction`, `comment_on_post`, `reply_to_comment`, `mention`, `vote`, `repost`, `quote_post`) plus the DM kinds (`dm_new_message`, `dm_added_to_group`, `dm_role_changed`, `dm_reaction`).","enum":["follow","reaction","comment_on_post","reply_to_comment","mention","vote","repost","quote_post","dm_new_message","dm_added_to_group","dm_role_changed","dm_reaction","tag","follow_request","follow_request_approved"],"example":"reaction"},"actor_id":{"type":"string","description":"Actor who triggered the event. Never equals `recipient_actor_id`.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"target_kind":{"type":"string","description":"Switches what `target_id` references. `actor` for follow; `post` for vote / repost / quote_post / comment_on_post and post-targeted reactions/mentions; `comment` for reply_to_comment and comment-targeted reactions/mentions; `direct_message` for the dm_* kinds.","enum":["actor","post","comment","direct_message"],"example":"post"},"target_id":{"type":"string","description":"id of the actor / post / comment the event happened on.","example":"bbbbbbbb-0000-0000-0000-000000000001"},"payload":{"type":"object","description":"Kind-specific extras. `reaction`: `{type}` (+ `{post_id}` when the target is a comment). `comment_on_post` / `reply_to_comment`: `{comment_id}`. `mention`: `{post_id}` (+ `{comment_id}` for comment mentions).","example":{"type":"like"}},"suppressed":{"type":"boolean","description":"When `true`, the recipient has restricted the triggering actor — the row is hidden from the default `inbox` slice and surfaces on `?status=requests` (or `all`).","example":false},"read_at":{"type":"string","description":"ISO timestamp; null while the row is unread.","example":null,"nullable":true},"created_at":{"type":"string","description":"ISO timestamp the row was inserted.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","recipient_actor_id","kind","actor_id","target_kind","target_id","payload","suppressed","read_at","created_at"]},"NotificationListResponse":{"type":"object","properties":{"data":{"description":"Page of notifications, newest first.","type":"array","items":{"$ref":"#/components/schemas/NotificationResponse"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UnreadCountResponse":{"type":"object","properties":{"count":{"type":"number","description":"Number of unread notifications for the actor.","example":7}},"required":["count"]},"UpdateNotificationDto":{"type":"object","properties":{"read":{"type":"boolean","description":"`true` to mark read, `false` to mark unread.","example":true}},"required":["read"]},"MarkAllReadDto":{"type":"object","properties":{"before":{"type":"string","description":"Optional ISO timestamp; only flips rows older than this. Omit to flip all unread.","example":"2026-05-01T00:00:00.000Z"}}},"MarkAllReadResponse":{"type":"object","properties":{"updated":{"type":"number","description":"Number of rows whose `read_at` flipped from null to now().","example":12}},"required":["updated"]},"NotificationPrefResponseDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"kind":{"type":"string","description":"NotificationKind this row applies to.","enum":["follow","reaction","comment_on_post","reply_to_comment","mention","vote","repost","quote_post","dm_new_message","dm_added_to_group","dm_role_changed","dm_reaction","tag","follow_request","follow_request_approved"],"example":"follow"},"enabled":{"type":"boolean","description":"When `false`, the producer silently skips notifications of this kind for this actor. Default `true`.","example":true},"updated_at":{"type":"string","description":"ISO timestamp of the last update. Synthesised entries (no explicit row) report epoch.","example":"2026-05-01T12:00:00.000Z"}},"required":["actor_id","kind","enabled","updated_at"]},"NotificationPrefListResponseDto":{"type":"object","properties":{"data":{"description":"One row per known NotificationKind. Kinds the actor has not explicitly toggled report `enabled=true` and an epoch `updated_at`, so the client always sees the full catalogue with effective state.","type":"array","items":{"$ref":"#/components/schemas/NotificationPrefResponseDto"}}},"required":["data"]},"UpdateNotificationPrefDto":{"type":"object","properties":{"enabled":{"type":"boolean","description":"`true` to receive notifications of this kind, `false` to mute. Default behaviour (no row) is `true`.","example":false}},"required":["enabled"]},"SuggestedFollowResponseDto":{"type":"object","properties":{"actor":{"$ref":"#/components/schemas/ActorResponseDto"},"score":{"type":"number","description":"For `followed_by_friends`: number of the requester's followees who follow this candidate. For `top_in_community`: the candidate's `follower_count`.","example":4},"reason":{"type":"string","description":"Why this candidate surfaced. `followed_by_friends` is the friend-of-a-friend signal; `top_in_community` is the cold-start fallback used when FoF doesn't fill the requested limit.","enum":["followed_by_friends","top_in_community"],"example":"followed_by_friends"}},"required":["actor","score","reason"]},"SuggestedFollowsListResponseDto":{"type":"object","properties":{"data":{"description":"Ordered candidates. FoF candidates are returned first (highest count first); cold-start fallbacks fill any remaining slots up to the requested limit.","type":"array","items":{"$ref":"#/components/schemas/SuggestedFollowResponseDto"}},"pagination":{"nullable":true,"description":"Always `null` — suggestions are a single bounded, scored page (no cursor in v1). Present for envelope consistency (task 008 / G13).","allOf":[{"$ref":"#/components/schemas/PaginationResponseDto"}]}},"required":["data","pagination"]},"CreateBookmarkDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Social actor UUID who is bookmarking — the customer-backend supplies the end-user identity here.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"collection_ids":{"description":"Optional collection UUIDs (task 020) to file this post into alongside the flat bookmark. Only the actor's own collections are written; unknown/other-owner ids are silently skipped. Max 20.","example":["99999999-0000-0000-0000-000000000001"],"type":"array","items":{"type":"string"}}},"required":["actor_id"]},"BookmarkResponseDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Social actor UUID who saved the post.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"post_id":{"type":"string","description":"Saved post UUID.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"created_at":{"type":"string","description":"ISO timestamp the bookmark was created.","example":"2026-05-01T12:00:00.000Z"}},"required":["actor_id","post_id","community_id","created_at"]},"BookmarkToggleResponseDto":{"type":"object","properties":{"bookmark":{"$ref":"#/components/schemas/BookmarkResponseDto"},"created":{"type":"boolean","description":"`true` on first call (row inserted); `false` on idempotent retry of an existing bookmark.","example":true}},"required":["bookmark","created"]},"BookmarkListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BookmarkResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateCollectionDto":{"type":"object","properties":{"owner_actor_id":{"type":"string","description":"The owning actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"name":{"type":"string","description":"Collection name (1-200 chars).","example":"Travel"},"kind":{"type":"string","enum":["bookmark","highlight"],"description":"Default `bookmark` (owner-scoped). `highlight` is profile-public and bypasses story expiry."},"cover_asset_id":{"type":"string","description":"Cover image — a ready asset (task 019) owned by the collection owner.","format":"uuid"},"cover_url":{"type":"string","description":"Cover image URL (alternative to cover_asset_id).","maxLength":2048},"visibility":{"type":"string","enum":["public","private"],"description":"Default `private`."},"position":{"type":"number","description":"Owner-defined ordering position.","minimum":0}},"required":["owner_actor_id","name"]},"CollectionResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"99999999-0000-0000-0000-000000000001"},"community_id":{"type":"string","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"owner_actor_id":{"type":"string","example":"aaaaaaaa-0000-0000-0000-000000000001"},"name":{"type":"string","example":"Travel"},"kind":{"type":"string","enum":["bookmark","highlight"],"example":"bookmark"},"cover_asset_id":{"type":"string","nullable":true,"example":null},"cover_url":{"type":"string","nullable":true,"example":null},"visibility":{"type":"string","enum":["public","private"],"example":"private"},"position":{"type":"number","description":"Owner-defined ordering position.","example":0},"item_count":{"type":"number","description":"Number of posts in the collection.","example":12},"created_at":{"type":"string","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","owner_actor_id","name","kind","cover_asset_id","cover_url","visibility","position","item_count","created_at","updated_at"]},"CollectionListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CollectionResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"UpdateCollectionDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","description":"The caller — must be the owner.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"name":{"type":"string","maxLength":200},"cover_asset_id":{"type":"string","format":"uuid","nullable":true},"cover_url":{"type":"string","maxLength":2048,"nullable":true},"visibility":{"type":"string","enum":["public","private"]},"position":{"type":"number","minimum":0}},"required":["caller_actor_id"]},"DeleteWithCallerDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","example":"aaaaaaaa-0000-0000-0000-000000000001"}},"required":["caller_actor_id"]},"AddItemDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","description":"The caller — must be the owner.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"post_id":{"type":"string","description":"The post to add.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"position":{"type":"number","description":"Curator ordering position.","minimum":0}},"required":["caller_actor_id","post_id"]},"CollectionItemResponseDto":{"type":"object","properties":{"collection_id":{"type":"string","example":"99999999-0000-0000-0000-000000000001"},"post_id":{"type":"string","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"community_id":{"type":"string","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"position":{"type":"number","example":0},"added_at":{"type":"string","example":"2026-05-01T12:00:00.000Z"}},"required":["collection_id","post_id","community_id","position","added_at"]},"ReorderItemDto":{"type":"object","properties":{"caller_actor_id":{"type":"string","example":"aaaaaaaa-0000-0000-0000-000000000001"},"position":{"type":"number","description":"New curator position.","minimum":0,"example":3}},"required":["caller_actor_id","position"]},"CollectionItemListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CollectionItemResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"MentionEntryResponseDto":{"type":"object","properties":{"source":{"type":"string","description":"Where the mention happened.","enum":["post","comment"],"example":"post"},"source_id":{"type":"string","description":"UUID of the source row — post id when source=post, comment id when source=comment.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"post_id":{"type":"string","description":"Parent post UUID. Always set, including for comment mentions, so the client can deep-link.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"mentioned_id":{"type":"string","description":"The actor that was mentioned.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"created_at":{"type":"string","description":"ISO timestamp the source row was created.","example":"2026-05-01T12:00:00.000Z"}},"required":["source","source_id","post_id","mentioned_id","created_at"]},"MentionListResponseDto":{"type":"object","properties":{"data":{"description":"Interleaved post + comment mentions, newest-first. Cursor is opaque base64url(JSON({created_at, source, source_id})).","type":"array","items":{"$ref":"#/components/schemas/MentionEntryResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"ActorSearchHitDto":{"type":"object","properties":{"actor":{"$ref":"#/components/schemas/ActorResponseDto"},"rank":{"type":"number","description":"PG `ts_rank` score on the (display_name A, external_id B, bio C) tsvector. Higher = more relevant.","example":0.6079}},"required":["actor","rank"]},"ActorSearchResponseDto":{"type":"object","properties":{"data":{"description":"Up to `limit` (default 20, max 50) ranked hits. Not paginated in v1; results are bounded by `limit`.","type":"array","items":{"$ref":"#/components/schemas/ActorSearchHitDto"}},"pagination":{"nullable":true,"description":"Always `null` — this lane returns a single bounded, ranked page (no cursor in v1). Present for envelope consistency.","allOf":[{"$ref":"#/components/schemas/PaginationResponseDto"}]}},"required":["data","pagination"]},"PostSearchHitDto":{"type":"object","properties":{"post":{"$ref":"#/components/schemas/PostResponseDto"},"rank":{"type":"number","description":"PG ts_rank score on the (title A, body B) tsvector. Higher = more relevant. Returned even when order='recent' so clients can render a relevance badge.","example":0.6079}},"required":["post","rank"]},"PostSearchResponseDto":{"type":"object","properties":{"data":{"description":"Up to `limit` (default 20, max 50) hits. Not paginated in v1.","type":"array","items":{"$ref":"#/components/schemas/PostSearchHitDto"}},"pagination":{"nullable":true,"description":"Always `null` — this lane returns a single bounded, ranked page (no cursor in v1). Present for envelope consistency.","allOf":[{"$ref":"#/components/schemas/PaginationResponseDto"}]}},"required":["data","pagination"]},"StoryTrayEntryResponse":{"type":"object","properties":{"actor_id":{"type":"string","description":"Author actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"actor_external_id":{"type":"string","description":"Author's external_id.","example":"alice"},"actor_display_name":{"type":"string","description":"Author display name.","example":"Alice","nullable":true},"actor_avatar_url":{"type":"string","description":"Author avatar URL.","example":"https://...","nullable":true},"latest_story_created_at":{"type":"string","description":"When the most recent unexpired-or-pinned story was created.","example":"2026-05-01T11:00:00.000Z"},"story_count":{"type":"number","description":"Total unexpired-or-pinned stories from this author.","example":3},"has_unviewed":{"type":"boolean","description":"True when the requester has not viewed the latest story.","example":true}},"required":["actor_id","actor_external_id","actor_display_name","actor_avatar_url","latest_story_created_at","story_count","has_unviewed"]},"StoryTrayResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StoryTrayEntryResponse"}},"pagination":{"nullable":true,"description":"Always `null` — the tray is a single bounded page (≤200 entries, no cursor in v1). The key is present for envelope consistency with cursor-paginated lists (task 008 / G13).","allOf":[{"$ref":"#/components/schemas/PaginationResponseDto"}]}},"required":["data","pagination"]},"HighlightsResponse":{"type":"object","properties":{"data":{"description":"Page of pinned posts (full post rows; expiry is bypassed for pinned rows).","type":"array","items":{"$ref":"#/components/schemas/PostResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"RecordViewDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Viewer social actor UUID. Standardized name (task 008 / G11).","example":"aaaaaaaa-0000-0000-0000-000000000002"}},"required":["actor_id"]},"StoryViewerResponse":{"type":"object","properties":{"viewer_actor_id":{"type":"string","description":"Viewer actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"viewer_external_id":{"type":"string","description":"Viewer's external_id.","example":"bob"},"viewer_display_name":{"type":"string","description":"Viewer display name.","example":"Bob","nullable":true},"viewer_avatar_url":{"type":"string","description":"Viewer avatar URL.","example":"https://...","nullable":true},"viewed_at":{"type":"string","description":"When the view was recorded.","example":"2026-05-01T11:30:00.000Z"}},"required":["viewer_actor_id","viewer_external_id","viewer_display_name","viewer_avatar_url","viewed_at"]},"StoryViewersResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StoryViewerResponse"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateCloseFriendDto":{"type":"object","properties":{"owner_actor_id":{"type":"string","description":"Owner social actor UUID (the actor whose close-friends list this is).","example":"aaaaaaaa-0000-0000-0000-000000000001"},"member_actor_id":{"type":"string","description":"Member social actor UUID (the actor being added).","example":"aaaaaaaa-0000-0000-0000-000000000002"}},"required":["owner_actor_id","member_actor_id"]},"CloseFriendResponse":{"type":"object","properties":{"owner_actor_id":{"type":"string","description":"Owner actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"member_actor_id":{"type":"string","description":"Member actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"member_external_id":{"type":"string","description":"Member's external_id.","example":"bob"},"member_display_name":{"type":"string","description":"Member display name.","example":"Bob","nullable":true},"member_avatar_url":{"type":"string","description":"Member avatar URL.","example":"https://...","nullable":true},"created_at":{"type":"string","description":"When the close-friend edge was created.","example":"2026-05-01T11:00:00.000Z"}},"required":["owner_actor_id","member_actor_id","member_external_id","member_display_name","member_avatar_url","created_at"]},"CloseFriendsListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CloseFriendResponse"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CastVoteDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Voter social actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"option_index":{"type":"number","description":"Zero-based index into post.attributes.poll.options.","example":1}},"required":["actor_id","option_index"]},"PollResultOptionResponse":{"type":"object","properties":{"index":{"type":"number","description":"Zero-based index into the original options list.","example":0},"label":{"type":"string","description":"Option label as supplied at post creation.","example":"VS Code"},"count":{"type":"number","description":"Number of votes recorded for this option.","example":42}},"required":["index","label","count"]},"PollResultsResponse":{"type":"object","properties":{"post_id":{"type":"string","description":"Post UUID the poll lives on.","example":"bbbbbbbb-0000-0000-0000-000000000001"},"question":{"type":"string","description":"Poll question, copied from post.attributes.poll.question.","example":"Best dev tool?"},"options":{"type":"array","items":{"$ref":"#/components/schemas/PollResultOptionResponse"}},"total_votes":{"type":"number","description":"Sum of counts across every option.","example":67}},"required":["post_id","question","options","total_votes"]},"PollVoteResponse":{"type":"object","properties":{"option_index":{"type":"number","description":"Zero-based index of the option the actor voted for.","example":1},"created_at":{"type":"string","description":"When the vote was cast (or last changed).","example":"2026-05-01T11:30:00.000Z"}},"required":["option_index","created_at"]},"PollVoterResponse":{"type":"object","properties":{"voter_actor_id":{"type":"string","description":"Voter actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000002"},"voter_external_id":{"type":"string","description":"Voter's external_id.","example":"bob"},"voter_display_name":{"type":"string","description":"Voter display name.","example":"Bob","nullable":true},"voter_avatar_url":{"type":"string","description":"Voter avatar URL.","example":"https://...","nullable":true},"option_index":{"type":"number","description":"The option this voter chose.","example":1},"voted_at":{"type":"string","description":"When the vote was cast.","example":"2026-05-01T11:30:00.000Z"}},"required":["voter_actor_id","voter_external_id","voter_display_name","voter_avatar_url","option_index","voted_at"]},"PollVotersResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PollVoterResponse"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"CreateReactionDto":{"type":"object","properties":{"actor_id":{"type":"string","description":"Reactor — social actor UUID. Standardized name (task 008 / G11).","example":"aaaaaaaa-0000-0000-0000-000000000001"},"type":{"type":"string","description":"Reaction type (1..64 chars).","minLength":1,"maxLength":64,"example":"like"}},"required":["actor_id","type"]},"ReactionEdgeResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Edge UUID.","example":"eeeeeeee-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"kind":{"type":"string","description":"Edge kind (`reaction:<type>`).","example":"reaction:like"},"src_actor_id":{"type":"string","description":"Reactor actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"dst_actor_id":{"type":"string","description":"Destination actor UUID — always `null` for post reactions (the target is `dst_post_id`).","example":null,"nullable":true},"dst_post_id":{"type":"string","description":"Target post UUID.","example":"pppppppp-pppp-pppp-pppp-pppppppppppp"},"payload":{"type":"object","description":"Free-form payload.","example":{}},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","kind","src_actor_id","dst_actor_id","dst_post_id","payload","created_at"]},"ReactionResponseDto":{"type":"object","properties":{"edge":{"$ref":"#/components/schemas/ReactionEdgeResponseDto"},"created":{"type":"boolean","description":"`true` on first call; `false` on idempotent retry.","example":true},"reaction_counts":{"type":"object","description":"Denormalised reaction counts after the write.","example":{"like":13,"fire":3}}},"required":["edge","created","reaction_counts"]},"ReactionCountsResponseDto":{"type":"object","properties":{"reaction_counts":{"type":"object","description":"Denormalised reaction counts after the write.","example":{"like":12}}},"required":["reaction_counts"]},"PostReactionEntryDto":{"type":"object","properties":{"post_id":{"type":"string","description":"Post UUID.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"actor_id":{"type":"string","description":"Reactor actor UUID.","example":"aaaaaaaa-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"type":{"type":"string","description":"Reaction type label.","example":"like"},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["post_id","actor_id","community_id","type","created_at"]},"PostReactionListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PostReactionEntryDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"PostStateRequestDto":{"type":"object","properties":{"post_ids":{"description":"UUIDs of the posts to hydrate viewer state for. Capped at 200 (matches the batch-views precedent); ids that are unknown, removed, or from another community resolve to the empty state instead of failing the batch.","example":["bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","cccccccc-cccc-cccc-cccc-cccccccccccc"],"type":"array","items":{"type":"string"}}},"required":["post_ids"]},"PostViewerStateEntryDto":{"type":"object","properties":{"post_id":{"type":"string","description":"Post UUID this entry describes.","example":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},"reacted_types":{"description":"Reaction types the viewer currently has on the post, sorted ascending. Empty when the viewer has not reacted.","example":["fire","like"],"type":"array","items":{"type":"string"}},"bookmarked":{"type":"boolean","description":"The viewer has bookmarked the post.","example":true},"reposted":{"type":"boolean","description":"The viewer has a currently-published repost of the post.","example":false},"voted_option":{"type":"number","description":"Poll option index the viewer voted for; `null` when the viewer has not voted (or the post has no poll).","example":null,"nullable":true}},"required":["post_id","reacted_types","bookmarked","reposted","voted_option"]},"PostStateResponseDto":{"type":"object","properties":{"data":{"description":"One entry per requested post id (de-duplicated, request order). Unknown / cross-community ids resolve to the empty state.","type":"array","items":{"$ref":"#/components/schemas/PostViewerStateEntryDto"}}},"required":["data"]},"HashtagPostListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PostResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"WebhookResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Webhook UUID.","example":"wwwwwwww-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"url":{"type":"string","description":"Subscriber URL.","example":"https://example.com/webhooks/social"},"event_types":{"type":"array","description":"Subscribed event types. Each item is one of the social event names — see GET /v1/webhook-event-types for the live catalog.","example":["post.created","comment.created"],"items":{"type":"string","enum":["post.created","post.updated","post.deleted","post.reaction.created","repost.created","comment.created","comment.updated","comment.deleted","comment.reaction.created","edge.follow.created","edge.follow.deleted","edge.follow.requested","edge.follow.request_approved","flag.created","flag.updated","moderation.action.applied","actor.updated","dm.message.created","tag.created","asset.ready","asset.failed"]}},"signing_secret_hint":{"type":"string","description":"Last 4 chars of the signing secret. Use the full secret returned by create / rotate to verify the X-Pcft-Signature header.","example":"abcd"},"disabled_at":{"type":"string","description":"ISO timestamp when the webhook was auto-disabled (or null if active).","example":null,"nullable":true},"disabled_reason":{"type":"string","description":"Reason the dispatcher auto-disabled the row.","example":null,"nullable":true},"consecutive_failures":{"type":"number","description":"Consecutive failed attempts since the last success.","example":0},"last_success_at":{"type":"string","description":"ISO timestamp of the most recent 2xx response.","example":null,"nullable":true},"last_failure_at":{"type":"string","description":"ISO timestamp of the most recent non-2xx response.","example":null,"nullable":true},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","community_id","url","event_types","signing_secret_hint","disabled_at","disabled_reason","consecutive_failures","last_success_at","last_failure_at","created_at","updated_at"]},"CreateWebhookDto":{"type":"object","properties":{"url":{"type":"string","description":"Subscriber URL. Must be `https://`.","example":"https://example.com/webhooks/social"},"event_types":{"type":"array","description":"Event types to subscribe to. Unknown values are rejected with a 400 naming the valid set — see GET /v1/webhook-event-types.","example":["post.created","comment.created"],"items":{"type":"string","enum":["post.created","post.updated","post.deleted","post.reaction.created","repost.created","comment.created","comment.updated","comment.deleted","comment.reaction.created","edge.follow.created","edge.follow.deleted","edge.follow.requested","edge.follow.request_approved","flag.created","flag.updated","moderation.action.applied","actor.updated","dm.message.created","tag.created","asset.ready","asset.failed"]}}},"required":["url","event_types"]},"CreatedWebhookResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Webhook UUID.","example":"wwwwwwww-0000-0000-0000-000000000001"},"community_id":{"type":"string","description":"Owning community UUID.","example":"cccccccc-cccc-cccc-cccc-cccccccccccc"},"url":{"type":"string","description":"Subscriber URL.","example":"https://example.com/webhooks/social"},"event_types":{"type":"array","description":"Subscribed event types. Each item is one of the social event names — see GET /v1/webhook-event-types for the live catalog.","example":["post.created","comment.created"],"items":{"type":"string","enum":["post.created","post.updated","post.deleted","post.reaction.created","repost.created","comment.created","comment.updated","comment.deleted","comment.reaction.created","edge.follow.created","edge.follow.deleted","edge.follow.requested","edge.follow.request_approved","flag.created","flag.updated","moderation.action.applied","actor.updated","dm.message.created","tag.created","asset.ready","asset.failed"]}},"signing_secret_hint":{"type":"string","description":"Last 4 chars of the signing secret. Use the full secret returned by create / rotate to verify the X-Pcft-Signature header.","example":"abcd"},"disabled_at":{"type":"string","description":"ISO timestamp when the webhook was auto-disabled (or null if active).","example":null,"nullable":true},"disabled_reason":{"type":"string","description":"Reason the dispatcher auto-disabled the row.","example":null,"nullable":true},"consecutive_failures":{"type":"number","description":"Consecutive failed attempts since the last success.","example":0},"last_success_at":{"type":"string","description":"ISO timestamp of the most recent 2xx response.","example":null,"nullable":true},"last_failure_at":{"type":"string","description":"ISO timestamp of the most recent non-2xx response.","example":null,"nullable":true},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"signing_secret":{"type":"string","description":"Full plaintext HMAC signing secret. Returned ONCE — store it; subsequent reads only expose the last-4 hint.","example":"abc...xyz"}},"required":["id","community_id","url","event_types","signing_secret_hint","disabled_at","disabled_reason","consecutive_failures","last_success_at","last_failure_at","created_at","updated_at","signing_secret"]},"WebhookListResponseDto":{"type":"object","properties":{"data":{"description":"Every webhook on the community — disabled rows included, so auto-disabled subscribers stay discoverable.","type":"array","items":{"$ref":"#/components/schemas/WebhookResponseDto"}}},"required":["data"]},"UpdateWebhookDto":{"type":"object","properties":{"url":{"type":"string","description":"New subscriber URL."},"event_types":{"type":"array","description":"New subscription set. Unknown values are rejected with a 400 naming the valid set.","items":{"type":"string","enum":["post.created","post.updated","post.deleted","post.reaction.created","repost.created","comment.created","comment.updated","comment.deleted","comment.reaction.created","edge.follow.created","edge.follow.deleted","edge.follow.requested","edge.follow.request_approved","flag.created","flag.updated","moderation.action.applied","actor.updated","dm.message.created","tag.created","asset.ready","asset.failed"]}},"disabled":{"type":"boolean","description":"Set `false` to re-enable a disabled webhook (clears the auto-disable state and resets the failure streak — no secret rotation required). Set `true` to disable it manually.","example":false}}},"DeliveryJobResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Delivery job UUID."},"webhook_id":{"type":"string","description":"Webhook UUID the job targets."},"event_id":{"type":"string","description":"Stable event id (carried in the request body as `id`; stable across retries and redelivers).","example":"evt_…"},"event_type":{"type":"string","description":"Event type. `ping` for test deliveries.","example":"post.created"},"status":{"type":"string","description":"Job state.","enum":["pending","delivering","succeeded","exhausted","canceled"],"example":"pending"},"attempt_count":{"type":"number","description":"Delivery attempts made so far.","example":0},"next_attempt_at":{"type":"string","description":"ISO timestamp of the next scheduled attempt (backoff: 1m, 5m, 30m, 2h, 6h, 24h).","example":"2026-05-01T12:00:00.000Z"},"last_error":{"type":"string","description":"Error from the most recent failed attempt. Null before the first failure.","example":null,"nullable":true},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"},"updated_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","webhook_id","event_id","event_type","status","attempt_count","next_attempt_at","last_error","created_at","updated_at"]},"DeliveryResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Delivery row UUID."},"webhook_id":{"type":"string","description":"Webhook UUID."},"event_id":{"type":"string","description":"Stable event id (carried in the request body as `id`).","example":"evt_…"},"event_type":{"type":"string","description":"Event type for this attempt.","example":"post.created"},"attempt_number":{"type":"number","description":"Attempt number, 1-based. Increments per retry along the backoff schedule (1m, 5m, 30m, 2h, 6h, 24h — 7 attempts total).","example":1},"request_body":{"type":"object","description":"Signed request body that was sent.","example":{}},"response_status":{"type":"number","description":"HTTP status code (null on transport / SSRF failure).","example":200,"nullable":true},"response_body":{"type":"string","description":"Truncated response body (≤1024 bytes).","example":"OK","nullable":true},"error_message":{"type":"string","description":"Free-form error string. Null on success.","example":null,"nullable":true},"latency_ms":{"type":"number","description":"Total round-trip time in ms. Null on transport-level failure.","example":67,"nullable":true},"succeeded":{"type":"boolean","description":"Whether the delivery returned 2xx.","example":true},"created_at":{"type":"string","description":"ISO timestamp.","example":"2026-05-01T12:00:00.000Z"}},"required":["id","webhook_id","event_id","event_type","attempt_number","request_body","response_status","response_body","error_message","latency_ms","succeeded","created_at"]},"DeliveryListResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"WebhookEventTypesResponseDto":{"type":"object","properties":{"data":{"type":"array","description":"Every event type a webhook can subscribe to, in catalog order.","example":["post.created","post.deleted","comment.created"],"items":{"type":"string","enum":["post.created","post.updated","post.deleted","post.reaction.created","repost.created","comment.created","comment.updated","comment.deleted","comment.reaction.created","edge.follow.created","edge.follow.deleted","edge.follow.requested","edge.follow.request_approved","flag.created","flag.updated","moderation.action.applied","actor.updated","dm.message.created","tag.created","asset.ready","asset.failed"]}}},"required":["data"]},"ActorCommentsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CommentResponseDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]},"ActorReactionEntryDto":{"type":"object","properties":{"target_type":{"type":"string","description":"What the reaction is on.","enum":["post","comment"],"example":"post"},"target_id":{"type":"string","description":"UUID of the reacted-to post or comment (per `target_type`).","example":"pppppppp-pppp-pppp-pppp-pppppppppppp"},"type":{"type":"string","description":"Reaction type (e.g. `like`).","example":"like"},"created_at":{"type":"string","description":"ISO timestamp the reaction was recorded.","example":"2026-05-01T12:00:00.000Z"}},"required":["target_type","target_id","type","created_at"]},"ActorReactionsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ActorReactionEntryDto"}},"pagination":{"$ref":"#/components/schemas/PaginationResponseDto"}},"required":["data","pagination"]}}}}