Webhooks.
Keep your backend in step with your communities without polling: every event is recorded the moment it happens, sent to your endpoint, signed, and retried for about 32 hours if your server is down.
You'll need a workspace with Social enabled and a workspace key (pcft_live_…) whose policy allows social.read and social.update. Delivery is at-least-once: dedupe on the event id (also sent as webhook-id).
1
One endpoint for the workspace, or one per community
The routes are identical; only the path decides which events an endpoint receives.
/v1/workspaces/:workspace_id/webhooks/…: the endpoint receives the events of every community in the workspace, including communities you create later. Use this when you run many communities and want one receiver./v1/communities/:community_id/webhooks/…: the endpoint receives that community's events only. The same endpoints are also at/v1/workspaces/:workspace_id/communities/:community_id/webhooks/…, which the console uses.
An event goes to each subscribed endpoint once, whichever scope it is in. Every body carries community_id and workspace_id, so a workspace endpoint can tell the communities apart. Up to 10 endpoints per workspace and 10 per community.
2
Add an endpoint
From the console (Social → Webhooks, or a community's Webhooks tab) or the API. Endpoints, signatures, tests, retries and replay work the same in every ProductCraft product.
curl -X POST "https://api.social.productcraft.co/v1/workspaces/<workspace_id>/webhooks/endpoints" \
-H "Authorization: Bearer pcft_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: social-hook-1" \
-d '{
"url": "https://hooks.acme.com/webhooks/social",
"event_types": ["post.created", "post.deleted", "comment.created"]
}'The answer carries the whsec_… signing secret exactly once, and its scope_type / scope_id say which scope it is in. Unknown event types are refused with a 400, so a typo cannot register silently. Verifying the signature, Send test, automatic retries, auto-disable, the event log, replay and secret rotation are in the Webhooks guide, with Node code you can paste.
3
What a Social event looks like
Signed with Standard Webhooks like every ProductCraft event; community_id and workspace_id are on every body.
{
"id": "evt_83cfc794-78e9-4102-96de-3a9a6f5bc31e",
"type": "post.created",
"created_at": "2026-10-17T10:00:00.000Z",
"community_id": "6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
"workspace_id": "24fab978-7cd7-48ae-bce1-aba876b11957",
"data": {
"community_id": "6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
"post_id": "2d3e4f50-6172-4c8d-ae9f-0a1b2c3d4e5f",
"actor_id": "0b1c2d3e-4f50-4a6b-8c7d-8e9f0a1b2c3d",
"body": "Shipping the new onboarding today.",
"status": "published",
...
}
}4
Event catalog
Twenty-one event types (live list with descriptions and examples: GET …/webhooks/event-types). New types never reach an endpoint that lists its events until you add them; an endpoint with an empty list gets them automatically.
| Event type | Fires when |
|---|---|
| post.created | A post (text / link / image / quote / story) was created. Includes drafts and scheduled posts — check data.status. |
| post.updated | A post body / title / attributes was edited. |
| post.deleted | A post was soft-deleted — including reposts deleted via either delete route. Use this to evict mirrored posts. |
| post.reaction.created | A reaction landed on a post. |
| repost.created | An actor reposted a post (data.source_post_id points at the original). |
| comment.created | A comment was created (data.parent_id null for top-level). |
| comment.updated | A comment body was edited. |
| comment.deleted | A comment was removed. |
| comment.reaction.created | A reaction landed on a comment. |
| edge.follow.created | An actor followed another actor. |
| edge.follow.deleted | An actor unfollowed another actor. |
| edge.follow.requested | An actor asked to follow a private actor — awaiting that actor’s approval. |
| edge.follow.request_approved | A private actor approved a pending follow request (the edge itself arrives separately as edge.follow.created). |
| flag.created | Content was reported to the moderation queue (data.auto_hidden tells you if it tripped the auto-hide threshold). |
| flag.updated | A flag was resolved (actioned / dismissed). |
| moderation.action.applied | A moderator applied an action (hide / remove / restore / warn / ban / dismiss) — audit-row granularity. |
| actor.updated | An actor profile changed (update, or upsert on an existing actor). |
| dm.message.created | A direct message was sent — the minimal DM push primitive (data.recipient_actor_ids lists everyone to notify). |
| tag.created | An actor was tagged in a post (data.tagged_actor_id). The tag lands pending until that actor approves it. |
| asset.ready | An uploaded asset finished processing and is attachable to a post. |
| asset.failed | An asset failed processing or storage (data.failure_reason). |
5
Set up before 17 October 2026?
Webhooks made through the older routes keep working until 15 January 2027.
Every webhook you registered with POST /v1/communities/:community_id/webhooks is now a community endpoint with the same id, URL, events and secret: you see it under …/webhooks/endpoints, and it receives the Standard Webhooks headers. Until 15 January 2027 it also still gets the old x-pcft-signature header; the Webhooks guide covers that header and moving your verifier. Secrets issued since already start with whsec_. The body is unchanged apart from the new workspace_id.
The old routes (…/webhooks, …/webhooks/:id with /test, /deliveries, /redeliver and /rotate-secret, and GET /v1/webhook-event-types) keep working until the same date and answer with Deprecation and Sunset headers. Two differences: a test now sends the example of the first event the webhook subscribes to (there is no ping type any more) and both test and redeliver report the outcome in their answer instead of running later.