Help improve docs quality by sharing anonymous interaction telemetry (no PII or token data).

PostHog Packaging Analytics

Stream entitlement denial events to PostHog with explicit identity mapping and bounded volume.

Every denied entitlement check is a commercial moment: a customer just hit the edge of what their plan allows. The packaged destination (Integrations → PostHog) streams each denial into your PostHog project as an mk_denial event carrying the feature, the machine-readable reason, and the plans that would have granted it — the raw material for conversion funnels, feature-wall rankings, and packaging experiments. For richer client-side instrumentation (including allowed decisions), the SDK's DecisionObserver extension point is the manual half.

Step 1 — Connect your PostHog project

In Integrations → PostHog → Connect, paste your project API key (phc_…, from PostHog → Project settings). This is the ingestion key — MonetizeKit never asks for a personal API key and never reads from your project. The key is validated against PostHog's read-only /decide endpoint, which authenticates it without creating any event, person, or group, then stored envelope-encrypted and never re-displayed. Pick your ingestion host (us.i.posthog.com or eu.i.posthog.com) and make two explicit identity choices:

Identity mappingWhat each event carriesFits when
group (default)$groups: { account } + $process_person_profile: falseB2B / org-backed customers. Events land as group analytics; person profiles are never created for accounts.
personThe customer id as the person distinct_idB2C products where each MonetizeKit customer is one human.

The mapping choices are exactly group and person. Group-mapped events set $process_person_profile: false on the wire, so a group-mapped workspace can never accidentally create person profiles for accounts. Separately, choose how customer identifiers travel:

Identifier modeEffect
fullCustomer ids are sent as-is (default) — easiest to join against your own data.
hashedIds are replaced with a stable SHA-256 hash — cohorts still work, raw ids never leave.
omittedNo customer identifier at all — events are anonymous volume only.

Step 2 — Preview before anything is sent

Nothing streams until you confirm. Before connecting, the wizard shows two things: a volume projection computed from your workspace's own denial history over the last 30 days (so the monthly cap you set is grounded in your real traffic, not a guess), and the exact event PostHog will receive — rendered by the same code the delivery pipeline uses, from your most recent real denial when one exists:

{
  "event": "mk_denial",
  "distinct_id": "cus_sample",
  "properties": {
    "feature_key": "advanced_reports",
    "reason_code": "not_in_plan",
    "granted_by_plans": [
      "Growth",
      "Scale"
    ],
    "environment": "production",
    "customer_id": "cus_sample",
    "$process_person_profile": false,
    "$groups": {
      "account": "cus_sample"
    }
  },
  "timestamp": "2026-07-28T12:00:00.000Z"
}
PropertyMeaning
feature_keyThe feature the customer was denied.
reason_codeMachine-readable cause (not_in_plan, limit_reached, unknown_feature, …).
granted_by_plansPublished plans that would grant (or raise the limit for) the feature.
environmentThe environment the check ran in (production, test, …).
customer_idThe customer identifier — full, hashed, or absent per your identifier mode.
collapsed_countHow many identical denials this delivery represents (collapse window).

granted_by_plans is the actionable part: the published plans that would have granted (or raised the limit for) the denied feature — the same signal your app can use for upgrade prompts via the API's grantedByPlans field.

Volume controls — caps and collapsing

Two mechanisms keep a hot denial loop from becoming a PostHog bill surprise:

  • Collapse window — identical denials (same customer + feature + reason) inside a 300-second window merge into one delivery with a collapsed_count property, so a retry storm is one event, not a thousand.

  • Monthly cap — you set a hard events-per-month cap at connect time (default 50,000). At the cap, delivery stops visibly: further events are marked skipped_capped (never silently dropped), the destination flips to capped, and you get a notification. Delivery resumes automatically when the month rolls over.

Build the funnel in PostHog

The event is deliberately minimal so it composes with your existing PostHog data. The highest-signal starting point is denial → upgrade:

# In PostHog: Insights -> Funnel
# Step 1: mk_denial            (customer hit a packaging wall)
# Step 2: subscription upgraded    (from your billing events)
#
# Break down step 1 by "feature_key" to rank which walls convert —
# and by "reason_code" to separate plan gaps (not_in_plan) from
# limit pressure (limit_reached).

Manual pattern — every decision via the SDK's DecisionObserver

The packaged destination streams denials from the platform side. When you also want allowed decisions, cache/degradation visibility, or custom event names, attach an observer in your app (requires @monetizekit/node ≥ 0.2.0):

import { PostHog } from "posthog-node";
import { MonetizeKit, type DecisionObserver } from "@monetizekit/node";

const posthog = new PostHog(process.env.POSTHOG_PROJECT_KEY, {
  host: "https://us.i.posthog.com",
});

const posthogObserver: DecisionObserver = {
  onDecision(event) {
    posthog.capture({
      distinctId: event.customerId,
      event: event.allowed ? "mk_check_allowed" : "mk_check_denied",
      properties: {
        feature_key: event.featureKey,
        reason_code: event.reasonCode,
        cached: event.cached,
        degraded: event.degraded,
        latency_ms: event.latencyMs,
      },
    });
  },
};

export const mk = new MonetizeKit({
  apiKey: process.env.MONETIZEKIT_API_KEY,
  observers: [posthogObserver],
});

FAQ

Will this create persons in my PostHog project?

Not with the default group mapping — every event carries $process_person_profile: false, so accounts appear only as group analytics. Person profiles are created only if you explicitly choose person mapping at connect time.

Can a PostHog outage slow down entitlement checks?

No. The decision path only writes a durable queue row (fire-and-forget) — delivery to PostHog happens in a separate drainer. If PostHog is down, rows sit as failed and retry on the next hourly drain; decisions are unaffected in latency and outcome.

What about customer PII?

Events never include names or emails — only the customer id, and only in the form you chose: full, SHA-256 hashed, or omitted entirely. Switching the identifier mode later applies to new events (already-delivered events are in your PostHog project, under your retention controls).

Which events are available?

The packaged event types today are exactly mk_denial. The destination stores an explicit per-event-type toggle list, so additional packaging events (trials, upgrades, credit exhaustion) can ship individually — you opt into each, and nothing new is ever streamed without your action.

Node SDK Integration

The DecisionObserver extension point the manual pattern builds on.

Open guide

Entitlement Evaluation Patterns

The reason codes and grantedByPlans field the events carry.

Open guide

Clerk Identity Integration

Keep customer identities synced so distinct ids stay meaningful.

Open guide

Was this page helpful?