Goal
Problem: IP is only available on event properties (e.g. $ip on $pageview), but you need it on the person profile so:
- You can send it to Meta / Google Ads via PostHog.
- Free trial / product logic doesn’t break because later events differ.
Solution: Use a Custom Transformation in PostHog’s Data pipeline to copy ‘$ip’ from the event into:
- $set.$ip → current IP on the person.
- $set_once.$initial_ip → first seen IP on the person.
Applied only to real pageviews with a client IP (not backend/server events).
1. Check that your events have $ip
You need events that already contain the IP in their properties.
- Go to Data → Events in PostHog.
- Open a recent $pageview (or your page view event).
- Confirm you see something like: { “event”: “$pageview”, “properties”: { “$ip”: “123.45.67.89”, … } }
If $ip isn’t there, fix your tracking / proxy first. The transformation can’t invent the IP.
2. Create a Custom Transformation
- In PostHog, go to Data → Data pipeline → Custom transformations.
- Click “New transformation”.
- Give it a name like:
“Set IP on person profile from pageviews”.
3. Configure when the transformation runs
Inside the transformation config:
- Event matcher
- Set event matcher to your pageview event, e.g.
- Event name: $pageview (or your specific event).
- Set event matcher to your pageview event, e.g.
- Filter to only events where IP exists
- Add a filter:
- Property: IP address (this is $ip)
- Condition: is set / is not empty
- Add a filter:
This ensures:
- It only runs on front-end pageviews with a real user IP.
- It doesn’t touch backend events that contain the server’s IP.
4. Add the transformation code
In the Source code section, choose JavaScript/TypeScript and use this:
What this does:
- Reads the IP from event.properties.$ip.
- Ensures $set and $set_once objects exist on the event.
- Writes to:
- person.$ip (updatable current IP).
- person.$initial_ip (first IP, never overwritten because of $set_once).
5. Enable the transformation
- Click Save.
- Toggle the transformation On (enabled).
- Make sure it’s attached to the correct source (where your pageviews come from) and destination (PostHog itself).
6. Test it
- Open your website in an incognito/private window.
- Trigger a $pageview (just load the page).
- In PostHog:
- Go to Activity / Live events and find your
$pageview. - Inspect the event payload – you should see: “properties”: { “$ip”: “123.45.67.89”, “$set”: { “$ip”: “123.45.67.89” }, “$set_once”: { “$initial_ip”: “123.45.67.89” }, … }
- Go to Activity / Live events and find your
- Open the Person created for that event.
- You should now see person properties:
- $ip
- $initial_ip
- You should now see person properties:
If you do, the sync to Meta / Google Ads can now use these person properties.
7. Notes / gotchas
- Existing users: This only affects events processed after the transformation is enabled. Old events won’t retroactively set IP.
- Backend events: As long as you kept the filter IP address is set and event matcher to $pageview, you won’t pollute profiles with your server’s IP.
- Changing IPs: $ip will update over time as users move networks. $initial_ip stays as the first captured IP because of $set_once.
That’s it. With this transformation active, every qualifying pageview automatically propagates the IP onto the person profile, and you avoid breaking your trial / product tracking logic.
Frequently asked qusetions
Why can I only get the IP from event properties and not from person properties?
Person properties are updated after an event is processed.
If the first event didn’t pass an IP or your backend replaced it with the server IP, then the person profile never receives the correct one. Event properties are the source of truth; that’s why you extract the $ip field from the event payload during transformation.
Should I store both $ip and $initial_ip?
Yes. They serve different purposes:
$ip: The most recent IP. Useful for ad platforms and fraud checks.$initial_ip: The user’s first IP. Helpful for attribution and identity resolution.
Using $set_once ensures you don’t overwrite the first IP even if the user changes networks.
Can I use this IP to improve my Meta/Google Ads performance?
Yes. Both Meta CAPI and Google Enhanced Conversions benefit from accurate IP data.
If you send events without real client IPs:
Google may drop conversions.
Meta will downgrade match quality.
Attribution accuracy falls off a cliff.
Once the IP is stored on the person, PostHog’s CAPI integrations can reliably send it for all future conversions.
How do I avoid capturing my server’s IP from backend events?
Two safeguards are essential:
Event matcher: Only run the transformation on
$pageviewor whichever front-end event you trust.IP is set filter: Backend events often overwrite
$ipwith the server’s IP; filtering prevents this.
If you skip this, every user may end up with your backend’s IP, which breaks matching and attribution.
Will this affect historical users?
No. Transformations only apply to new incoming events.
If you want to enrich old profiles, you must re-ingest past events or manually backfill person properties.
What if multiple users share the same IP (e.g., office, school, VPN)?
This doesn’t break anything. The IP is just one matching factor. Meta/Google combine it with:
Email
Phone
Click IDs
Browser signals
User agent
You don’t risk “merging” users based solely on shared IP.
Does this work with PostHog’s reverse proxy or server-side rendering setups?
Yes, but only if the IP is preserved correctly.
For SSR setups, ensure:
The proxy forwards
X-Forwarded-For.PostHog’s ingestion endpoint uses the forwarded IP instead of the server IP.
If the event arrives without a real client IP, the transformation can’t fix it.
Is storing IP compliant with GDPR/CCPA?
It depends on your basis for processing:
If you’re using legitimate interest for analytics & marketing, storing IP may be justified.
If you require consent, you must only set IP after the user accepts tracking.
PostHog gives you the mechanism; you are responsible for the compliance posture.
Can I set other person properties the same way?
Yes. The same pattern applies to any event property you want to persist:
UTM parameters
Landing URL
Device info
Custom user traits
Just add extra $set / $set_once keys inside the transformation code.



