If you’ve been using PostHog, you might have been wondering if there’s any way that you can send the conversion events (such as AddToCard, Purchase, or InitiateCheckout) with all the contextual information you have from the users (email, first & last name, address, order value & currency, etc.) to ad platforms such as Meta, Google, Pinterest, etc.

In this post, we’re going to cover how to integrate your PostHog conversion tracking with Meta using CAPI (Conversions API), achieving the highest event score possible with all the information you have from the customers in PostHog.

PostHog has a feature called Destinations [PostHog destinations doc] that allows you to send your data to external systems in real time. ‘Meta Ads Conversions’ is a PostHog default destination that you should use to send conversion events to Meta Ads.

Requirements

We assume you are already have PostHog setup and tracking the events you intend to send to Meta Ads. If not, go to installation guides in our PostHog articles, OR reach out to us to schedule a free consultation call.

Begin Here

To start, navigate to ‘Data management’ and then to ‘Destinations’. Here, you can find your required destination by selecting ‘Meta Ads Conversions’. It’s worth noting that for each event type, you’re going to need a new Meta destination in PostHog. (This is not the case for Google and other ad platforms in PostHog, where they can handle all event types in a single destination.)

First, you should set the filters and triggers to select the PostHog events that should be sent to Meta. For instance you have an action name Order Completed that should be sent as a Purchase event. This is basically the mapping step.

After filters and triggers, you’ll find event parameter fields. PostHog has set a decent default values there already. Some of the fields you see are common among all event types, but some need to be added/set depending on the event type. We’ll cover both in-depth:

Common fields

Event data

These are the fields you’re going to need to fill in for every event, regardless of what event it is.

Access Token: It’s your authentication token. You can generate it from the Event Source settings in Meta.

Pixel ID: Specifies which pixel you want this event sent to. Found in your Meta Business Manager under your website pixel settings.

Event name: Which is the name of the event. (Case sensitive!)

Event ID: This is perhaps the most important field, as it directly determines how effective your deduplication process will be. Make sure it’s populated with a value that remains consistent every time the event runs. For example, for a Purchase event, use the Order ID; for bookings, use the Booking ID… or any similar variable that is generated only once per transaction.

Event source URL: The URL of the page where the event occurred (if it’s a web event, not for renewal transactions for instance). PostHog already prefills it with {event.properties.$current_url}.

Event time: The time when the event happened. If you have your own timestamp recording process, use it. Otherwise, just leave it as {toInt(toUnixTimestamp(event.timestamp))}.

Action source: Where the conversion occurred. Most of the time, simply “Website – Conversion was made on your website.”

User data

This is perhaps the second most important field, as it directly determines the quality of your Meta CAPI event and the fate of your campaign optimizations.

Fields that require hashing:

All of the following parameters need to be converted to lowercase, trimmed of whitespace, and hashed with SHA-256 before being sent.

em: Email address. If you set this property, you can catch it with this {sha256Hex(lower(person.properties.email))}.

ph: Phone number (digits only, including country code). Same as above, and it will be {sha256Hex(replaceRegexpAll(person.properties.phone, '[^0-9]', ''))}.

fn: First name. Same as above, it will be {sha256Hex(lower(person.properties.first_name))}.

ln: Last name. Same as above, it will be {sha256Hex(lower(person.properties.last_name))}.

ge: Gender. Same as above, it will be {sha256Hex(lower(person.properties.gender))}.

db: Date of birth. {sha256Hex(replaceAll(person.properties.date_of_birth, '-', ''))}.

ct: City. In case you don’t ask it from the user, you have the option to fill it with the city detected from the user’s GeoIP. In that case, it will be {not empty (person.properties.$geoip_city_name) ? sha256Hex(replaceAll(lower(person.properties.$geoip_city_name), ' ', '')) : null}.

st: State. Same as above, it will be {not empty (person.properties.$geoip_subdivision_1_name) ? sha256Hex(replaceAll(lower(person.properties.$geoip_subdivision_1_name), ' ', '')) : null}.

zp: Zip/Postal code. Same as ‘ct’. It will be {not empty (person.properties.$geoip_postal_code) ? sha256Hex(replaceAll(lower(person.properties.$geoip_postal_code), ' ', '')) : null}.

country: Country code. Same as ‘ct’. It will be {sha256(lower(person.properties.$geoip_country_code))}.

Fields that do not require hashing:

client_ip_address: This is usually {event.properties.$ip}, but make sure IP capture is enabled on your end. Also, confirm that this is the client’s IP address, not the server IP from which the backend event is being sent. In case you’re using external integrations like RevenueCat, you might find it in {event.properties.subscriber_attributes.$ip}.

client_user_agent: This may sound like gibberish, but it can hugely impact the event quality. Definitely send it if you have it. It can usually be found at {event.properties.$raw_user_agent}. But again, make sure it’s not the server user agent from which the backend event is being sent.

fbc: It is Meta’s way of preserving ad click context once a user lands on your website. It is derived from the fbclid query parameter that Meta automatically appends to your URL when someone clicks a Meta ad. Its purpose is to help Meta match downstream conversion events (Purchase, Lead, etc.) back to the specific ad, ad set, and campaign that generated the click. This significantly improves event match quality and attribution accuracy, especially for server-side events sent through the Conversions API.
Use the following expression to build it:
{not empty(person.properties.fbclid ?? person.properties.$initial_fbclid) ? f'fb.1.{toUnixTimestampMilli(now())}.{person.properties.fbclid ?? person.properties.$initial_fbclid}' : ''}.
This expression checks whether a valid fbclid value exists on the user (either the current fbclid or the initially captured $initial_fbclid). if one exists, it constructs the required fbc string in Meta’s expected format:

  • Prefix: fb.1. (static identifier required by Meta)
  • Timestamp: current Unix timestamp in milliseconds
  • Click ID: the available fbclid value

If no fbclid is available, the expression returns an empty string, meaning no fbc value will be sent.

If you don’t see any of these fields defined by default, you can add as many as you want by clicking on ‘Add entry’.

Event-specific fields:

Besides those common fields among all events, you may also need some extra fields depending on the event type. We’ll cover the most important ones based on our experience. For a full list of standard events and their parameters, visit our Meta for Developers site. You can quickly add any parameters you want after this article.

value:
May come with: AddPaymentInfo, AddToCart, AddToWishlist, CompleteRegistration, InitiateCheckout, Lead, Search, StartTrial, Subscribe, and ViewContent.
It’s the value associated with this event. (e.g. {event.properties.price})

currency:
May come with: AddPaymentInfo, AddToCart, AddToWishlist, CompleteRegistration, InitiateCheckout, Lead, Purchase, Search, Subscribe, and ViewContent.
It shows the currency for the value specified. (e.g. {event.properties.currency})

The currency and value fields always come together, and they are required properties for the Purchase event.

predicted_ltv:
May come with: StartTrial and Subscribe.
It’s the advertiser’s estimate of how much total revenue a subscriber will generate over their entire lifetime..

content_ids:
May come with: AddPaymentInfo, AddToCart, AddToWishlist, InitiateCheckout, Purchase, Search, and ViewContent.
The product IDs associated with the event, such as SKUs or article IDs.

content_type:
May come with: AddToCart, Purchase, Search, and ViewContent.
It’s either ‘product’ or ‘product_group’ based on the ‘content_ids’ or ‘contents’ being passed.
If the IDs being passed are IDs of ‘products’, then the value should be product. If product group IDs are being passed, then the value should be ‘product_group’.

contents:
May come with: AddPaymentInfo, AddToCart, AddToWishlist, InitiateCheckout, Purchase, Search, and ViewContent.
It’s an array of JSON objects that contains the quantity and the International Article Number (EAN), when applicable, or other product or content identifier(s). id and quantity are the required fields.

num_items:
May come with: InitiateCheckout and Purchase.
It’s the number of items when checkout was initiated. (e.g. {length(event.properties.products)})

search_string:
May come with: Search.
It includes the string entered by the user for the search.

Also, you can find the object properties that Meta needs you to send for any event through this link.

Important Note: Use test code

While testing, always use the Test Event Code that Meta provides so you can preview the data you’re sending. Get it from your Event Source settings. When you’ve finalized the setup, just delete it, and Meta will count all incoming events as real ones.

Final Word

I hope you found this article from 99Ways helpful. Everything shared here comes from hands-on experience implementing PostHog across dozens of businesses with similar growth and optimization goals.
If you’re looking for a full-service PostHog setup that goes beyond technical implementation, and is aligned with your actual business outcomes, we’d be happy to connect.
Feel free to fill out the Contact Us form to book a free consultation call with our co-founder & CEO, Iman Nazari.

Related blog posts

Leave a Reply

Your email address will not be published. Required fields are marked *