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
Each event gets rc_vid, the visitor id, and one rc_<page>_<slot> property per slot the page shows, holding its variant’s key, such as rc_home_hero set to learns.
- The super properties need no setup. Once a page has shown, RoutingCat’s script registers them as super properties with the page’s
window.posthog, where PostHog’s HTML snippet puts it, for every later event. - posthog-js from npm works the same once your code that initializes it also makes its instance
window.posthog, since that is where RoutingCat finds PostHog. - A slot that shows no variant, or several, gets no property.
One line in PostHog’s init adds them to the events captured before the page has shown. Add before_send: window.routingcat.routingcatProperties to the options of PostHog’s init, in its snippet or in your own code. RoutingCat’s hook then puts the properties on every event as it leaves, from the visitor id and the variants the page shows at that moment.
- The head partial, first in
<head>, gives every page that carries itwindow.routingcatbefore PostHog’s snippet runs. On a page whose layout doesn’t include the partial, such as an admin area with its own layout,window.routingcatdoesn’t exist, so that page’s PostHoginittakes no hook. - An existing
before_send: make it an array withwindow.routingcat.routingcatPropertieslast. posthog-js runs the hooks in order. - A page view’s first
$pageviewcan go without the properties. The hook adds them once RoutingCat’s post part has loaded, after the page’s HTML has been read, and PostHog can capture that$pageviewbefore then, right afterinit. - In an app that renders in the browser, the
$pageviewof a route change gets the new page’s slots only once they show; it carriesrc_vidall the same. - In development, RoutingCat registers nothing, and 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 track: one capture, counted once. The destination:
- matches that event alone, on your production hosts through its
$hostproperty, so events from development and staging stay out; - posts to
https://ingest.routingcat.com/v1/events, with your production server events key, fromnpx routingcat keys server, as a bearer token; - sends the event under PostHog’s name for it, with PostHog’s
uuidas itsid, itsrc_vidproperty 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 RoutingCat’s 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:writeon the project: post that body tohttps://us.posthog.com/api/projects/<project id>/hog_functions/, oreu.posthog.comon 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:
- Open Data pipelines, choose New, then Destination, and create an “HTTP Webhook”.
- Name it, such as
RoutingCat: waitlist_joined. - Set the webhook URL to
https://ingest.routingcat.com/v1/events, and the method toPOST. - Replace the JSON body with the
bodyvalue above. - Add the headers
Content-Type: application/json, andAuthorization: Bearerfollowed by the server events key. - Filter on the event alone, and on the event property
$hostequal to your production hosts, with “Filter out internal and test users” off. - 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.