Revenue: your payment webhooks
Hereling learns what your users pay through verified payment webhooks. Add your webhook address, configure its verification secret, and include your user id to match revenue to AI cost. No payment-provider API key is needed.
Your three addresses
Open your project in Hereling. Under Webhook addresses there is one address per provider, already made for you:
https://api.hereling.com/webhooks/stripe/<your token>
https://api.hereling.com/webhooks/paddle/<your token>
https://api.hereling.com/webhooks/revenuecat/<your token>The token at the end is a long random string that belongs to your project alone. Treat the whole address as private. It identifies your project, but does not authenticate a payment: every event must also pass verification. The address stays the same when you add or replace the secret.
Recorded revenue, currency and annual payments
Recorded revenue is payments minus refunds and disputes, plus reversals. Subtracting AI cost gives recorded revenue minus AI cost, not accounting profit or a payout balance. Provider totals may include tax. Taxes, payment/store fees and other operating expenses are not deducted; Hereling does not calculate separate tax, fee or net settlement totals.
This is a cash-event view: a $120 annual payment counts as $120 in its payment period, not $10 in each service month. A view that spreads revenue across service months is not available. Refunds and disputes count in their own event periods, including negative-revenue periods.
For non-USD payments, Hereling uses ECB reference rates stored in its database. A daily job imports the ECB reference file directly; the first run loads its full history. Payment webhooks make no external exchange-rate requests. A reference from the preceding seven days is accepted for weekends and holidays; future or older rates are rejected. Each event retains its original amount and conversion snapshot. A refund uses its own event-date rate, so its USD value can differ from the original payment. These reference rates are not the payment processor's settlement rates.
RevenueCat's explicit USD price is retained with RevenueCat as its source; its exchange-rate date is not disclosed and Hereling does not invent one. If the purchased-currency amount is missing, it stays missing. An explicit USD price can still be recorded when the purchase currency is unknown. Unsupported currencies without a provider USD price, missing amounts or unavailable rates leave revenue and comparisons Not yet calculated, with the known subtotal shown separately. The daily job automatically completes missing conversions when a reference becomes available; payment providers do not need to resend the event. Existing verified conversions are not repriced. Historical non-USD amounts remain included with an awaiting-rate notice until that job replaces them once using a dated reference; their previous values are preserved. Historical RevenueCat records are an exception: older versions could store USD in the original-amount field. These amounts remain included and require a verified replay of the original RevenueCat event; the daily job never converts them a second time. Currencies outside the ECB reference set (including AED and SAR) remain unavailable.
Subscribers from before you connected
Hereling learns about payments from the day you connect. A user who subscribed earlier shows no revenue until their next renewal arrives. Until then they are awaiting first payment: the Users tab says Not yet seen instead of $0.00, and they are not counted as losing money.
To show them correctly from day one, load your current subscribers once: connect a read-only payment sync key, or upload a subscriber list as CSV. Both are under your project's Settings. With the key, App Store and Google Play subscribers also get the payment for their current period recorded on the day it was paid.
Where to paste it
| Provider | Screen |
|---|---|
| Stripe | Developers → Webhooks → Add endpoint |
| Paddle | Developer tools → Notifications → New destination (Webhook) |
| RevenueCat | Project → Integrations → Webhooks |
You only need the one that matches your payment provider. Send us every event type if that is easier — the ones we do not use are accepted and dropped after verification.
Put your user id on every payment
This is the step that makes revenue and AI cost comparisons per user possible. Your payment provider knows its own customer id; Hereling needs your id for that person — the same one you send in the x-hereling-user header on AI calls.
| Provider | The field to fill in |
|---|---|
| Stripe | metadata.hereling_user (or client_reference_id on Checkout) |
| Paddle | custom_data.hereling_user |
| RevenueCat | app_user_id — already there if you identify users in the SDK |
Ids shaped like a UUID match in any letter case, so an iOS app that sends UUID().uuidString (uppercase) and a server that sends the same Supabase id (lowercase) still land on one user.
Stripe
Add it where you create the Checkout session. One-time payments read the session's own metadata; subscriptions need it on subscription_data as well, because Stripe copies that onto every future invoice and that is what keeps renewals matched.
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: "price_…", quantity: 1 }],
// one-time payments:
metadata: { hereling_user: user.id },
// subscriptions: copied onto every invoice, so renewals stay matched
subscription_data: { metadata: { hereling_user: user.id } },
success_url: "https://example.com/done",
});If the subscription was created some other way, put metadata.hereling_user on the subscription itself. A payment link with no code of your own can carry client_reference_id instead.
Paddle
Add it where you open the checkout. Paddle copies custom_data onto the transaction and the subscription, so renewals stay matched without anything else from you.
Paddle.Checkout.open({
items: [{ priceId: "pri_…", quantity: 1 }],
customData: { hereling_user: user.id },
});RevenueCat
Nothing to add, as long as you tell RevenueCat who the user is instead of letting it invent an anonymous id. Then use that same id in the x-hereling-user header.
Purchases.configure(withAPIKey: "…", appUserID: user.id)Webhook verification (required)
Stripe and Paddle sign webhook bodies; Hereling checks each signature with the secret for that exact endpoint. RevenueCat uses a shared Authorization header value instead. Open Settings → Webhook addresses, select Add, and save the matching value below. Verification is required for every delivery, including test events.
| Provider | The value to paste |
|---|---|
| Stripe | The endpoint's Signing secret, starts with whsec_ |
| Paddle | The destination's Secret key |
| RevenueCat | The Authorization header value you set on the webhook |
Enter the secret directly in Settings. Once saved, it is never returned to your browser or exposed through MCP; your project page only says whether one is set. Saving an empty value cannot remove an existing secret.
403 verification_not_configured and no revenue is recorded. After saving, send a provider test event and resend any failed deliveries from the provider's dashboard. A wrong or missing signature or Authorization value returns 400 bad_signature.What Hereling keeps
Numbers and labels only: the kind of event (payment, refund, dispute, reversal or subscription update), the amount in USD, the plan name, the provider's event id (so a retry is not counted twice), the provider's customer reference, payment/invoice/charge references used to link corrections, and the time. No emails, no names, no raw webhook bodies.
Refunds, disputes and test payments
Revenue includes payments minus successful refunds and withdrawn disputes, plus returned funds. A $100 payment with a $20 refund leaves $80. Corrections count on their own event date, so a period containing only refunds can show negative revenue. Canceling a subscription alone does not refund a payment.
Corrections are linked to the original payment by provider references. Retries count once, including refunds delivered before their payment. Until matching events arrive, unmatched corrections still affect the project total. Keep the original payment and reference events enabled; a customer id alone is not enough to identify which payment was refunded.
Stripe test-mode events and RevenueCat sandbox or Test Store purchases never enter revenue totals. For Paddle, select the secret's environment when saving it in Settings → Webhook addresses. Test connections accept verified events without recording revenue. Existing Paddle connections must save their secret again with an environment; otherwise deliveries return 403 environment_not_configured.
To check connectivity, send a test event and confirm successful delivery in your provider dashboard; the payment count should stay unchanged. Historical refunds are not fetched automatically: resend the original payment, reference events and successful refund events from the provider when backfilling.
Stripe details
| Stripe event | Becomes |
|---|---|
checkout.session.completed | purchase (a one-time payment, or the first payment of a subscription) |
invoice.paid | purchase for the first invoice, renewal for every later one |
customer.subscription.deleted | cancel |
checkout.session.async_payment_succeeded | purchase when a delayed payment succeeds |
refund.created / refund.updated | refund only when status is succeeded; enable both |
refund.failed | restore a recorded refund if the bank returns it; a failed pending refund adds no revenue |
charge.refunded | successful refunds included in the payload, deduplicated by refund id |
charge.dispute.funds_withdrawn / charge.dispute.funds_reinstated | withdrawn dispute / returned principal (fees excluded) |
charge.succeeded / invoice_payment.paid | reference links for matching corrections; no additional revenue |
Plan name: the price's nickname or lookup key in Stripe, or an explicit hereling_plan in the same metadata if you want a different label. A subscription started through Checkout sends two webhooks for the same first payment; we count it once, and Stripe's own retries once as well. Non-USD amounts use an ECB reference rate for the event date, read from the local ECB reference table. The original amount, rate, source, reference date and retrieval time are retained.
Always enable refund.created, refund.updated and refund.failed: a charge's embedded refund list may be missing or paginated. Its cumulative amount_refunded is never counted as a second refund. Pending, failed and canceled refunds do not reduce revenue. Enable both reference-link events above to connect invoices and Checkout payments to later corrections across Stripe API versions. Subscription Checkout and its invoice count once.
Paddle details
| Paddle event | Becomes |
|---|---|
transaction.completed | purchase, or renewal when it came from a recurring billing run |
subscription.canceled | cancel |
adjustment.created / adjustment.updated | approved refund or chargeback subtracts revenue; chargeback_reverse adds it back |
Plan name: the price name in Paddle, or an explicit hereling_plan in the same custom data. Amounts come from the transaction totals — what the customer was actually charged, after tax and discounts. Pending or rejected adjustments, credits on unpaid invoices, and chargeback warnings do not change recorded cash revenue. Enable both adjustment events because some adjustments start approved.
RevenueCat details
| RevenueCat event | Becomes |
|---|---|
INITIAL_PURCHASE, NON_RENEWING_PURCHASE | purchase |
RENEWAL | renewal |
CANCELLATION | refund when cancel_reason is CUSTOMER_SUPPORT; otherwise a zero-amount cancel |
EXPIRATION | expiration |
REFUND_REVERSED | returned refund amount |
Plan name: the first entitlement id, or the product id when there is none. Amounts are already in USD from RevenueCat and are used as they come, with the purchased-currency amount kept alongside.
Sandbox events are accepted but not counted as revenue, while the endpoint's "last event" time still updates — so a sandbox purchase is a safe way to prove the webhook is wired up. Set a long, random Authorization header value on the webhook and save the same value in Hereling. This is required; the webhook URL alone is not enough.
Checking it worked
After saving the secret, send a test delivery from your payment provider and check that it receives a 200 response. Refresh Settings and confirm that Last event advances. An older timestamp does not verify a newly saved secret. For RevenueCat, use its TEST event or a sandbox purchase; neither creates revenue. Stripe test events also leave revenue unchanged. For Paddle, use a destination explicitly configured as a test connection in Hereling.
Connect and the MCP server show recorded payment counts, including historical events. Those counts alone do not confirm your current verification setup. A verified event that Hereling ignores can return 200 and update Last event without increasing the payment count.