[{"data":1,"prerenderedAt":37},["ShallowReactive",2],{"doc-revenue":3},{"html":4,"headings":5},"\u003Cp>A project's \u003Cstrong>Revenue\u003C\u002Fstrong> tab shows what an app earns, next to its conversations:\nnet revenue and MRR for a period, the last twelve months, plans, top customers and\nrecent payments. It reads from every place you sell through. Connect as many\nsources as you have and the numbers add up; each one can be disconnected without\ntouching the others.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Source\u003C\u002Fth>\n\u003Cth>How it gets the data\u003C\u002Fth>\n\u003Cth>Brings\u003C\u002Fth>\n\u003Cth>Link to a contact\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"#stripe\">Stripe\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>HelpBites reads it with a restricted key, hourly\u003C\u002Ftd>\n\u003Ctd>Charges, subscriptions, MRR\u003C\u002Ftd>\n\u003Ctd>Stripe customer id, then email\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"#revenuecat\">RevenueCat\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>RevenueCat sends events to HelpBites\u003C\u002Ftd>\n\u003Ctd>Purchases, renewals, refunds, subscriptions, MRR\u003C\u002Ftd>\n\u003Ctd>The app's user id (the contact's external id), then email\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"#app-store\">App Store\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>HelpBites reads Apple's sales reports, hourly\u003C\u002Ftd>\n\u003Ctd>Daily proceeds per product\u003C\u002Ftd>\n\u003Ctd>None\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"#paddle\">Paddle\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>HelpBites reads it with an API key, hourly\u003C\u002Ftd>\n\u003Ctd>Transactions, refunds, subscriptions, MRR\u003C\u002Ftd>\n\u003Ctd>Email\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"#lemon-squeezy\">Lemon Squeezy\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>HelpBites reads it with an API key, hourly\u003C\u002Ftd>\n\u003Ctd>Orders, invoices, subscriptions, MRR\u003C\u002Ftd>\n\u003Ctd>Email\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ca href=\"#custom-api\">Custom API\u003C\u002Fa>\u003C\u002Ftd>\n\u003Ctd>You send it\u003C\u002Ftd>\n\u003Ctd>Whatever you send\u003C\u002Ftd>\n\u003Ctd>Email, or the app's user id\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Google Play has no source of its own yet: RevenueCat reports Google Play purchases\n(and the App Store's), and anything else can be sent to the custom API.\u003C\u002Fp>\n\u003Cp>HelpBites only reads from these services. The one write it ever makes is a pricing\ntable's Stripe Checkout session, and only with a Stripe key you gave that right\n(\u003Ca href=\"\u002Fdocs\u002Fpricing\">Pricing tables\u003C\u002Fa>).\u003C\u002Fp>\n\u003Ch2 id=\"what-the-numbers-mean\">What the numbers mean\u003Ca class=\"heading-anchor\" href=\"#what-the-numbers-mean\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The period is \u003Ccode>30d\u003C\u002Fcode>, \u003Ccode>90d\u003C\u002Fcode>, \u003Ccode>12m\u003C\u002Fcode> or \u003Ccode>all\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Number\u003C\u002Fth>\n\u003Cth>What counts\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Net revenue\u003C\u002Ftd>\n\u003Ctd>Payments that succeeded in the period, less what was refunded.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>MRR\u003C\u002Ftd>\n\u003Ctd>Subscriptions that are active or past due, at their list price a month. A trial or a lapsed subscription is not recurring revenue yet.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Subscribers\u003C\u002Ftd>\n\u003Ctd>Active, past due and trialing subscriptions, and how many started or ended in the period.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Paying customers\u003C\u002Ftd>\n\u003Ctd>People who paid in the period. A payment that names nobody (an App Store sale) is not a customer.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Cstrong>Currency.\u003C\u002Fstrong> Totals are in the currency with the most revenue; payments in any\nother currency are listed under the totals and left out of them. RevenueCat reports\na USD price whatever the customer paid in, so its amounts are always USD.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Gross, net and proceeds differ by source.\u003C\u002Fstrong> Stripe, Paddle and Lemon Squeezy count\nwhat the customer was charged (tax included, as a Stripe charge is), less refunds.\nRevenueCat counts its USD price before the store's commission and tax. The App Store\ncounts Apple's proceeds, which are after both. Mixing sources therefore mixes these;\nthe \u003Cstrong>By source\u003C\u002Fstrong> list on the tab shows each source's share so you can read them\napart.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Where a payment goes.\u003C\u002Fstrong> A source's money is kept under that source. Disconnecting\none forgets its credentials and the payments and subscriptions it brought; the others\nare kept. Contacts keep any \u003Ccode>stripe_*\u003C\u002Fcode> attributes a Stripe sync wrote.\u003C\u002Fp>\n\u003Ch2 id=\"stripe\">Stripe\u003Ca class=\"heading-anchor\" href=\"#stripe\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Connect it with a restricted key (Stripe → Developers → API keys → Create restricted\nkey) that can read Charges, Customers, Subscriptions and Products. HelpBites copies\nthe account's charges and subscriptions hourly (\u003Cstrong>Sync\u003C\u002Fstrong> runs it now; \u003Cstrong>Sync\neverything again\u003C\u002Fstrong> reads the whole history again, to pick up refunds on old\ncharges). A summary of each customer is written on their contact as \u003Ccode>stripe_*\u003C\u002Fcode>\nattributes, which the People filters, segments and campaigns can use, and a person's\npage looks them up in Stripe live. Those attributes, filters and the live lookup are\nStripe's only.\u003C\u002Fp>\n\u003Ch2 id=\"revenuecat\">RevenueCat\u003Ca class=\"heading-anchor\" href=\"#revenuecat\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>RevenueCat posts its events to HelpBites as they happen. Choose \u003Cstrong>RevenueCat\u003C\u002Fstrong> under\nRevenue → Add a source; HelpBites shows two values to paste into RevenueCat\n(Project settings → Integrations → Webhooks):\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Webhook URL\u003C\u002Fstrong>: \u003Ccode>https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fwebhooks\u002Frevenuecat\u002F&lt;project id&gt;\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Authorization header value\u003C\u002Fstrong>: a secret HelpBites made. A request without it is\nanswered 401. \u003Ccode>Bearer \u003C\u002Fcode> in front of it is accepted. Only people who manage the\nproject see it; disconnecting and connecting again makes a new one.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Choose production events, save, and use RevenueCat's \u003Cstrong>Send test event\u003C\u002Fstrong>: it shows\nunder the source as the last event.\u003C\u002Fp>\n\u003Cp>What the events do:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Event\u003C\u002Fth>\n\u003Cth>Effect\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>INITIAL_PURCHASE\u003C\u002Fcode>, \u003Ccode>RENEWAL\u003C\u002Fcode>, \u003Ccode>NON_RENEWING_PURCHASE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A payment (its USD \u003Ccode>price\u003C\u002Fcode>), and for subscriptions an active subscription priced a month from the period's length. A free trial is a trialing subscription with no payment until it converts.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>CANCELLATION\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Renewal is off; the subscription runs to the end of the period. A refund (\u003Ccode>cancel_reason\u003C\u002Fcode> \u003Ccode>CUSTOMER_SUPPORT\u003C\u002Fcode>, negative price) refunds the payment it names.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>UNCANCELLATION\u003C\u002Fcode>, \u003Ccode>SUBSCRIPTION_EXTENDED\u003C\u002Fcode>, \u003Ccode>PRODUCT_CHANGE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Renewal back on, the period moved, the plan renamed.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>BILLING_ISSUE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Past due; it still counts towards MRR until it expires.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>EXPIRATION\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Ended, in the month it ran out.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>REFUND_REVERSED\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Takes the refund back.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Limits: RevenueCat cannot replay the past, so \u003Cstrong>history starts when the webhook\ndoes\u003C\u002Fstrong>. For earlier months send payments to the \u003Ca href=\"#custom-api\">custom API\u003C\u002Fa>. Sandbox\npurchases and event types that carry no revenue (\u003Ccode>TEST\u003C\u002Fcode>, \u003Ccode>TRANSFER\u003C\u002Fcode>, …) are\nacknowledged and ignored. An event that arrives after a newer one for the same\nsubscription is ignored for its state. A subscription that HelpBites first sees on a\nrenewal counts in the MRR by month from that renewal on. An introductory price counts as\nthe price until the next renewal brings the real one. Plans are named by RevenueCat's\nproduct id.\u003C\u002Fp>\n\u003Cp>People are matched to contacts by the App User ID (the contact's external id, which\nis the user id your app signs in with), else by the \u003Ccode>$email\u003C\u002Fcode> subscriber attribute.\nSet RevenueCat's \u003Ccode>$email\u003C\u002Fcode> attribute from your app to link anonymous purchases.\u003C\u002Fp>\n\u003Ch2 id=\"app-store\">App Store\u003Ca class=\"heading-anchor\" href=\"#app-store\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Reads the Sales and Trends reports from App Store Connect. Create an API key (Users\nand Access → Integrations → App Store Connect API) that can read Sales and Reports,\nand enter its \u003Cstrong>Issuer ID\u003C\u002Fstrong>, \u003Cstrong>Key ID\u003C\u002Fstrong>, your \u003Cstrong>Vendor number\u003C\u002Fstrong> (Payments and\nFinancial Reports) and the \u003Cstrong>private key\u003C\u002Fstrong> (the \u003Ccode>.p8\u003C\u002Fcode> file's text, pasted whole;\nApple lets you download it once).\u003C\u002Fp>\n\u003Cp>HelpBites reads one daily report at a time, up to the last 365 days (the most Apple\nkeeps), then each hour the newest days, again three days back because Apple revises\nrecent days. A day's report is published the next day. Amounts are \u003Cstrong>your proceeds\u003C\u002Fstrong>\n(after Apple's commission and tax), one payment per product and currency per day;\nrefunds are their own negative lines and subtract.\u003C\u002Fp>\n\u003Cp>Limits: Apple's reports name no customers, so there are no top customers, no contact\nlinks, no subscribers and no MRR from this source. Use RevenueCat as well if you\nneed those. A day Apple later removes keeps its old row.\u003C\u002Fp>\n\u003Ch2 id=\"paddle\">Paddle\u003Ca class=\"heading-anchor\" href=\"#paddle\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Create a server-side API key (Paddle → Developer tools → Authentication) that can\nread transactions, subscriptions, customers and adjustments. A key made in the\nsandbox connects the sandbox. HelpBites reads paid and completed transactions\n(\u003Ccode>grand_total\u003C\u002Fcode>, tax included), refunds from approved adjustments, and subscriptions\nwith their plan and price.\u003C\u002Fp>\n\u003Cp>Limits: refunds on transactions older than 35 days are picked up by \u003Cstrong>Sync everything\nagain\u003C\u002Fstrong>. Credits and chargebacks are not counted as refunds.\u003C\u002Fp>\n\u003Ch2 id=\"lemon-squeezy\">Lemon Squeezy\u003Ca class=\"heading-anchor\" href=\"#lemon-squeezy\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Create an API key (Lemon Squeezy → Settings → API). HelpBites reads orders (which\ncover every payment, a subscription's first included), subscription invoices after the\nfirst, and subscriptions, with the plan and price taken from the variant. Test-mode\norders and free ones are left out.\u003C\u002Fp>\n\u003Cp>Limits: tiered and usage-based prices count 0 towards MRR; there is no date filter\non Lemon Squeezy's lists, so each run reads the newest pages back to 35 days.\u003C\u002Fp>\n\u003Ch2 id=\"custom-api\">Custom API\u003Ca class=\"heading-anchor\" href=\"#custom-api\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>For anything with no connector: Google Play's earnings report, Gumroad, PayPal, a bank\ntransfer, invoices you send by hand, or revenue from before RevenueCat's webhook.\nSend payments and subscriptions to one route with an admin API key\n(Settings → API keys; a key limited to the project is enough). The source connects\nitself on first use.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -X\u003C\u002Fspan> POST https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Frevenue\u002Fpayments \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Authorization: Bearer ak_…\"\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -H\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"Content-Type: application\u002Fjson\"\u003C\u002Fspan> \\\n \u003Cspan class=\"shj-syn-kwd\"> -d\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-syn-str\">\"project\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"PROJECT_ID\"\u003C\u002Fspan>,\n    \u003Cspan class=\"shj-syn-str\">\"payments\"\u003C\u002Fspan>: [\n      \u003Cspan class=\"shj-syn-oper\">{\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"id\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"invoice-1042\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"amount\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">19.99\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"currency\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"usd\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"paidAt\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"2026-03-01T10:00:00Z\"\u003C\u002Fspan>,\n        \u003Cspan class=\"shj-syn-str\">\"email\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"ada@example.com\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"description\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"Pro, yearly\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">}\u003C\u002Fspan>\n    ],\n    \u003Cspan class=\"shj-syn-str\">\"subscriptions\"\u003C\u002Fspan>: [\n      \u003Cspan class=\"shj-syn-oper\">{\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">\"id\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"member-7\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"plan\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"Pro\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"amount\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-num\">4.99\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"currency\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"usd\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"interval\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"month\"\u003C\u002Fspan>,\n        \u003Cspan class=\"shj-syn-str\">\"status\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"active\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"email\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"ada@example.com\"\u003C\u002Fspan>, \u003Cspan class=\"shj-syn-str\">\"startedAt\"\u003C\u002Fspan>: \u003Cspan class=\"shj-syn-str\">\"2026-01-05T00:00:00Z\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-oper\">}\u003C\u002Fspan>\n    ]\n  \u003Cspan class=\"shj-syn-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">'\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-js\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F Node, on your server: never put an admin key in a browser or an app.\n\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">fetch\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'https:\u002F\u002FYOUR-SUPPORT-HOST\u002Fv1\u002Fadmin\u002Frevenue\u002Fpayments'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> {\n  method\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'POST'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n  headers\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> { authorization\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">`Bearer \u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">${\u003C\u002Fspan>process\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>env\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-num\">HELPBITES_ADMIN_KEY\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> 'content-type'\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'application\u002Fjson'\u003C\u002Fspan> }\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan>\n  body\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">stringify\u003C\u002Fspan>({ project\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">PROJECT_ID\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> payments\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> [{ id\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> sale\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> amount\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> sale\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>total\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> currency\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'usd'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> paidAt\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> sale\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>date\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> email\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> sale\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>email }] })\n})\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>There is no browser or Swift snippet for this one on purpose: it takes an admin key,\nwhich must stay on a server. Rules:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Up to 500 payments and 500 subscriptions per request. \u003Ccode>id\u003C\u002Fcode> is yours; sending it\nagain \u003Cstrong>updates\u003C\u002Fstrong> that row, so a script can run twice.\u003C\u002Fli>\n\u003Cli>Amounts are decimals in the currency's units (\u003Ccode>19.99\u003C\u002Fcode>, or \u003Ccode>500\u003C\u002Fcode> yen), never cents.\n\u003Ccode>currency\u003C\u002Fcode> is a three-letter code.\u003C\u002Fli>\n\u003Cli>A payment: \u003Ccode>id\u003C\u002Fcode>, \u003Ccode>amount\u003C\u002Fcode>, \u003Ccode>currency\u003C\u002Fcode>, and optionally \u003Ccode>paidAt\u003C\u002Fcode> (ISO 8601 or epoch\nseconds; now by default), \u003Ccode>email\u003C\u002Fcode>, \u003Ccode>customerId\u003C\u002Fcode>, \u003Ccode>description\u003C\u002Fcode>, \u003Ccode>receiptUrl\u003C\u002Fcode>,\n\u003Ccode>refunded\u003C\u002Fcode> (the part given back, same units) and \u003Ccode>status\u003C\u002Fcode> (\u003Ccode>succeeded\u003C\u002Fcode> counts; any\nother is kept but adds nothing).\u003C\u002Fli>\n\u003Cli>A subscription: \u003Ccode>id\u003C\u002Fcode>, \u003Ccode>plan\u003C\u002Fcode>, \u003Ccode>amount\u003C\u002Fcode> (per period), \u003Ccode>currency\u003C\u002Fcode>, \u003Ccode>interval\u003C\u002Fcode>\n(\u003Ccode>day\u003C\u002Fcode>, \u003Ccode>week\u003C\u002Fcode>, \u003Ccode>month\u003C\u002Fcode> or \u003Ccode>year\u003C\u002Fcode>), \u003Ccode>status\u003C\u002Fcode> (\u003Ccode>active\u003C\u002Fcode>, \u003Ccode>trialing\u003C\u002Fcode>, \u003Ccode>past_due\u003C\u002Fcode>,\n\u003Ccode>paused\u003C\u002Fcode>, \u003Ccode>canceled\u003C\u002Fcode>, …), and optionally \u003Ccode>intervalCount\u003C\u002Fcode>, \u003Ccode>email\u003C\u002Fcode>, \u003Ccode>customerId\u003C\u002Fcode>,\n\u003Ccode>startedAt\u003C\u002Fcode>, \u003Ccode>canceledAt\u003C\u002Fcode> and \u003Ccode>endedAt\u003C\u002Fcode>. Only \u003Ccode>active\u003C\u002Fcode> and \u003Ccode>past_due\u003C\u002Fcode> count towards\nMRR. A canceled one with no \u003Ccode>endedAt\u003C\u002Fcode> ended when it was canceled, else now.\u003C\u002Fli>\n\u003Cli>A bad row answers 400 and names it (\u003Ccode>payments[3].amount: must be a number\u003C\u002Fcode>);\nnothing is saved from that request.\u003C\u002Fli>\n\u003Cli>A person is linked to a contact by \u003Ccode>email\u003C\u002Fcode>, or by \u003Ccode>customerId\u003C\u002Fcode> equal to the\ncontact's external id.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"linking-people\">Linking people\u003Ca class=\"heading-anchor\" href=\"#linking-people\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>A customer opens their contact from Top customers and Recent payments, and a\nperson's page lists their payments and subscriptions from \u003Cstrong>every\u003C\u002Fstrong> source, found by\ntheir Stripe customer id, the app's user id or their email.\u003C\u002Fp>\n\u003Ch2 id=\"the-api\">The API\u003Ca class=\"heading-anchor\" href=\"#the-api\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>All of these take an operator session or an admin key and are scoped to the\nworkspace like the rest of \u003Ccode>\u002Fv1\u002Fadmin\u003C\u002Fcode>; the OpenAPI reference at \u003Ccode>\u002F_docs\u002Fscalar\u003C\u002Fcode> has\nthe schemas.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Route\u003C\u002Fth>\n\u003Cth>What it does\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GET \u002Fv1\u002Fadmin\u002Frevenue\u002Fsources?project=\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The connected sources and how each last ran; RevenueCat's webhook URL and (for people who manage the project) secret.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>POST \u002Fv1\u002Fadmin\u002Frevenue\u002Fconnect\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ project, source, credentials }\u003C\u002Fcode>. \u003Ccode>apiKey\u003C\u002Fcode> for Stripe, Paddle and Lemon Squeezy; \u003Ccode>issuerId\u003C\u002Fcode>, \u003Ccode>keyId\u003C\u002Fcode>, \u003Ccode>vendorNumber\u003C\u002Fcode>, \u003Ccode>privateKey\u003C\u002Fcode> for Apple; nothing for RevenueCat and custom. A source HelpBites reads is tried first (502 when the service refuses the credentials).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>POST \u002Fv1\u002Fadmin\u002Frevenue\u002Fsync\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ project, source, reset? }\u003C\u002Fcode>: a run now, for Stripe, Apple, Paddle and Lemon Squeezy.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>DELETE \u002Fv1\u002Fadmin\u002Frevenue?project=&amp;source=\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Disconnects one source and forgets what it brought.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>GET \u002Fv1\u002Fadmin\u002Frevenue\u002Freport?project=&amp;range=\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The report the tab shows, over every source, with \u003Ccode>sources\u003C\u002Fcode> saying what each brought in the period. \u003Ccode>null\u003C\u002Fcode> until something has been paid.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>POST \u002Fv1\u002Fadmin\u002Frevenue\u002Fpayments\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The \u003Ca href=\"#custom-api\">custom API\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>POST \u002Fv1\u002Fwebhooks\u002Frevenuecat\u002F&lt;project id&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Where RevenueCat posts; authenticated by its Authorization header.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>GET \u002Fv1\u002Fadmin\u002Foverview\u003C\u002Fcode> and the workspace Overview add up each project's sources, each\nproject in its own main currency (\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Felrumo\u002Fhelp-bites-oss\u002Fblob\u002Fmain\u002Fdocs\u002FOVERVIEW.md\" target=\"_blank\" rel=\"noopener\">Overview\u003C\u002Fa>). The older \u003Ccode>\u002Fv1\u002Fadmin\u002Fstripe\u003C\u002Fcode>\nroutes still work for Stripe alone.\u003C\u002Fp>\n\u003Ch2 id=\"native-app\">Native app\u003Ca class=\"heading-anchor\" href=\"#native-app\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Revenue has the same screens in the iPhone, iPad and Mac app: connect a source, the\nsetup sheet for RevenueCat and the custom API, a status line per source, sync and\ndisconnect, and the report with its by-source list.\u003C\u002Fp>\n",[6,10,13,16,19,22,25,28,31,34],{"id":7,"text":8,"level":9},"what-the-numbers-mean","What the numbers mean",2,{"id":11,"text":12,"level":9},"stripe","Stripe",{"id":14,"text":15,"level":9},"revenuecat","RevenueCat",{"id":17,"text":18,"level":9},"app-store","App Store",{"id":20,"text":21,"level":9},"paddle","Paddle",{"id":23,"text":24,"level":9},"lemon-squeezy","Lemon Squeezy",{"id":26,"text":27,"level":9},"custom-api","Custom API",{"id":29,"text":30,"level":9},"linking-people","Linking people",{"id":32,"text":33,"level":9},"the-api","The API",{"id":35,"text":36,"level":9},"native-app","Native app",1792766843063]