Skip to content

PostHog

RoutingCat works beside PostHog in two ways. It puts the visitor id and the variants each page shows on your PostHog events, so your PostHog data splits by variant. And it can take a conversion your site already captures in PostHog, so you never track it twice.

Put the variants on PostHog’s events

The properties are rc_vid, the visitor id, and one rc_<page>_<slot> per slot the page shows, holding its variant’s key, such as rc_home_hero set to learns.

The super properties need no setup. The tracker that RoutingCat’s head partial loads registers them with your PostHog as super properties, once each page has shown. It finds PostHog at window.posthog, where PostHog’s HTML snippet puts it. A site that loads posthog-js from npm assigns its instance to window.posthog in the browser code that initializes it, so that the tracker finds it.

One line in PostHog’s init adds them to events captured before the page has shown. It names the tracker’s hook, routingcat.routingcatProperties, which puts the properties on each event as it leaves, from the visitor id and the variants the page shows at that moment. The tracker loads defer, after PostHog’s snippet has run init, and a blocker may stop it, so the line calls the hook only once window.routingcat exists. Here it is on the example site’s home page, whose <head> loads PostHog’s snippet after the head partial:

public/index.html
<script>
…
posthog.init('phc_routingcat_static_example', {
api_host: 'https://us.i.posthog.com',
defaults: '2026-08-30',
// RoutingCat's visitor id and each slot's variant on every event, once its tracker has
// loaded: the tracker runs after this, deferred, and an ad blocker may stop it.
before_send: (event) => (window.routingcat ? routingcat.routingcatProperties(event) : event),
})
</script>
  • Events captured before the tracker has loaded go without the properties, and a page view’s first $pageview can be one of them, since PostHog may capture it first. Every event after the page has shown carries them, from the super properties.
  • Keep the check for window.routingcat. Written as before_send: routingcat.routingcatProperties, the line would fail in init, before the tracker exists, and PostHog would never start. With the check, where a blocker stops the tracker, each event goes as it came, and PostHog runs as it would without RoutingCat.
  • An existing before_send: make it an array with RoutingCat’s line last. posthog-js runs the hooks in order.
  • On your own machine, where RoutingCat sets no visitor id, the hook adds nothing.

Take conversions from PostHog

When an outcome is an event your site already captures in PostHog, such as posthog.capture('waitlist_joined'), forward it to RoutingCat with a PostHog webhook destination rather than adding a call of your own: one capture, counted once. The destination:

  • matches that event alone, on your production hosts through its $host property, so events from development and previews stay out;
  • posts to https://ingest.routingcat.com/v1/events, with your production server events key, from npx routingcat keys server, as a bearer token;
  • sends the event under PostHog’s name for it, with PostHog’s uuid as its id, its rc_vid property as the visitor, and "source": "posthog".

Declare the outcome in routingcat.config.json under PostHog’s name for the event, or doctor reports an undeclared name.

Here is the destination as PostHog’s API takes it, for an example app’s contact_sales on its one production host. Change the event, the hosts and the key:

{
"type": "destination",
"template_id": "template-webhook",
"name": "RoutingCat: contact_sales",
"description": "Forwards contact_sales to RoutingCat's events endpoint, as RoutingCat's PostHog recipe sets it up.",
"enabled": true,
"filters": {
"source": "events",
"events": [{ "id": "contact_sales", "name": "contact_sales", "type": "events", "order": 0 }],
"properties": [
{
"key": "$host",
"value": ["routingcat-example.vercel.app"],
"operator": "exact",
"type": "event"
}
],
"filter_test_accounts": false
},
"inputs": {
"url": { "value": "https://ingest.routingcat.com/v1/events" },
"method": { "value": "POST" },
"headers": {
"value": { "Content-Type": "application/json", "Authorization": "Bearer sk_…" }
},
"body": {
"value": {
"name": "{event.event}",
"id": "{event.uuid}",
"visitor_id": "{event.properties.rc_vid}",
"occurred_at": "{event.timestamp}",
"source": "posthog"
}
},
"debug": { "value": false }
}
}

Create it whichever way your session allows:

  • Through PostHog’s API, with a personal API key holding hog_function:write on the project: post that body to https://us.posthog.com/api/projects/<project id>/hog_functions/, or eu.posthog.com on PostHog’s EU cloud.
  • Through PostHog’s MCP server, if your agent has it connected, with the same fields.
  • By hand, in about two minutes, as the steps below show.

To add it by hand, in your PostHog project:

  1. Open Data pipelines, choose New, then Destination, and create an “HTTP Webhook”.
  2. Name it, such as RoutingCat: waitlist_joined.
  3. Set the webhook URL to https://ingest.routingcat.com/v1/events, and the method to POST.
  4. Replace the JSON body with the body value above.
  5. Add the headers Content-Type: application/json, and Authorization: Bearer followed by the server events key.
  6. Filter on the event alone, and on the event property $host equal to your production hosts, with “Filter out internal and test users” off.
  7. Choose Create & enable. Skip the page’s test: it sends a sample event, which RoutingCat would record as real.

The destination takes the visitor from the event’s rc_vid: a conversion captured as the visitor acts on the page carries it, and one captured before its page has shown needs the before_send line. An event without it arrives with no key. PostHog delivers the event on its own terms: it retries a failing destination a few times before setting it aside, and posthog-js loses events to ad blockers unless you route PostHog through your own domain, as PostHog’s guide shows.

Know who can read the key

Anyone who can open your PostHog project’s destinations can read the server events key in the destination’s settings. That key only writes events and identity links for its site and environment, so a leak allows false conversions, never reads. npx routingcat keys server --rotate replaces it, and the old key keeps working for 24 hours, time to update the destination’s header. Access and keys covers every key.

Don’t use PostHog’s distinct_id as a visitor or account id: posthog-js writes it in the browser, and changes it at identify and reset.

Next