Skip to main content

Send Candu data to PostHog

Use a Candu outgoing webhook and a PostHog workflow to capture Content Interactions and Form Responses as PostHog events.

Written by Jonathan Anderson

This guide connects Candu to PostHog so that every user event in your Candu content becomes a PostHog event, joined to the same person record as the rest of your product analytics.

No middleware, no Zapier or Make, and no code changes in your app. Candu posts to a PostHog webhook, and a PostHog workflow turns that into a captured event.

What you get

Every user event type flows through a single webhook. Button clicks, form submissions, rich text copies, and anything else Candu tracks:

PostHog field

Example

Event name

Finish Sign Up Flow Clicked

Person

16948 (your Candu userId)

Current URL

candu_topic

candu.sdk.content.interaction

candu_node_type

Button, Form, RichText

candu_form_data

{"Main Goal":"Feature adoption"}

PostHog Activity list showing several Candu events with Person and URL columns populated

Before you start: check your identity join

This is the one thing that will silently ruin the integration if you get it wrong.

Candu sends its own userId. PostHog identifies people by distinct_id. If those two values are not the same string for the same human, your Candu events will land against a separate, orphaned set of people, and none of it will join to your product analytics. There is no error message when this happens. It simply produces useless data.

Before building anything, confirm that the userId you pass to Candu is the same identifier you pass to posthog.identify(). If they differ, fix that first.

Step 1: Create the PostHog workflow

  1. In PostHog, go to Tools > Workflows and create a new workflow.

  2. Click the Trigger node and set Trigger type to Webhook.

  3. Copy the webhook URL shown under Usage instructions. You will need it in Step 4.

  4. Turn on Log payloads. This is invaluable while you are setting up.

Step 2: Map the Candu fields on the Trigger

Still on the Trigger node, set these three fields.

Event name

{request.body.payload.properties.eventName ?? request.body.payload.properties.name}

Distinct ID

{request.body.payload.userId}

Event properties

{
"$ip": "{request.ip}",
"$lib": "posthog-webhook",
"$current_url": "{request.body.payload.context.page.url}",
"candu_topic": "{request.body.payload.eventName}",
"candu_content_id": "{request.body.payload.properties.contentId}",
"candu_node_type": "{request.body.payload.properties.nodeType}",
"candu_trigger": "{request.body.payload.properties.trigger}",
"candu_placement_id": "{request.body.payload.properties.placementId}",
"candu_segment_id": "{request.body.payload.properties.segmentId}",
"candu_document_id": "{request.body.payload.properties.documentId}",
"candu_form_data": "{request.body.payload.properties.formData}",
"candu_anonymous_id": "{request.body.payload.anonymousId}",
"candu_properties": "{request.body.payload.properties}"
}

PostHog Trigger node showing the completed Event name, Distinct ID and Event properties, with the three node canvas visible on the left.

Two things to note about these mappings, both explained in Troubleshooting below:

  • Every path starts with request.body.payload. The payload level is easy to miss.

  • The ?? in Event name is doing real work. Different Candu event types store the readable name under different keys.

Step 3: Add the Capture event action

The Trigger alone does not write anything to PostHog. You need an action node.

  1. From the Build panel, drag Capture event (under PostHog actions) onto the line between Trigger and Exit.

  2. Leave its three fields on the defaults:

  • Event name: {event.event}

  • Distinct ID: {event.distinct_id}

  • Event properties: {event.properties}

Do not use request.* paths in this node. The Trigger has already turned the incoming request into an event; the action node reads that event. Using request.* here fails with Could not execute bytecode for input field: event.

PostHog Capture event node showing {event.event}, {event.distinct_id} and {event.properties}, with Trigger, Capture event and Exit all visible on the canvas.

Click Save, then Enable the workflow.

Step 4: Create the Candu webhook

  1. In Candu, go to Settings > Integrations > Webhooks and click Set up a new Webhook.

  2. Give it a name, for example "PostHog".

  3. Paste the PostHog webhook URL into Endpoint URL.

  4. Leave Format as Default (.json).

  5. Expand Content interactions and tick Enable all user events. This single toggle covers button clicks, form submissions, rich text copies and every other user event type.

  6. Click Save.

Step 5: Verify

  1. In your app, click a button or submit a form inside a piece of Candu content.

  2. Back in Candu, open Settings > Integrations > Webhooks and click View on your webhook. Under Recent Webhook Events you should see a teal dot. Click it and you should see 201 with {"status":"queued"}.

  3. A red dot means the delivery failed. Click it to read the response.

  4. In PostHog, go to Activity. Your event should appear within a minute or two, with the person and URL populated.

Candu Recent Webhook Events showing teal dots against several event types.

The webhook event detail modal showing a 201 response with status queued.

Troubleshooting

Every delivery returns 400 "event could not be parsed correctly"

This means your Event name mapping resolved to nothing. Almost always it is a wrong path, and almost always it is one of the two issues below.

The payload wrapper

Candu's actual HTTP body wraps everything in a payload key:

{
"payload": {
"userId": "16948",
"eventName": "candu.sdk.content.interaction",
"properties": { ... },
"context": { "page": { "url": "..." } }
}
}

Candu's own Triggering Event viewer displays the inner object, not the wire body. If you copy paths from that screen you will be exactly one level too shallow, every path will silently resolve to nothing, and every delivery will 400.

The example payload panel on the webhook setup form can also differ from what is actually sent, and differs between topics on the same screen. Trust neither. Use the technique below instead.

How to see the real payload

When a mapping is not resolving and you cannot tell why, capture the raw body:

  1. On the Trigger, temporarily set Event name to a literal string such as debug_probe and Distinct ID to debug_user, so delivery cannot fail.

  2. Set Event properties to {"raw_body": "{request.body}"}.

  3. Save, then resend an event from Candu.

  4. Open the resulting event in PostHog Activity and expand raw_body. That is the exact wire format.

  5. Write your real mappings against it, then remove the debug properties.

Do not leave raw header capture enabled in a live setup, as request headers can contain credentials.

Two fields called eventName

There are two, at different depths, and they mean different things:

Path

Value

Use for

payload.eventName

candu.sdk.content.interaction

The topic. Map to a property.

payload.properties.eventName

Finish Sign Up Flow Clicked

The event name you want.

Form submissions use a different key

Most event types store the readable name in properties.eventName. Form submissions store it in properties.name. Same purpose, different key. That is why the Event name mapping uses ?? to fall back, which lets one webhook and one workflow serve every event type.

The field autocomplete is misleading

In the Capture event node, autocomplete offers request and variables but does not list event. Ignore it. event is available and is what you want. PostHog's own pre-filled defaults are correct.

The Event properties box is strict JSON

Templates must be quoted. "candu_properties": {request.body.payload.properties} fails validation. "candu_properties": "{request.body.payload.properties}" works, and still arrives in PostHog as a real nested object rather than a string.

An alternative worth considering

If you can add a few lines to your app, calling posthog.capture() from Candu's client side callback gives you better data than the webhook route. Identity matches automatically because it is the same PostHog instance in the same browser session, and events link to session replays. The webhook approach documented here is the right choice when you cannot ship code.

Going the other way

PostHog can also send data into Candu: person properties, cohort membership, and activity become Candu traits you can use in segments and targeting. See Send PostHog data to Candu.

Did this answer your question?