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 event | Normalized type | Commissionable? |
|---|---|---|
INITIAL_PURCHASE | purchase | yes |
INITIAL_PURCHASE (trial) | trial_start | no ($0; counts in funnel) |
RENEWAL | renewal | yes |
NON_RENEWING_PURCHASE | purchase | yes |
CANCELLATION | cancel | no |
EXPIRATION | expiration | no |
REFUND | refund | no |
| 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 theStripe-Signatureheader (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 event | Normalized type | Commissionable? |
|---|---|---|
invoice.paid (billing_reason: subscription_create) | purchase | yes |
invoice.paid (billing_reason: subscription_cycle) | renewal | yes |
charge.refunded | refund | no |
customer.subscription.deleted | cancel | no |
checkout.session.completed (mode: payment) | purchase | yes |
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, createdpendingthenmaturedafter the hold period.
Test it
Without a device, rehearse the whole loop against the live DB:
pnpm --filter @maa/api smokeFor 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.