PostHog

Adapty can pass subscription events (purchases, renewals, refunds, trial starts) to PostHog’s event stream, so they join the rest of your data.

Each event includes revenue, currency, store, and product details, plus the paywall and A/B test variation that produced the purchase. Any PostHog tool can use this data once it arrives.

Adapty’s subscription events add a revenue dimension to everything you already track in PostHog:

  • Which in-app actions predict a subscription? Correlate your own PostHog events with trial_started and subscription_started to find the behavior that leads to purchases.
  • Which experiment variant earned more? Use Adapty’s revenue events as the metric for a PostHog experiment on any product change. Adapty’s A/B tests cover paywalls and onboardings.
  • What happens before a refund? Build an insight on subscription_refunded, then open the persons behind it and watch their session recordings.
  • Do bugs affect renewals? Cross-reference error tracking with subscription_renewal_cancelled, which fires as soon as a user turns off auto-renewal — well ahead of subscription_expired.
  • How do subscribers retain compared to free users? Build cohorts and retention curves on derived access status — see How to tell a subscriber from a free user.
  • Which paywall and variation produced the revenue? Purchase events carry the paywall and variation that produced them, so you can break revenue down by paywall. Capture paywall views with PostHog’s SDK to measure view-to-purchase alongside them.
  • Ask questions across both datasets with insights or SQL.

How the integration works

PostHog identifies each user by a string called distinct_id. Everything about this integration depends on Adapty and PostHog agreeing on that string.

  1. The PostHog SDK assigns the user a distinct_id. On first launch, this string is device-scoped and anonymous. But you can assign a custom value on login, or reset it on logout.
  2. On every app launch, after Adapty.activate() and before any purchase can happen, pass the distinct_id to Adapty with setIntegrationIdentifier(). Adapty attaches the value to the user’s Adapty profile as posthog_distinct_user_id.
  3. When the user starts a trial or makes a purchase, Adapty’s servers post the event to PostHog’s capture API with that distinct_id attached. The exchange happens server-to-server, outside your app.
  4. PostHog attaches the event to the person holding that distinct_id, so subscription revenue appears next to everything else the user did in your app.

Match Adapty’s ID to PostHog’s

Both systems must use the same distinct_id for a user, or one person splits into two unconnected entities. You have two ways to avoid the split, and which one fits depends on when a purchase can happen.

If a purchase requires login, call PostHog’s identify() with your Customer User ID at the moment the user logs in. Adapty already uses that value when it has nothing else, so both systems match.

If anonymous users can purchase, read PostHog’s distinct_id and pass it to Adapty with setIntegrationIdentifier. Call it on every launch after Adapty.activate(), and again after every PostHog reset(). Anonymous profiles have no Customer User ID, so PostHog’s own ID is the only value both systems can share — see Configure your app code.

Whichever route you take, the value must survive a reinstall. Adapty creates a new profile on each reinstall, and PostHog a new anonymous distinct_id per device, so neither system carries identity across that gap on its own. Adapty does pass paid access between a user’s anonymous profiles, but that links access levels rather than analytics identity — the inheriting profile still sends events under its own ID. Without a stable ID your own backend owns, a reinstalling subscriber appears in PostHog as a new person with a renewal and no purchase before it.

PostHog blocks certain values during person merges: null, undefined, None, 0, anonymous, guest, distinct_id, id, email, true, false, [object Object], NaN, empty strings, and quoted variants of these. Make sure the value you share can never take one of them. PostHog recommends UUIDs, or validation against that list before you send. See PostHog’s identity resolution guide.

Mismatched IDs cause data fragmentation — see One user appears as several persons.

Adapty’s events carry person properties, which makes every one an identified event in PostHog’s terms — and PostHog charges up to 4x more to process those than anonymous ones. Adapty sends one event per subscription lifecycle change, so the volume stays small next to client-side analytics. Matching IDs is still worth it: PostHog recommends identifying users in almost all cases.

Setup instructions

Copy your PostHog project token

  1. Sign in to the deployment that hosts your data — US Cloud or EU Cloud — and select the project you want to connect.

  2. Go to Settings > Project > General and find the Project token & ID section.

    PostHog project settings showing the Project token, Project ID, and Region
  3. Copy the Project token. It starts with phc_. PostHog describes it as write-only and safe to publish, so you don’t need to rotate it.

Configure Adapty

  1. Open Integrations > PostHog in the Adapty Dashboard.

  2. Enable the PostHog toggle.

  3. Paste the token into Project API key. Adapty checks it against PostHog on save — if the token is invalid, it fails immediately instead of silently dropping events.

  4. The “server location” section depends on your setup.

    • If you use PostHog Cloud, set the Region to the deployment you signed in to — US Cloud or EU Cloud. Leave the PostHog Instance URL field empty.
    • If you run your own instance, select Self-hosted and populate the PostHog Instance URL field. Don’t select a region. Adapty must be able to reach the instance without a proxy / VPN.
  5. Under How the revenue data should be send, choose which revenue figure Adapty sends. The three options match the Adapty Analytics revenue views, so your choice also names the view your PostHog numbers should agree with.

    OptionWhat Adapty sends
    Gross revenueThe full amount the buyer paid, before commission and tax. The default.
    Proceeds after store commissionThe amount minus the store’s commission, with tax still included.
    Proceeds after store commission and taxesThe amount minus both.
  6. Set the remaining options:

    ToggleWhen onDefault
    Report user’s currencyAdapty reports each sale in the currency the buyer paid in, rather than USD.Off
    Send trial priceTrial starts carry no revenue otherwise. Turn this on to give each one a placeholder price, and a Trial price percentage field appears — set it to the share of the subscription price a trial should report. At 60%, a $10 subscription sends $6.Off
    Exclude historical eventsAdapty skips events that happened before the user installed a build containing the Adapty SDK.On
  7. Rename or disable individual events in the Events names section. PostHog accepts any non-empty event name, so use whatever your existing taxonomy expects.

  8. Click Save.

Adapty Dashboard PostHog integration page with the toggle, key, server location, revenue settings, and event names

Keep sandbox data out of production

Adapty sends sandbox and production transactions through the same integration, so both land in the same PostHog project. The integration takes one Project API key, with no separate sandbox key to point elsewhere.

Giving your development build its own PostHog project won’t keep sandbox events out of the production one either. Sandbox is a property of the store transaction, not of your build. App Store review and TestFlight purchases are sandbox transactions from a production build.

Instead, separate the two with queries. Every event includes an environment property set to Sandbox or Production. Filter it to isolate actual revenue:

WHERE properties.environment = 'Production'

Configure your app code

  1. Ask PostHog’s SDK for the current distinct_id:

    • On every app launch, after Adapty.activate() and before any purchase can happen. PostHog attributes any earlier event to a different person.
    • After PostHog’s reset(), which most apps run on logout. reset() mints a new anonymous ID and doesn’t link it to the previous person, so a stale value keeps pointing at the user who just logged out.
  2. Pass it to Adapty with setIntegrationIdentifier().

No extra call is needed after calling PostHog’s identify(). PostHog merges the anonymous person with the identified, so Adapty events resolve the same way. See Match Adapty’s ID to PostHog’s.

Third-party SDKs generate user IDs asynchronously. The ID may not be ready when Adapty.activate() runs. If your Customer User ID comes from one of these SDKs, call Adapty.activate() without it. Once the ID arrives, call setIntegrationIdentifier(), then identify() with the CUID.

Verify the integration

  1. Trigger a sandbox purchase, then open the Event Feed in the Adapty Dashboard. Each delivery attempt appears with its result. Adapty verifies your Project API key and instance URL when you save, so failures at this stage are rare. The usual causes:

    • A key that stopped working. PostHog returns 401 if you delete the integration key.
    • An instance that stopped responding. Adapty stops waiting after 10 seconds. May affect self-hosted deployments.

    Hover a failed event to read PostHog’s response.

  2. In PostHog, open the Activity view and look for the event. The environment property of your purchase should be Sandbox.

  3. Open that person’s profile and check the Distinct IDs tab. Your app’s own events and Adapty’s server events should belong to one person. Two persons for the same user means the IDs don’t match — see One user appears as several persons.

Adapty’s events never reach your app’s own PostHog debug output. Adapty posts them from its servers, so they never pass through your app’s SDK. An empty local log says nothing about the integration.

Report revenue in PostHog

Adapty Analytics stays the source of truth for revenue figures, because it calculates from complete store data while PostHog only receives what this integration forwards. If you also want revenue on a PostHog dashboard next to your product metrics, PostHog’s Revenue Analytics reads it from event properties you nominate. Open Data management > Revenue in PostHog and map:

PostHog fieldAdapty property
Revenueprice_usd, proceeds_usd, or net_revenue_usd — match the option you selected under How the revenue data should be send
Currencycurrency, or set a static currency if you report in USD
Productvendor_product_id
Subscriptionoriginal_transaction_id

Leave PostHog’s “values are in cents” option off. Adapty sends decimal amounts, not minor units.

PostHog event structure

Adapty sends the events you enabled in the Events names section of the PostHog integration page, one capture request per event:

{
  "api_key": "phc_YOUR_PROJECT_TOKEN",
  "distinct_id": "john.doe@example.com",
  "timestamp": "2026-01-08T11:06:12+00:00",
  "event": "subscription_started",
  "properties": {
    "$ip": "10.168.1.1",
    "$geoip_time_zone": "America/New_York",
    "$geoip_disable": true,
    "$set": {
      "email": "user@example.com",
      "first_name": "John",
      "last_name": "Doe",
      "birthday": "1990-01-01",
      "gender": "male",
      "os": "iOS"
    },
    "*": "{{other_event_properties}}"
  }
}
ParameterTypeDescription
api_keyStringYour PostHog Project API key.
distinct_idStringIdentifies the person in PostHog. Adapty uses the first value it finds — see distinct ID priority.
timestampISO 8601 date & timeWhen the event occurred. Renewals and trial conversions can be dated in the future — see Events appear in PostHog before they happen.
eventStringThe name you set in the Events names section.
propertiesObjectAdapty’s event properties, the IP and location properties, and $set. Adapty omits any property without a value.

Five webhook-only properties never appear here — see Limitations.

Distinct ID priority

Adapty uses the first value it detects:

PriorityValueSet by
1posthog_distinct_user_idYour setIntegrationIdentifier call
2Customer User IDAdapty.activate() or Adapty.identify()
3Adapty’s internal profile IDAdapty, always present

Adapty resolves this order per event, not once per user. An event that fires before your setIntegrationIdentifier call lands under a lower-priority ID, and PostHog records a second person for the same user.

Pick one of the two setups in Match Adapty’s ID to PostHog’s, then put the value in place before any event can fire. For persons that already split, see One user appears as several persons.

IP and location properties

To segment Adapty events by location, use the store_country and profile_country properties. Adapty switches off PostHog’s location lookup, so PostHog adds no $geoip_* values of its own. The three properties below apply per event, so your project settings and your own events are unaffected.

PropertyValueEffect
$ipThe user’s IP addressPostHog stores it on the event. Adapty sends the same value as an x-forwarded-for header.
$geoip_time_zoneThe user’s time zoneAdapty sets this value directly.
$geoip_disableAlways trueSwitches off PostHog’s location lookup for this event.

Person properties

Everything inside $set becomes a PostHog person property instead of an event property. PostHog attaches person properties to the person rather than to a single event, so they describe the user’s current state rather than a moment in time. Adapty omits any field it has no value for, and drops $set altogether when it has none of them.

ParameterTypeDescription
emailStringThe user’s email address.
first_nameStringThe user’s first name.
last_nameStringThe user’s last name.
birthdayString (date)The user’s date of birth.
genderStringThe user’s gender.
osStringThe operating system of the user’s device.

Limitations

  • No access level or subscription status. The access_level_updated event is exclusive to webhook integrations, so Adapty sends PostHog no field describing what the user currently has access to — see How to tell a subscriber from a free user.
  • No historical backfill. Adapty forwards events from the moment you enable the integration. Past purchases never reach PostHog.
  • PostHog doesn’t geo-tag Adapty events. PostHog infers location from the event source’s IP address. The source for Adapty events is always an Adapty server — not the user’s device. To avoid data pollution, Adapty tells PostHog to skip the lookup. Adapty populates $geoip_time_zone, store_country, and profile_country — but no finer location data.
  • You can’t filter Adapty events by source. PostHog records the sending SDK in $libposthog-ios, posthog-android, web — but Adapty posts straight to PostHog’s API without SDK mediation, so that property stays empty. Filter by event name instead.

Troubleshooting

Events don’t appear in PostHog

  • Check the Adapty Event Feed first. A failed delivery shows the error PostHog returned.
  • A successful delivery doesn’t guarantee PostHog kept the event. PostHog answers 200 OK once the payload and key are valid, then silently discards events that have no name or an empty distinct_id.
  • Confirm the event you’re looking for is enabled in the integration settings.
  • If you host PostHog yourself, make sure the server accepts Adapty’s POST requests at /capture. A successful configuration doesn’t guarantee this access exists — Adapty uses a different endpoint to check your key’s validity.

access_level_updated shows as failed in the Event Feed

access_level_updated is a webhook-only event. Adapty never sends it to this integration. But Adapty records an outcome for every enabled integration, and an unsupported event is displayed as a failure.

One user appears as several persons in PostHog

PostHog cannot undo most splits after the fact — see Match Adapty’s ID to PostHog’s.

Why the IDs diverge

Each event Adapty sends carries a distinct_id from the user’s Adapty profile — see Distinct ID priority. Adapty reads that value when it sends the event, not when your app calls setIntegrationIdentifier. If the distinct_id of an Adapty event differs from the internal distinct_id of the app install, PostHog attributes the two kinds of events to two different persons.

Three things produce the mismatch:

  1. Your app never calls setIntegrationIdentifier on one of the platforms. Adapty falls back to the Customer User ID or the anonymous profile ID. Check every platform you ship on.
  2. The setIntegrationIdentifier call happens too late. Subscription events that occur before the call carry the fallback ID.
  3. The ID you send to PostHog with identify() differs from the one you set as the integration identifier. Adapty keeps the value you last passed and never updates it on its own. PostHog’s reset() assigns a new anonymous ID, which leaves Adapty with the previous one — so call setIntegrationIdentifier again after every reset().

Fix all three, and PostHog records one person per user from then on.

Merge diverging user IDs at the first identify() call

Your app gets one window to reconcile the divergent IDs: its first call to PostHog’s identify(). PostHog merges the events of the app install into the person you name in that call, so name the ID Adapty sends — see Match Adapty’s ID to PostHog’s. After that call, PostHog treats the app install as identified, and refuses to merge two identified persons.

Check for a refused merge

To confirm that PostHog recorded one user as two persons, open Data management > Ingestion warnings in PostHog and look for the Refused to merge an already identified user error.

PostHog also blocks merges when the ID is one of its reserved values — see Match Adapty’s ID to PostHog’s for the list.

Repair an existing split

Neither identify() nor alias() recovers a split once the merge window has closed. Only PostHog’s $merge_dangerously can force the merge. PostHog documents it as irreversible, without safeguards, and meant as a one-off recovery from implementation problems.

You send it as an event rather than as a setting, and it names two persons. The direction decides which one survives:

FieldValue
distinct_idThe person that survives the merge
properties.aliasThe person merged into it — its events and distinct_id transfer to the survivor

Decide which side survives before you send anything. The Adapty person holds the subscription history, and your app’s person holds the in-app behavior. Run a single user through it and check the result before any bulk repair. PostHog’s How to merge users has the payload for each of its SDKs.

Revenue in PostHog doesn’t match Adapty Analytics

Adapty and PostHog process the same events differently. These differences account for almost every mismatch.

  • Every Adapty event carries three revenue amounts, one for each option under How the revenue data should be send. Here’s how they match to PostHog properties:

    Adapty AnalyticsEvent property (In USD)Event property (In the buyer’s currency)
    Gross revenueprice_usdprice_local
    Proceeds after store commissionproceeds_usdproceeds_local
    Proceeds after store commission and taxesnet_revenue_usdnet_revenue_local

    Comparing across rows produces a gap equal to the commission, the tax, or both. Adapty sends all six properties on every event, whatever Report user’s currency is set to.

  • Adapty counts every revenue event in a period; a PostHog insight counts only the events you add to it. If you omit subscription_renewed, you drop most of the revenue for any established app.

  • Adapty’s date range covers the whole final day; a timestamp filter stops at the instant you give it. Adapty’s Jul 1 – Jul 15 includes every event through Jul 15 23:59:59. In PostHog, timestamp < 2026-07-15 drops that entire day — use timestamp < 2026-07-16.

  • Adapty Analytics separates sandbox from production; PostHog mixes them. Filter on properties.environment = 'Production' — see Keep sandbox data out of production.

  • Adapty uses your app’s reporting timezone; PostHog receives UTC. Integrations always get UTC timestamps, whatever you set in App Settings. A purchase at 23:30 UTC on Jul 1 lands on Jul 2 in Adapty if your reporting timezone is +02:00, while PostHog keeps it on Jul 1.

  • PostHog is missing historical events. Two separate limits keep old events out, so earlier subscriptions and renewals only reach Adapty Analytics. Events your own PostHog SDK captured are unaffected.

    • Exclude historical events, a toggle on the PostHog integration page, is on by default. Adapty doesn’t send events dated before the profile existed, and the Event Feed flags each one as expired. A user’s Adapty events therefore start at their first launch of an Adapty build. Turn it off to let back-dated events through from that point on.
    • No historical backfill is a permanent limitation. Adapty sends events as it processes them, and never returns for events it processed before you enabled the integration.
  • Events you disabled in the integration settings never reach PostHog. Adapty Analytics still counts them. Check the Events names section in Configure Adapty.

Events appear in PostHog before they happen

For renewals and trial conversions, Apple notifies Adapty before the event happens. Adapty forwards these events immediately, with the future timestamp unchanged. Adapty Analytics holds them back until that time passes, so PostHog shows events Adapty doesn’t report yet. Both are correct. Filter on timestamp to exclude them — see Event timestamps with future dates.

No country or city data on Adapty events

Use the store_country and profile_country properties instead. PostHog’s own $geoip_* values stay empty on Adapty events — see IP and location properties.

How to tell a subscriber from a free user

Adapty event reports don’t disclose the user’s current access. $set includes only email, first_name, last_name, birthday, gender, and os. The event properties that describe access levels are populated only on access_level_updated, and Adapty shares that event exclusively with the webhook integration.

Two ways forward:

  • Derive status inside PostHog from the event history. A person whose most recent Adapty event is subscription_started, subscription_renewed, or trial_converted currently has access; one whose most recent event is subscription_expired, trial_expired, or subscription_refunded does not.
  • Use the webhook integration to receive access_level_updated, then forward it into PostHog yourself. PostHog’s capture API expects its own payload shape, so this needs a transformation step on your side — Adapty’s webhook payload can’t be posted to PostHog unchanged.

Paywall views don’t appear in PostHog

Adapty’s SDK captures paywall, flow, and onboarding interactions for Adapty Analytics only. Adapty’s server forwards subscription events to integrations, and these interactions aren’t subscription events. Webhooks don’t include them either. Capture them with PostHog’s SDK where you display the paywall.