⚡ ⚡ Learn how Investing.com uses Jitsu to move data faster

Subscription analytics for publishers: from anonymous reader to subscriber

A subscription is the conversion a publisher's ad spend is judged on, and the browser sees little of it: trials convert days later, in the billing system. Subscription analytics on Jitsu starts from the events. The funnel from first article to paid subscription lands in your warehouse, billing sends subscription_started and trial_converted, and a function passes those two to Google Ads and Meta Ads only for readers who consented to advertising. This page is part of the media and publishing guide.

TL;DR

  • Billing sends subscription_started and trial_converted over the HTTP API with a server-to-server write key.
  • Google Ads and Meta Ads get those two events with three properties: order_id, value and currency.
  • A conversion without recorded advertising consent reaches neither ad platform.
  • Identity stitching rewrites warehouse rows only, up to 30 days back, on Jitsu Cloud Enterprise or self-hosted Jitsu.
  • The SQL below joins anonymous reading to subscribers through identify events, on any plan.

Who this is for

Heads of subscriptions and performance marketing at publishers that buy Google and Meta ads to acquire subscribers, and the engineers who connect the billing backend.

The problem

An ad platform's pixel sees the checkout page at best. Trial conversions and renewals happen in billing, and blockers stop the pixel for part of the audience.

A reader who subscribes today can have read anonymously for weeks. Without joining those IDs, nobody can say which articles, paywall hits and newsletters came first.

The events

EventEmitted fromRequired propertiesPII handling
pageBrowserarticle_id, is_paywalledPersonal query parameters stripped
paywall_shownBrowser, apparticle_id, paywall_type, meter_countNone
registration_wall_shownBrowserarticle_id, placementNone
signed_up, then identifyBrowser, appmethod; userId (internal ID)No email in the browser
newsletter_subscribedBrowserlist_id, source_article_idNone
checkout_startedBrowseroffer_id, plan, price, currencyCard numbers never enter events
subscription_startedBilling backendsubscription_id, plan, value, currency, trial, ad_consentNo email
trial_convertedBilling backendsubscription_id, value, currency, ad_consentNo email
subscription_canceledBilling backendsubscription_id, reason, tenure_daysWarehouse only

Keep card numbers out of events at the source; the checkout page and payment provider handle them. The media tracking plan (CSV) has every event with property types.

Sending them

In the browser, identify the reader at sign-up and on every login with your internal user ID, and send funnel events with track. Leave email out of browser traits.

jitsu.identify("u-1001", { plan: "registered" });
jitsu.track("paywall_shown", { article_id: "a-2231", paywall_type: "meter", meter_count: 5 });

The billing backend sends the conversions over the HTTP API with a server-to-server write key and Strict Mode on. Here's how it works:

  1. At checkout, store with the subscription: the reader's anonymous ID (the __eventn_id cookie), the advertising consent your consent platform recorded, Meta's _fbc and _fbp cookies, and the Google click ID (gclid, gbraid or wbraid) the reader arrived with.
  2. Send subscription_started when billing starts a trial or a subscription, and trial_converted on the first paid charge after a trial.
  3. Put the stored consent in properties.ad_consent as true or false.
  4. Pass the reader's IP address and user agent in context. Server-to-server events do not take them from request headers.
  5. Put the stored click IDs in context.clientIds and the checkout page URL in context.page.url. The Google Ads destination matches the conversion on its click ID, and the Meta Ads destination sends fbc, fbp and the page URL as event_source_url.
curl --location 'https://events.example.com/api/s/track' \
--header 'Content-Type: application/json' \
--header 'X-Write-Key: keyId:keySecret' \
--data-raw '{"type":"track","event":"trial_converted","userId":"u-1001","anonymousId":"7c1e9a52-0d4b-4a8e-9f3a-2b6f1c9d8e01","properties":{"subscription_id":"sub_8812","plan":"digital-annual","value":89,"currency":"EUR","ad_consent":true},"context":{"ip":"203.0.113.7","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)","clientIds":{"gclid":"EAIaIQobChMIexample","fbc":"fb.1.1792560000000.IwAR0example","fbp":"fb.1.1792560000000.1098765432"},"page":{"url":"https://www.example.com/subscribe"}}}'

The function

Attach it to the billing stream's Google Ads connection with AD_PLATFORM=google-ads and to its Meta Ads connection with AD_PLATFORM=meta. For each event:

  1. Anything other than subscription_started and trial_converted is dropped: page views, video and engagement events included.
  2. A conversion without ad_consent: true is dropped.
  3. Conversions without a subscription ID, a numeric value or a three-letter currency are dropped with a warning in Live Events.
  4. The rest leave with three properties, order_id, value and currency, and without context.traits. The order_id is the subscription ID and the event name, such as sub_8812:trial_converted, so the trial start and the first paid charge on one subscription carry different IDs. Article IDs, sections, plan names and email stay in the warehouse.
  5. On the Meta connection, the event takes Meta's standard event name: StartTrial for a trial start, Subscribe for a paid subscription (Meta Pixel reference).

Build and test it with jitsu-cli: it is TypeScript, and the UI editor takes JavaScript. Destination setup: Google Ads docs, Meta Ads.

import { JitsuFunction } from "@jitsu/protocols/functions";
import { AnalyticsServerEvent } from "@jitsu/protocols/analytics";

export const config = {
  slug: "ad-conversions",
  name: "Subscription conversions for ad platforms",
  description: "Lets consented subscription conversions through to ad platforms with three properties and no traits",
};

type Props = {
  // "google-ads" or "meta": the destination on this connection
  AD_PLATFORM: string;
};

const CONVERSIONS = ["subscription_started", "trial_converted"];

const adConversions: JitsuFunction<AnalyticsServerEvent, Props> = async (event, { log, props }) => {
  // Only the two conversions reach an ad platform. Page views, video and engagement events stop here.
  if (event.type !== "track" || !CONVERSIONS.includes(event.event ?? "")) {
    return "drop";
  }
  const p = event.properties ?? {};
  // ad_consent is the advertising consent your backend recorded for this reader at checkout.
  if (p.ad_consent !== true) {
    return "drop";
  }
  const currency = typeof p.currency === "string" ? p.currency.toUpperCase() : "";
  const valid =
    typeof p.subscription_id === "string" &&
    p.subscription_id !== "" &&
    typeof p.value === "number" &&
    Number.isFinite(p.value) &&
    /^[A-Z]{3}$/.test(currency);
  if (!valid) {
    log.warn(`${event.event} ${event.messageId}: needs subscription_id, numeric value and currency, dropped`);
    return "drop";
  }
  // Only these three properties reach the ad platform. Articles, sections, plan names,
  // cancellation reasons and traits such as email stay in the warehouse. The order ID includes
  // the event name, so a trial start and its first paid charge do not share one.
  event.properties = { order_id: `${p.subscription_id}:${event.event}`, value: p.value, currency };
  if (event.context?.traits) {
    delete event.context.traits;
  }
  if (props.AD_PLATFORM === "meta") {
    event.event = event.event === "subscription_started" && p.trial === true ? "StartTrial" : "Subscribe";
  }
  return event;
};

export default adConversions;

Video events never pass this function. The United States Video Privacy Protection Act restricts disclosing which videos a person requested or obtained together with information that identifies them.

Identity stitching scope

On Jitsu Cloud Enterprise and on self-hosted Jitsu, identity stitching rewrites a reader's earlier anonymous events with their user ID and traits when they identify, up to 30 days back. Six warehouses support it: ClickHouse, BigQuery, Postgres, Redshift, Snowflake and MySQL. It requires deduplication.

What Google Ads, Meta or analytics tools already received does not change.

Safari shortens the window. The anonymous ID cookie set by JavaScript lasts 7 days there, so a Safari reader who returns after a week starts a new anonymous ID unless Jitsu's ID endpoint is deployed (see the server-side tracking page).

Querying it

The query joins through identify events, which carry both anonymous_id and user_id, so it runs on any plan. With identity stitching on, earlier rows already carry the user ID and the join still holds.

-- BigQuery: what new subscribers did in the 30 days before subscribing
WITH subs AS (
  SELECT user_id, MIN(timestamp) AS subscribed_at
  FROM `your_dataset.events`
  WHERE type = 'track'
    AND event = 'subscription_started'
    AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  GROUP BY user_id
),
id_map AS (
  -- every anonymous ID a reader has identified from, on any plan
  SELECT DISTINCT user_id, anonymous_id
  FROM `your_dataset.events`
  WHERE type = 'identify'
    AND user_id IS NOT NULL
    AND anonymous_id IS NOT NULL
),
history AS (
  SELECT s.user_id, e.message_id, e.type, e.event, e.article_id
  FROM subs AS s
  JOIN id_map AS m ON m.user_id = s.user_id
  JOIN `your_dataset.events` AS e
    ON e.anonymous_id = m.anonymous_id
   AND e.timestamp BETWEEN TIMESTAMP_SUB(s.subscribed_at, INTERVAL 30 DAY) AND s.subscribed_at
),
per_reader AS (
  SELECT
    s.user_id,
    COUNT(DISTINCT IF(h.type = 'page', h.article_id, NULL)) AS articles_read,
    COUNT(DISTINCT IF(h.event = 'paywall_shown', h.message_id, NULL)) AS paywalls_seen,
    COUNT(DISTINCT IF(h.event = 'newsletter_subscribed', h.message_id, NULL)) AS newsletter_signups
  FROM subs AS s
  LEFT JOIN history AS h ON h.user_id = s.user_id
  GROUP BY s.user_id
)
SELECT
  COUNT(*) AS new_subscribers,
  APPROX_QUANTILES(articles_read, 2)[OFFSET(1)] AS median_articles_read,
  APPROX_QUANTILES(paywalls_seen, 2)[OFFSET(1)] AS median_paywalls_seen,
  ROUND(SAFE_DIVIDE(COUNTIF(newsletter_signups > 0), COUNT(*)), 3) AS share_with_newsletter_signup,
  ROUND(SAFE_DIVIDE(COUNTIF(articles_read = 0 AND paywalls_seen = 0), COUNT(*)), 3) AS share_with_no_history
FROM per_reader;

share_with_no_history is the share of new subscribers with no reading linked to them in the 30 days before. A high value points at Safari readers without the ID endpoint, or at readers who subscribed on a second device.

Destinations

DestinationReceives
BigQuery (or another warehouse)Every event, including cancellations and reasons
Google AdsConsented subscription_started and trial_converted, three properties
Meta AdsConsented StartTrial and Subscribe, three properties
Mixpanel, Amplitude or PostHogFunnel events without email

What others in the industry do

Public write-ups from other publishers, cited as industry examples. They do not describe Jitsu.

  • Reach plc, which owns more than 200 UK publications, named 10 million registered users as its next target in December 2021 (Digiday interview).

Example implementation (illustrative)

Illustrative example: not a Jitsu customer. A digital magazine sells annual subscriptions with a 14-day trial and buys Google and Meta ads.

  • Web and apps: identify at sign-up and login, funnel events with track, no email in traits.
  • Billing: the backend sends subscription_started when a trial starts and trial_converted on the first annual charge, each with the anonymous ID and ad_consent stored at checkout.
  • Google Ads: the billing stream's connection carries ad-conversions with AD_PLATFORM=google-ads.
  • Meta Ads: the billing stream's connection carries ad-conversions with AD_PLATFORM=meta and receives StartTrial and Subscribe.
  • Warehouse: BigQuery receives every event, and the query above compares the reading history of new subscribers each week.

Where Jitsu does not fit

  • Jitsu is not a consent platform. Your backend records consent and sends it with each conversion; the function reads what you send.
  • Identity stitching on Jitsu Cloud needs the Enterprise plan, reaches 30 days back, and rewrites warehouse rows only.
  • Jitsu Cloud is hosted in the United States. The security page covers SOC 2, the DPA and the EU-U.S. Data Privacy Framework; pricing lists which plan includes what.

Questions people ask

Which events reach Google Ads and Meta? Only subscription_started and trial_converted, and only when ad_consent is true. They carry order_id, value and currency. On the Meta connection they are renamed StartTrial and Subscribe.

Does email reach the ad platforms? Not through this function. It removes context.traits from every event it passes, and the billing call on this page sends no email.

Where does identity stitching apply? Identity stitching rewrites rows in the warehouse only: ClickHouse, BigQuery, Postgres, Redshift, Snowflake and MySQL, up to 30 days back. It is part of Jitsu Cloud Enterprise and of self-hosted Jitsu.

What happens to readers who declined advertising consent? Their conversions carry ad_consent false, and the function drops them on the Google Ads and Meta connections. The warehouse still receives their events under the consent rules that govern the warehouse connection.

Get started with Jitsu

Get started on the free plan with 200k active events a month, or book a demo to talk through identity stitching and Enterprise terms.

Subscribe to the Jitsu newsletter
Occasional product updates when we ship something worth knowing about. No spam.