Webhooks

This is how revenue becomes attributed commissions. Two sources today: RevenueCat (mobile subscriptions) and Stripe (web SaaS billing).

RevenueCat

In the RevenueCat dashboard → Integrations → Webhooks:

  • URL: https://api.myappaffiliate.com/webhooks/revenuecat/<appId>
  • Authorization header: the secret printed by onboarding (whsec_…)

The API compares the Authorization header against the SHA-256 we stored for your app (constant-time). A bad token is recorded and rejected with 401.

Match the user

Your app must call RevenueCat logIn(userId) with the same id you pass to AffiliateSDK.identify(userId:), so the webhook's app_user_id matches the attribution. And set the affiliate_id subscriber attribute before the purchase (see iOS SDK).

Event mapping

RevenueCat eventNormalized typeCommissionable?
INITIAL_PURCHASEpurchaseyes
INITIAL_PURCHASE (trial)trial_startno ($0; counts in funnel)
RENEWALrenewalyes
NON_RENEWING_PURCHASEpurchaseyes
CANCELLATIONcancelno
EXPIRATIONexpirationno
REFUNDrefundno
others (BILLING_ISSUE, PRODUCT_CHANGE, …)ignored

Stripe

For web SaaS billing — the full flow is in the Web SaaS guide.

In the Stripe dashboard → Developers → Webhooks → Add endpoint:

  • URL: https://api.myappaffiliate.com/webhooks/stripe/<appId>
  • Events: invoice.paid, charge.refunded, customer.subscription.deleted, checkout.session.completed
  • Signing secret: copy the endpoint's whsec_… secret into your MyAppAffiliate app settings (onboarding prompts for it). Every delivery is verified against the Stripe-Signature header (timestamped HMAC, replay-safe) before anything is processed.

Metadata requirement

Attribution joins on metadata.customer_user_id. Set it — to the same id you used with identify(userId) / trackSignup — on the subscription (via subscription_data.metadata at checkout) and on the checkout session for one-time payments. Invoices without it are stored but earn no commission.

Event mapping

Stripe eventNormalized typeCommissionable?
invoice.paid (billing_reason: subscription_create)purchaseyes
invoice.paid (billing_reason: subscription_cycle)renewalyes
charge.refundedrefundno
customer.subscription.deletedcancelno
checkout.session.completed (mode: payment)purchaseyes

Reliability (both sources)

  • Persist-before-process: the raw payload is stored before parsing, so a parser bug never loses a financial event.
  • Idempotent: a re-fired webhook with the same event id produces no second event or commission (unique (source, sourceEventId)).
  • Commission: amount_usd_cents × rate_bps / 10000 (rate snapshotted), attributed to the most recent referral within the attribution window, created pending then matured after the hold period.

Test it

Without a device, rehearse the whole loop against the live DB:

Shell
pnpm --filter @maa/api smoke

For Stripe, use the Stripe CLI: stripe listen --forward-to https://api.myappaffiliate.com/webhooks/stripe/<appId> and trigger stripe trigger invoice.payment_succeeded in test mode.