Revenue Connectors · Stripe

When to connect Stripe to Admaxxer (and why)

Stripe is the right revenue source for any business not running entirely on Shopify Payments. If your money lives in Stripe Dashboard — directly, or alongside Shopify — connect Stripe. Setup auto-registers the webhook (no manual dashboard step) using the rak_webhook_write scope on a restricted key, then signs every payload via the Stripe-Signature header and stitches visitors via metadata.admaxxer_visitor_id.

Open Revenue Settings Back to all connectors

When should I connect Stripe?

Stripe is the right revenue source for any business not running entirely on Shopify Payments. If your money lives in Stripe Dashboard — directly, or alongside Shopify — connect Stripe.

Connect Stripe if you're one of these five

  1. 01

    SaaS / subscription business

    MRR/ARR is your headline metric, not GMV. Shopify can't track recurring revenue.

  2. 02

    Digital products

    Courses, ebooks, downloads, software licenses. Stripe Checkout, Payment Links, no inventory.

  3. 03

    Agency or services

    Invoiced revenue, retainers, project fees. Stripe is the source of truth — Shopify isn't in the loop.

  4. 04

    Hybrid Shopify + Stripe

    Physical goods on Shopify plus a separate Stripe for upsell subscriptions, digital add-ons, or B2B billing.

  5. 05

    Custom storefronts

    Webflow, Framer, Next.js, custom React, Astro — anything using Stripe Checkout, Payment Links, or Subscriptions directly.

When Shopify alone is enough

If all your revenue runs through Shopify Payments, you don't need to connect Stripe separately. Orders, refunds, and Shopify Payments chargebacks all stream automatically via the Shopify connection's webhooks. Shopify Payments is built on Stripe under the hood, but Shopify exposes the data — not Stripe directly. See GL#284 and the Shopify Custom App walkthrough.

Why Stripe attribution beats the Stripe Dashboard alone

You already see gross revenue in your Stripe Dashboard. Five things change once Admaxxer is in the loop:

  1. Real-time

    Auto-registered webhook fires charge.succeeded to Admaxxer in under 500ms. Stripe Dashboard updates have polling delays.

  2. Refund-aware cohort LTV

    rak_refund_read lets us net out refunds when computing 7/30/90-day cohort revenue. Stripe Dashboard shows gross revenue only.

  3. Net revenue after Stripe fees

    rak_balance_transaction_read powers accurate MER (revenue ÷ ad spend) by subtracting Stripe's 2.9% + 30¢ before the calculation.

  4. Cross-channel attribution

    Every charge is joined to the visitor's first-touch ad source via the admx_visitor_id metadata field. Stripe Dashboard cannot do this — it has no concept of marketing source.

  5. Coupon and promo cohort separation

    rak_coupon_read distinguishes full-price first-orders from discount-chasers. Critical for cohort LTV accuracy.

Prerequisites

Step 1 — Create a Stripe Restricted Key (12 reads + 1 write)

Admaxxer requires a Stripe Restricted Key (prefix rk_live_* or rk_test_*) — never a Secret Key (sk_*). Restricted Keys cap blast radius if leaked: read-only on data, single write scope on webhook endpoints.

  1. In the Stripe dashboard, go to Developers › API keys › Restricted keys › + Create restricted key.
  2. Name it Admaxxer Revenue.
  3. Grant the 13 permissions below (12 reads + 1 write). Set every other resource to None. The single write scope — rak_webhook_write — is what authorizes Admaxxer to auto-register the revenue webhook on your account so you don't have to paste a URL into the Stripe dashboard yourself.
Permission Type Used for
rak_charge_read Read Attributed conversions
rak_checkout_session_read Read Match pixel sessions to checkouts
rak_payment_intent_read Read Detect successful payment flows
rak_subscription_read Read MRR / ARR tracking
rak_invoice_read Read Recurring invoice reconciliation
rak_refund_read Read Net out refunds (cohort LTV accuracy)
rak_customer_read Read Link revenue to the right visitor
rak_event_read Read Server-side stream backfill
rak_balance_transaction_read Read Net revenue after Stripe fees (MER)
rak_product_read Read Categorize revenue
rak_price_read Read Product-level ROAS
rak_coupon_read Read Promo / discount cohort separation
rak_webhook_write Write Auto-register webhook for real-time revenue (no manual dashboard step)
  1. Click Create key. Copy the rk_live_... value.
  2. Paste the key into Admaxxer › Revenue Settings › Stripe. The next second: the key is encrypted with AES-256-GCM at rest, every scope is validated, and the webhook endpoint is auto-registered on your Stripe account using rak_webhook_write — no manual dashboard step.

Step 2 — Webhook auto-registers (no manual setup needed)

Admaxxer auto-registers a webhook on your Stripe account when you connect. The following events stream in real-time:

Verify the auto-registered endpoint

You can confirm the endpoint exists in your Stripe Dashboard › Developers › Webhooks. Look for an endpoint named:

Admaxxer revenue attribution (auto-registered)

If you ever need the URL directly (for debugging, or to mirror events to a second internal endpoint), it follows this pattern:

https://admaxxer.com/api/pixel/webhooks/stripe/<YOUR_WEBSITE_ID>

Why auto-registration matters

Stamp the visitor ID on every checkout

This is the step that turns a Stripe payment into an attributed revenue event. The Admaxxer pixel exposes window.admaxxer.getVisitorId() for client-side reads, and a server-side cookie/header fallback for server-driven checkouts.

Stripe Checkout (recommended)

// Server-side (Node.js + stripe-node)
const session = await stripe.checkout.sessions.create({
  mode: 'payment',
  payment_method_types: ['card'],
  line_items: [/* ... */],
  metadata: {
    admaxxer_visitor_id: req.cookies['admx_vid']
      || req.headers['x-admx-vid']
      || ''
  },
  success_url: 'https://example.com/thanks',
  cancel_url: 'https://example.com/cart',
});

Payment Intents (custom integration)

const intent = await stripe.paymentIntents.create({
  amount: 4900,
  currency: 'usd',
  metadata: {
    admaxxer_visitor_id: req.cookies['admx_vid'] || ''
  },
});

The pixel writes the visitor ID to a 1p cookie (admx_vid) on first page view. Your server reads that cookie when building the Stripe object. If the user pays cross-device, the visitor ID is also recoverable from the x-admx-vid request header set by the pixel on every fetch.

Step 3 — Verify the connection

  1. In Stripe, go to Developers › Webhooks, select the auto-registered Admaxxer revenue attribution endpoint, and click Send test webhook.
  2. Choose checkout.session.completed and send.
  3. In Admaxxer, open Revenue Settings. The "Last 50 webhook deliveries" panel should show the test with status ok.
  4. Run a real test purchase in Stripe test mode. Inspect the resulting Checkout Session in Stripe and confirm metadata.admaxxer_visitor_id is non-empty.
  5. Within 3 seconds, the order should appear in the Admaxxer attribution dashboard, joined to the visitor's acquisition source.

Subscription lifecycle events (SaaS funnel)

If you run a SaaS, free-trial, or recurring-billing business, the four customer.subscription.* events are where your funnel lives — none of them move money, so they don't show up in revenue tiles. Admaxxer turns each one into a custom goal so you can chart trial starts, trial-to-paid conversion, churn, and the 3-day trial-end warning alongside your acquisition data.

What Admaxxer subscribes to

Admaxxer's auto-registered Stripe webhook listens to these seven events:

The first three (checkout.session.completed, payment_intent.succeeded, charge.succeeded) write rows to the visitor_payments datasource — they're the cash-moving events that drive your revenue tile, MER, and cohort LTV. The four customer.subscription.* events write to the your custom-goals store datasource — they're funnel signals that surface in the existing custom-goals tile and feed cohort/MMM filters.

Event → goal mapping

Stripe event & status Custom goal What it means
customer.subscription.created (status=trialing) Trial Started When a customer starts a free trial — no money has moved yet.
customer.subscription.created (status=active) Subscription Created When a paid subscription begins immediately (no trial).
customer.subscription.updated (trialing → active) Trial Converted The headline SaaS conversion — trial flipped to paid. We detect this via previous_attributes.status === 'trialing' on the update event.
customer.subscription.deleted Subscription Cancelled Customer churned. Fires once at end-of-life, regardless of whether cancellation was immediate or at-period-end.
customer.subscription.trial_will_end Trial Ending Soon Stripe fires this 3 days before trial_end. Useful for triggering rescue email sequences and last-chance ads.

Goal payload — what each row carries

Every subscription-lifecycle goal is fired with the following metadata so downstream pipes (cohorts, MMM, trial-conversion forecasts) have everything they need without an extra Stripe API call:

How MRR is computed

Stripe stores per-billing-period unit amounts (e.g. $120/yr as unit_amount=12000, recurring.interval='year'). Admaxxer normalises that to a clean monthly figure so your dashboards never show "$120 MRR" for an annual plan. The canonical formula:

Stripe intervalMRR contribution
month / 1unit_amount
year / 1unit_amount / 12
week / 1unit_amount × 4
day / 1unit_amount × 30
multi-period (interval_count = N)divide the result by N (e.g. quarterly = month / 3 → year / 12 / 3 = year / 36? No — Stripe's quarterly is month, interval_count=3, so MRR = unit_amount / 3).

Mixed-interval plans (some customers monthly, some annual) are common and supported — every event independently computes its own MRR, then dashboards aggregate.

Signups vs. Trials vs. Conversions: the SaaS funnel walkthrough

This is the funnel WarmySender, every B2B SaaS, and most free-trial digital products care about. Here's how Admaxxer turns Stripe events into the four steps of the funnel:

  1. Step 1 — Signup (no Stripe event)

    Account creation typically isn't a Stripe event. Fire a custom goal client-side from your app: window.admaxxer('Signup') the moment the user submits their signup form. Pair the visitor_id from the pixel with the signup so attribution chains back to the ad click.

  2. Step 2 — Trial Started

    When you create a Stripe Subscription with trial_period_days or an explicit trial_end, Stripe fires customer.subscription.created with status='trialing'. Admaxxer writes a Trial Started goal carrying the plan_id and projected MRR. No money has moved.

  3. Step 3 — Trial Ending Soon (optional rescue trigger)

    3 days before trial_end, Stripe fires customer.subscription.trial_will_end. Admaxxer writes a Trial Ending Soon goal — useful for retargeting audiences and rescue email triggers via webhook fan-out.

  4. Step 4 — Trial Converted (the headline)

    When the trial ends and the first invoice succeeds, Stripe fires customer.subscription.updated with previous_attributes.status='trialing' and the new status='active'. Admaxxer detects this transition and writes a Trial Converted goal. Now the cash event (invoice.paidpayment_intent.succeeded) writes the actual MRR to visitor_payments. The goal + the payment row share customer_id so cohort joins are clean.

  5. (Step 5 — Subscription Cancelled)

    If the customer churns, Stripe fires customer.subscription.deleted and Admaxxer writes a Subscription Cancelled goal. Combined with the original Trial Started timestamp, this gives you precise lifetime in days, churn cohort, and MRR-loss attribution back to the original acquisition channel.

Every goal carries the same customer_id, so a single analytics pipeline can join Trial Started → Trial Converted → Subscription Cancelled rows by customer to compute trial-to-paid conversion rate, average days in trial, and ad-source-attributed MRR — metrics no SaaS team can derive from the Stripe Dashboard alone.

Troubleshooting

Webhook returns 401 invalid_signature
The signing secret on file no longer matches Stripe's. Rare with auto-registration but happens if someone manually clicks Roll secret on the Admaxxer-managed endpoint. Fix: disconnect and reconnect Stripe in Site settings › Revenue › Stripe — Admaxxer will re-register the endpoint and capture the fresh whsec_*.
Order appears as unattributed
metadata.admaxxer_visitor_id was empty on the Stripe object. Common causes: the pixel hasn't fired yet (consent banner blocking), the cookie was stripped by a SameSite=Strict policy on a cross-domain checkout, or your server isn't reading the cookie when building the Checkout Session. Inspect the Stripe object in the dashboard and confirm metadata is populated.
Connection health says "Last event >24h ago"
Either no charges have happened, or the auto-registered webhook was deleted or disabled in your Stripe Dashboard. Open Stripe › Developers › Webhooks and confirm the endpoint named Admaxxer revenue attribution (auto-registered) is still present and enabled. If it's missing, disconnect and reconnect Stripe — the endpoint will be re-created.
API key error: "Invalid permissions"
Your Restricted Key is missing one or more of the 13 required scopes. The 12 reads are charge, checkout_session, payment_intent, subscription, invoice, refund, customer, event, balance_transaction, product, price, coupon — all Read. The 1 write is webhook_endpoint Write (used for auto-registration). Go to Stripe › Developers › API keys › click your key › Edit permissions and re-grant any scope set to None.
Stripe says "webhook_endpoint write permission is required"
Your Restricted Key was created without rak_webhook_write. Without it, Admaxxer can't auto-register the endpoint. Edit the key in Stripe › Developers › API keys, find Webhook Endpoints in the resource list, and set it to Write. Save, then re-paste the key in Admaxxer — registration retries automatically.

Frequently asked

Do I need Stripe if I'm on Shopify Payments?
No — Shopify Payments revenue is captured via the Shopify connection's order + refund webhooks. Connect Stripe only if you have non-Shopify Stripe revenue (subscriptions, digital products, agency invoices, custom storefronts). Shopify Payments is built on Stripe under the hood, but Shopify exposes the data — not Stripe directly — so the Shopify connection is the right surface for that revenue.
Is the webhook setup automatic?
Yes — Admaxxer auto-registers the webhook on your Stripe account the moment you paste your restricted key. No manual dashboard step is needed. The rak_webhook_write permission on the restricted key is what enables this; we create exactly one endpoint named 'Admaxxer revenue attribution (auto-registered)' subscribed to checkout.session.completed, payment_intent.succeeded, charge.succeeded, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, and customer.subscription.trial_will_end.
Why does Stripe attribution beat Stripe Dashboard alone?
Cross-channel ad-source attribution (every charge joined to the visitor's first-touch ad via admx_visitor_id), refund-aware cohort LTV (we net out refunds when computing 7/30/90-day cohorts), net revenue after fees (so MER subtracts Stripe's 2.9% + 30¢ before the calculation), real-time webhook (sub-500ms), and coupon-cohort separation (full-price first-orders vs. discount-chasers). Stripe Dashboard has none of these — it shows gross revenue, no marketing source, and no cohort math.
Do I need a full Stripe API key, or can I use a Restricted Key?
Always a Restricted Key — never a Secret Key. Admaxxer needs 13 scopes total: 12 reads (charge, checkout_session, payment_intent, subscription, invoice, refund, customer, event, balance_transaction, product, price, coupon) and 1 write (webhook_endpoint, used to auto-register the revenue webhook on your account). Set every other resource to None.
How does Stripe-Signature verification work?
Stripe signs every webhook with HMAC-SHA256 using your endpoint's signing secret (whsec_...). The Stripe-Signature header carries a timestamp plus the signature; Admaxxer rebuilds the signed payload (timestamp + . + body), HMACs it with the signing secret, and compares constant-time. Mismatches return 401 immediately. The 5-minute timestamp window is enforced to block replay attacks. The signing secret is paired automatically when Admaxxer auto-registers the endpoint, so you never have to copy/paste it.
Where do I put metadata.admaxxer_visitor_id?
Two places, depending on which Stripe API you use: (1) On Checkout Sessions, set metadata.admaxxer_visitor_id when calling stripe.checkout.sessions.create — it propagates to every downstream event. (2) On Payment Intents created directly, set metadata.admaxxer_visitor_id on the PaymentIntent. The Admaxxer pixel exposes window.admaxxer.getVisitorId() so your server can fetch the value when constructing the Stripe object.
What if I have refunds or chargebacks?
The auto-registered endpoint already subscribes to charge.refunded. We treat refunds as negative revenue events linked to the original charge by ID. Lost disputes are handled identically. Pending disputes show up flagged in the dashboard but don't subtract from MER until resolved. No additional configuration needed.
How does Admaxxer track free trials and trial-to-paid conversions?
Admaxxer subscribes to four customer.subscription.* events: created (status=trialing fires 'Trial Started'; status=active fires 'Subscription Created'), updated (a trialing → active transition fires 'Trial Converted' — detected via Stripe's previous_attributes.status field), deleted (fires 'Subscription Cancelled'), and trial_will_end (fires 'Trial Ending Soon' 3 days before trial_end). Each goal carries subscription_id, customer_id, plan_id, MRR in cents, currency, trial_end, current_period_end, cancel_at_period_end, canceled_at, and email_hash so funnel pipes can join across goals and revenue rows.
How is MRR computed for annual or weekly plans?
Stripe stores unit_amount per billing period (e.g. $120/yr = unit_amount=12000, interval=year). Admaxxer normalises to monthly: month/1 → unit_amount; year/1 → unit_amount / 12; week/1 → unit_amount × 4; day/1 → unit_amount × 30. Multi-period intervals divide by interval_count. So a $120/yr plan reports MRR = 1000 cents ($10), and a $30/quarter (month, interval_count=3) reports MRR = 1000 cents ($10). Mixed intervals across customers are fully supported — each event computes its own MRR independently.
What happens if the webhook is down or my server is offline?
Stripe automatically retries failed webhooks for up to 3 days with exponential backoff. Our endpoint is idempotent (deduped on Stripe's event id), so retries are safe. If you fix the issue within 72 hours, no revenue events are lost. Beyond 72 hours, you can backfill historical orders by hitting the manual sync button in /dashboard/site-settings#revenue — Admaxxer will paginate through your Stripe Events API and ingest anything missing.
Can I connect multiple Stripe accounts (e.g. one per brand)?
Yes — but each Stripe account maps to a separate Admaxxer site (one site, one pixel, one Stripe). To track multiple brands, create one workspace site per brand and connect each to its own Stripe account in /dashboard/site-settings#revenue. This keeps revenue attribution clean and avoids cross-brand contamination of your MMM and cohort LTV pipes.

Next steps