stubkit docs

Marketing · Optional

Google Ads + Meta server-side conversions

When you buy installs on Google Ads or Facebook, the ad networks need a signal back when those installs become paying users — otherwise their smart bidding models can’t tell good cohorts from bad ones. The traditional way is a browser pixel on a thank-you page, but that doesn’t exist for in-app subscriptions, and iOS 14+ ATT breaks the few signals that do reach pixels.

Stubkit fixes this end-to-end. It already receives every Apple, Google, and Stripe webhook the moment a purchase, renewal, or refund happens. With one OAuth click + a small SDK call, it forwards those events server-side to Google Ads (Enhanced Conversions API) and Meta (Conversions API) attached to the original ad click.

Two kinds of conversion events

Stubkit forwards two kinds of events to your ad networks. You map both in the same dashboard form.

KindFired byExamples
LifecycleStubkit, automatically, when an Apple/Stripe/Google webhook transitions the user’s subscription state.purchase, renewal, refund
CustomYour app, by calling stubkit.trackConversion(name, { userId, value }) from the SDK.tutorial_complete, trial_started, premium_upgrade, level_5_reached, anything you define.

The 3 lifecycle event names are reserved — you can’t override them via trackConversion(). Stubkit rejects any event_name that matches a lifecycle key with a 400. Pick any other identifier-shaped name (letters, digits, underscores; starting with a letter; ≤64 chars).

What you get

  • Real LTV signal for smart bidding instead of just first-touch “install.” Renewal events feed every month a subscriber stays active.
  • Refund adjustments sent automatically — Google Ads gets a RETRACTION, Meta gets a Refund event with negative value. Your bidder stops chasing churners.
  • iOS-safe: server-side bypasses ATT entirely. Hashed email + phone match users even with no IDFA.
  • No extra infra. Runs inside the same service that processes your webhooks. Adds <100 ms after the entitlement write.

How it works

  1. Your app captures the click ID (gclid from Google, fbclid from Facebook) at first launch and sends it to stubkit via captureAttribution().
  2. Stubkit stores it on the user, indexed by their external id.
  3. Later — minutes, days, or months later — Apple/Google/Stripe sends a webhook to stubkit (e.g. purchase, renewal, refund).
  4. The consumer worker writes the entitlement, then looks up your configured ad destinations for that app.
  5. For each destination, it pulls the user’s first attribution touch (with a click ID), maps the event type to your configured conversion action, and posts to the ad network’s server API.
  6. Result is logged under /integrations/ad/<id> with status (success / failed / skipped) so you can audit per-event.

Setup

Google’s Conversion API needs three pieces: a developer token, OAuth credentials so we can sign in on your behalf, and the conversion action ID(s) you want to fire.

  1. Developer token. In ads.google.com → Tools → API Center, request access if you don’t have a token yet. Note the token string.
  2. OAuth client. In Google Cloud Console → APIs & Services → Credentials, create an OAuth 2.0 Client ID. Type: Web application. Add this as an authorized redirect URI:
    https://api.stubkit.com/v1/admin/ad-destinations/oauth/google/callback
    Save the client ID and client secret.
  3. Conversion actions. In Google Ads → Tools → Conversions, create one conversion action per event you want to track (a typical setup uses one for “Subscription Purchase”). Set Source = Import, attribution model to your preference, value to Use different values for each conversion. Note the conversion action ID — it’s the long number in the URL when you open the conversion.
  4. In stubkit, go to /integrations Add Google Ads. Fill in:
    • Customer ID (10 digits, no dashes)
    • Login Customer ID — only if you sign in via an MCC manager
    • Developer token
    • OAuth client ID + secret
    • Event mapping — paste the conversion action ID for purchase, renewal, and refund (refund uses the same ID as purchase but fires a RETRACTION adjustment). Click Add custom event to map app-specific events like tutorial_complete or trial_started to their own conversion action IDs.
    Save — the destination is created in draft state.
  5. On the destination edit page, click Sign in with Google. Approve the read/write scope on https://www.googleapis.com/auth/adwords. You land back on stubkit with the destination flipped to active.

Meta Conversions API

Meta is simpler — there’s no OAuth dance. You just need a Pixel ID and a long-lived access token.

  1. In Meta Events Manager → Data Sources → your pixel → Settings → Pixel ID. Copy it (16-digit number).
  2. Same Settings page → Conversions API section → Generate Access Token. For production, use a System User token (under Business Settings → Users → System Users → Add). System user tokens don’t expire when an employee leaves.
  3. (Optional) Test event code. While you’re wiring this up, grab a code from Events Manager → Test Events tab and paste it into the stubkit form. Conversions will appear under that tab and won’t affect ad delivery. Clear the field to go live.
  4. In stubkit, /integrationsAdd Meta CAPI. Fill pixel ID + access token + (optional) test event code. Map your events:
    • purchasePurchase (Meta standard event)
    • renewalSubscribe or Purchase
    • refundRefund (sent with negative value)
    Save. Destination is active immediately.

Firing custom events from your app

Once a custom event name is mapped in the dashboard (e.g. tutorial_complete → 78901), call trackConversion() from your app at the moment the user completes that action:

// At the user's "tutorial complete" moment
await stubkit.trackConversion('tutorial_complete', {
  userId: currentUser.id,
  value: 4.99,        // optional — major currency units, e.g. dollars
  currency: 'USD',
});

// At trial start (different from purchase — Apple/Stripe webhooks call
// trial periods 'purchase' too, so use a custom event for finer signal)
await stubkit.trackConversion('trial_started', {
  userId: currentUser.id,
  value: 9.99,
  currency: 'USD',
});

// At a free milestone (no value)
await stubkit.trackConversion('reached_free_limit', {
  userId: currentUser.id,
});

For idempotent retries, pass idempotencyKey — your own internal event id is ideal:

await stubkit.trackConversion('premium_upgrade', {
  userId: currentUser.id,
  value: 19.99,
  currency: 'USD',
  idempotencyKey: `upgrade-${analyticsEventId}`,
});

If no destination has the event name in its mapping, the call is accepted and silently logged as skipped: no_mapping in the destination’s delivery log. No error to your app.

Capture click IDs from your app

Stubkit can only forward conversions for users it has a click ID for. Capture the click ID at first launch, before the user signs up.

Browser / Next.js

The SDK auto-reads ?gclid=, ?fbclid=, and Meta’s _fbp / _fbc cookies from the current page. Just call:

import { StubkitClient } from '@stubkit/js';

const stubkit = new StubkitClient({
  appId: 'your-app-id',
  publishableKey: 'pk_live_xxx',
  getAuthToken: async () => '...',
});

// Call once, after the user is identified
await stubkit.captureAttribution({
  userId: currentUser.id,
  email: currentUser.email,  // hashed before send for Enhanced Conversions
});

iOS / Android / React Native

Mobile apps don’t see the URL the user clicked. You have two options:

  • Deferred deep link via Branch / AppsFlyer / Adjust: they decode the original click URL and hand you back the gclid/fbclid on first install. Pass it in:
    await stubkit.captureAttribution({
      userId: currentUser.id,
      gclid: branchData.gclid,        // from your MMP
      fbclid: branchData.fbclid,
      email: currentUser.email,
    });
  • iOS App Tracking Transparency token + Google’s App Conversion API (different endpoint — not yet supported, on roadmap).

Privacy & PII

  • Email and phone are SHA-256 hashed at the API boundary (lowercase + trim for email, digits-only for phone). Stubkit never stores the plaintext.
  • Click IDs (gclid / fbclid) and Meta cookies are stored verbatim — they’re bound to the user only inside the ad network and aren’t personally identifying without a join key the network owns.
  • GDPR delete (DELETE /v1/admin/gdpr/delete/<user_id>) wipes all attribution rows for that user, so no further conversions fire for them.
  • Test-mode events (sk_test_ / pk_test_) are never forwarded to ad networks. Pollution of your smart bidding model would be irreversible.

Verifying it works

  • Open the destination edit page — recent deliveries appear below the form with status, event type, and the click ID used.
  • Set a Meta Test event code for the first run, then watch Events Manager → Test Events. Real Purchase events show up within ~10 seconds.
  • For Google Ads, use Diagnostics on the conversion action page → it shows recent uploads, including any rejected ones with the reason.
  • status: skipped (no_gclid_for_user) means the user has no captured click ID. Confirm captureAttribution() is firing in your app before the purchase flow.