Paddle webhook alongside Stripe for hybrid billing

Route Paddle and Stripe checkout events into one revenue timeline without double-counting migrations, trials, or test mode.

All guides on this topic: Webhooks & API in one ROI view

You started on Stripe. EU customers asked for VAT sanity. A template line still sells on Gumroad. Now Paddle handles some checkouts and Stripe handles the rest, and your analytics shows either half the money or double the glory depending on which webhook you forgot.

Part of the webhook ROI guides. Read the main guide on unified webhook revenue first; this is the hybrid Paddle + Stripe wiring guide for founders who refuse a billing migration weekend.

Why hybrid billing persists

Common honest reasons:

  • Stripe for US subscriptions and complex billing objects.
  • Paddle as merchant of record for EU/UK tax simplicity.
  • Gumroad/Lemon for one-off digital SKUs you will not port.
  • Legacy Stripe customers while new EU rows go Paddle.

Marketing stays one domain; money leaves through multiple pipes. ROI view must aggregate pipes, not pick a favorite.

One normalization layer

Do not send Stripe and Paddle to different analytics projects unless you enjoy schizophrenia. Ingest both into one stream with shared fields:

source: stripe | paddle | gumroad | custom
external_id: provider event or transaction id
amount_minor: integer cents
currency: ISO code
event_type: new_sale | renewal | refund | subscription_started
is_test: boolean
occurred_at: timestamp
metadata: { utm_source, landing_path, plan, ... }

Map provider-specific names in code once. Dashboards read normalized rows only.

Stripe-specific habits

  • Listen to checkout.session.completed for new acquisition; exclude renewals via billing_reason or invoice flags (one-time vs subscription attribution).
  • Copy UTMs to Customer or Session metadata at create time (first-touch UTM).
  • Filter livemode: false aggressively.

Paddle-specific habits

  • Verify Paddle signature on webhook endpoint; reject replayed bodies.
  • Map transaction.completed (and equivalents) to your new_sale type; read Paddle docs for your API version, names drift.
  • Paddle as MoR means gross may already net tax; pick one reporting convention (gross checkout vs seller earnings) and stick in footnotes.
  • Trials and proration events differ from Stripe; maintain a translation table in repo, not in your head.

Double-counting traps

TrapFix
Customer pays Paddle after Stripe trialMigration script fires bothDedupe on customer_email + window or single subscription_id canonical
Refund in Stripe, chargeback in PaddleTwo negative eventsNegative amount_minor; same external_id family
Test purchases in live dashboardFilter is_testSeparate API keys per env
Internal admin compManual webhookTag event_type: comp excluded from RPV

Attribution when checkout hosts differ

Stripe Checkout may live on billing.yourapp.com; Paddle overlay may feel “off-site.” UTMs still work if marketing landings set cookies/sessionStorage and you copy tags into both providers’ metadata at redirect to payment.

If Paddle checkout cannot carry metadata, pass a passthrough or custom data field with session_id from your app; join server-side before posting analytics webhook.

Success page on your domain should still fire a pageview, ordering issues in pageview after webhook.

Reading hybrid revenue beside traffic

Overlay daily visitors with daily new revenue (all sources). Segment by source only in retro, not during firefighting.

Launch live view: one revenue feed sorted by time, Stripe and Paddle rows interleaved (live launch dashboard).

RPV with multiple currencies

Convert to one reporting currency at ingest (ECB daily rate) or split RPV per currency (messy). Micro-SaaS default: pick USD or EUR, convert on webhook, note assumption in weekly script output (weekly stats API script).

When to split projects instead

Split analytics projects only if:

Otherwise unified.

Migration week playbook

Before: Document which customers on which provider; freeze pricing changes.

During: Run both webhooks; tag events migration_stripe_to_paddle in metadata; exclude from acquisition charts or show separately.

After: Single source per cohort; archive old endpoint with 410 after 30 days replay window.

Pin migration week in marketing pulse guides so traffic dips from “we changed checkout” are not blamed on content.

Gumroad/Lemon as third pipe

Same normalization. Digital product ratios in Gumroad guides. Hybrid digital + SaaS is normal; source field scales.

Support tickets that hybrid billing creates

Customers do not care which MoR processed them; they email billing@ with one screenshot. Train support to note provider in ticket tags. Analytics then explains revenue spikes that do not match “we only changed the homepage.”

Common confusions:

  • Receipt brand says Paddle while login email mentions Stripe, migration cohort not documented.
  • VAT line items make Paddle gross look “higher” than Stripe net, reporting convention not shared with team.
  • Refund path differs by provider, negative webhook arrives days later; weekly script should not declare victory on day one.

A single ROI view reduces finger-pointing between “marketing broke checkout” and “engineering broke webhooks” because both see the same timestamped feed.

Load testing webhooks on rehearsal day

Replay a handful of signed sample payloads from Stripe and Paddle docs into staging ingest. Confirm:

  • Signature verification passes for both.
  • external_id dedupe works when Stripe test clock fires twice.
  • Paddle passthrough metadata survives the field mapping you chose.

Do not load-test production endpoints with imaginary money on launch morning. Rehearsal is boring; launch morning is not the time to learn HMAC.

Investor and board reporting

Board decks want one revenue line. Behind the slide, keep a by_source table for yourself. Explaining “Paddle is EU MoR” once is fine; explaining it every month because charts disagree is not. Hybrid stack is a business choice; unified webhook ingest is how you keep the story one sentence.

Checklist

  • One ingest endpoint, two verified signatures
  • Idempotency keys per provider
  • Renewal exclusion policy written
  • Test mode isolated
  • Metadata passthrough tested on Paddle + Stripe
  • Monday script sums source breakdown

Sandbox and production isolation

Stripe test mode and Paddle sandbox must never share an ingest endpoint with live traffic without loud labeling. Founders routinely demo “the dashboard” to a friend using test cards; if those events land in production aggregates, your launch retro is fiction. Use separate API keys, separate projects, or hard is_test filters enforced in code, not commented out “for now.”

Document which Paddle environment maps to which URL in the same README where you store webhook secrets. Six months later you will not remember.

Observability: one Slack channel for webhook health

Create #billing-ingest (private if needed). Post only machine messages: “stripe checkout.completed $49”, “paddle transaction.completed €39”, “dedupe skipped id xyz”. Humans mute the channel; watchers unmute launch week. When traffic surges and channel goes silent, you have a smoking gun before Twitter does. This complements unified ROI charts, feeds confirm pipes, charts confirm trends. Keep messages short; you are not building a second analytics product in chat.

Migration week

When moving legacy Paddle subs to Stripe, tag dual webhook week billing-migration in notes so acquisition metrics do not look like a launch. Keep both webhooks active until the last Paddle sub churns or migrates. Label revenue rows with processor=paddle|stripe in your head when you scan weekly totals.

Hub

Webhook ROI stack · Main guide

More on this topic