Sources
Connect Stripe and RevenueCat
The restricted-key permissions Stripe needs, the RevenueCat key and webhook, how often each syncs, and how a subscription in both is counted once.
Stripe and RevenueCat are where Joinsight gets money from. Everything else (pages, keywords, posts, store sources) is joined to the customers and MRR these two sources report, so connect at least one of them first. This page covers the exact access each one needs, what is imported and how often, and how a subscription visible in both is counted once. For the overview, see the Stripe and RevenueCat integration pages.
Only workspace owners can connect, replace or disconnect a billing source. Connecting the first one also starts the 14-day trial.
Stripe
Create a restricted key
Joinsight only accepts restricted keys. A secret key can move money; a restricted key with Read permissions can't create charges, refunds or customers, and you can revoke it in Stripe at any time.
On the Stripe card, the Create restricted key in Stripe button opens Stripe's key form with the name Joinsight (read-only) and the permissions below already set to Read. Check them before you create the key, and leave everything else at None.
| Permission | Why Joinsight needs it |
|---|---|
| Customers | User IDs, countries, hashed emails |
| Subscriptions | Current plans and statuses |
| Invoices | Historical MRR |
| Prices | Intervals and price changes |
| Products | Plan names |
| Coupons | Discounts on current plans |
| Events | Updates every 15 minutes |
| PaymentMethods | Card expiry dates |
| Charges | One-time payments and refunds |
Paste the key into the Restricted key field and select Connect Stripe. While the button reads "Checking permissions", Joinsight makes one small read per permission, so a missing one is named now rather than halfway through the import. The key is stored encrypted and never shown again.
If you paste the wrong kind of key, nothing is stored:
- A secret key (
sk_live_orsk_test_) is refused with a note to create a restricted key instead. - A publishable key (
pk_live_orpk_test_) is refused the same way. - Anything else that doesn't start with
rk_live_orrk_test_is refused as not a restricted key.
The card shows the Stripe account name when Stripe shares it. Reading it needs a permission Joinsight doesn't ask for, so with most restricted keys the card says "Name not shared with restricted keys". That is expected.
Charges is optional
Every permission except Charges is required. Without Charges, the connection still works and MRR is complete, but payments made outside invoices are left out: Checkout in payment mode, Payment Links and other direct charges. In other words, no one-time sales from Stripe.
Keys created before Joinsight imported one-time sales often lack it. In that case the card shows One-time sales aren't included. Edit the restricted key in Stripe, set Charges to Read, and the next sync imports the payment history.
Test keys and live keys
Both rk_test_ and rk_live_ keys are accepted. A test key gets a Test mode badge on the card.
Replacing the key with one for the other mode, or for another Stripe account, replaces the data. Before importing, Joinsight deletes what the previous key brought in: customers, subscriptions, payments and price annotations. It recognizes another account by checking whether the new key can still read the customers imported so far. A new key for the same account keeps the data and refreshes it.
To try Joinsight on test data and keep it, use a separate workspace for it.
What is imported
The first sync imports your full history: every price, customer and subscription (in any status), every paid and open invoice, and, with Charges, every payment. Progress shows on the card as it goes. Historical MRR is rebuilt from invoice lines, after discounts and excluding tax; prorations and $0 lines such as trials and 100% coupons don't count as MRR.
After that, Joinsight reads Stripe's event log every 15 minutes and refetches whatever the new events touched. Stripe keeps events for 30 days, so if a connection hasn't synced for more than 25 days, the next sync is a full re-import instead.
Payments that Stripe takes on behalf of a platform (for example RevenueCat Web Billing) are skipped on the Stripe side. They belong to that platform's numbers.
Set metadata.user_id at Checkout
Joinsight matches a Stripe customer to a signup by the user ID you pass to identify(). Put the same ID on the Stripe customer and the subscription, as metadata.user_id:
// Create the customer with your user ID, then start Checkout for it.
const customer = await stripe.customers.create({
email: user.email,
metadata: { user_id: user.id },
});
const session = await stripe.checkout.sessions.create({
mode: "subscription",
customer: customer.id,
line_items: [{ price: "price_123", quantity: 1 }],
subscription_data: { metadata: { user_id: user.id } },
success_url: "https://example.com/welcome",
});
One-time payments (mode: "payment") have no subscription: put the ID in payment_intent_data.metadata instead.
Customers without a user ID fall back to matching on email, which Joinsight hashes on its side. The user ID is more reliable, especially when someone signs up with one email and pays with another. Link visitors to customers with identify() covers the other half, and Stripe Checkout metadata goes deeper on the Stripe side.
RevenueCat
Create a secret API key
In RevenueCat, open Project settings, API keys and create a secret API key, version 2, with Read only access to:
- Customer information
- Project configuration
- Charts metrics
On the RevenueCat card, paste it into Secret API key (v2), and copy your Project ID from Project settings, General (it looks like proj1a2b3c4d). Select Connect RevenueCat. Joinsight checks the key against the project with one call per permission before storing anything. A public SDK key (starting appl_ or goog_) is refused, and a project ID RevenueCat doesn't know is flagged on its field.
Skip this card if you only sell on the web. RevenueCat is for in-app subscriptions from the App Store and Google Play.
Add the webhook
Once connected, the card shows a Webhook URL and an Authorization header value. In RevenueCat, open Integrations, Webhooks and add a configuration with both. The URL is unique to your workspace:
https://<your Joinsight host>/api/webhooks/revenuecat/<workspace id>
The header value has the form Bearer <secret>. Only workspace owners can see it. Joinsight compares it in constant time and rejects requests that don't match.
Without the webhook, RevenueCat data is still complete, but up to a day old. With it, renewals, cancellations, billing issues and new purchases arrive within a minute. Each event is stored once by its ID, so RevenueCat's retries are harmless, and test events and sandbox events change nothing. RevenueCat webhooks and MRR explains which events move MRR and why a cancellation isn't churn until the subscription expires.
Replacing the API key for the same project keeps the same webhook secret, so the configuration in RevenueCat keeps working. Connecting another project creates a new secret, so the old project's webhook stops being accepted, and the old project's imported data is deleted before the new one is imported. Disconnecting deletes the secret too: if you connect again later, update the header value in RevenueCat.
What is synced, and how often
RevenueCat syncs on three clocks:
- Hourly: RevenueCat's own MRR figure, used in the Sources reconcile check on the Revenue screen.
- Daily: a full pass over customers and their subscriptions, plus one-time purchases when your project sells any non-subscription products. Customers with nothing paid or subscribed are skipped.
- Live: webhook events, as they happen.
RevenueCat limits how fast its API can be read, so Joinsight paces its requests to stay under each documented limit. On a large project, the first import takes a while; progress shows on the card. Sandbox purchases are left out.
If the key can't read customer purchases, the card shows Past one-time purchases aren't included. New one-time sales still arrive through the webhook. Give the key read access to Customer information and the next daily sync imports the older ones.
Amounts and currencies
RevenueCat reports money in two ways, and Joinsight uses both:
- The API gives each subscription's lifetime revenue in USD, not its price. Until a webhook event arrives for it, a subscription's monthly amount is estimated as revenue minus tax, divided by the billing periods elapsed.
- Webhook events carry the exact price in the buyer's currency. Joinsight removes tax using the tax share RevenueCat sends, and the event's amount replaces the estimate.
Store fees are not subtracted. MRR is gross, before App Store and Google Play commission, the same as for Stripe.
User IDs from RevenueCat
RevenueCat's app user ID is used as the user ID, unless it's an anonymous RevenueCat ID. If your app logs users in with the same ID you pass to identify() and to Stripe, one person is one customer across web and mobile. The $email attribute, when set, is hashed and used for email matching.
Counted once across both
RevenueCat can track Stripe subscriptions too. When it does, the same subscription appears in both sources. Joinsight counts it once, from Stripe, matched on the Stripe subscription ID that RevenueCat stores. A Stripe payment that RevenueCat also records counts once, from Stripe, and a Stripe payment that pays an invoice counts once, from the invoice.
A person who pays on the web and in the app is one customer when the user IDs match, with their MRR summed. The full rule is on the definitions page. The Sources reconcile panel on the Revenue screen shows Stripe MRR, RevenueCat MRR excluding Stripe-billed subscriptions, the overlap left out, and the combined figure. Combined MRR for Stripe and RevenueCat explains the problem in general terms.
One-time sales
Payments outside subscriptions are reported as one-time revenue, never as MRR:
- Stripe Checkout and Payment Link payments (needs Charges)
- One-off items on Stripe invoices
- RevenueCat non-renewing purchases
Each sale counts on the day it was paid, net of refunds: a refund reduces the sale it refunds. Discounts are applied and tax is excluded where the source separates it (Stripe invoices, RevenueCat). Stripe payments outside invoices carry no tax breakdown, so they include any tax collected.
Currency conversion
Joinsight shows everything in the workspace's reporting currency, set in workspace settings (see Workspaces). It converts with the European Central Bank's reference rates, fetched daily. MRR uses the rate of the day each billing period started; one-time sales use the rate of the day paid. On a day without a published rate, such as a weekend, the latest earlier rate applies.
Because a period's rate is fixed when it starts, last month's MRR doesn't move when exchange rates do.
Why MRR can differ from Stripe or RevenueCat
Stripe's and RevenueCat's dashboards apply their own rules and exchange rates. When Joinsight's number differs, these parts of its definition are the usual reason:
- Annual plans are divided by 12 and weekly ones multiplied by 52 and divided by 12.
- Discounts are applied; tax, trials and one-time payments are excluded.
- Past-due subscriptions stay in MRR, flagged as at risk, until they cancel.
- Amounts are converted at the ECB rate of the day each period started, not today's rate.
- A subscription in both Stripe and RevenueCat counts once.
- RevenueCat subscriptions without webhook events yet carry an estimated amount, in USD.
The Sources reconcile panel prints RevenueCat's own MRR figure, in its own currency and including the overlap, so you can compare the two directly. Troubleshooting walks through a mismatch step by step.
Sync status and errors
Each card shows one status:
| Label | Meaning |
|---|---|
| Not connected | No credentials stored. |
| Waiting to sync | Connected, first sync queued. It starts within a minute. |
| Setup not finished | Connected, but nothing is picked to import yet, such as a Search Console property or an App Store app's reports. Nothing syncs until you finish. |
| Syncing | A sync is running. During the first import, a progress bar shows what is being imported. |
| Synced, with a time | The last sync succeeded, for example "Synced 4 minutes ago". |
| Sync failing | The last sync failed. Joinsight retries on its own. |
| Access revoked | The source rejected the credentials. Syncing has stopped until you reconnect. |
Below the card, Sync activity lists the last syncs with their result and duration. Owners can queue one with Sync now.
Error messages on the card come from the source itself or describe something you can fix, such as "The restricted key is missing the Invoices Read permission." If a sync fails for a reason on Joinsight's side, you see "Syncing failed because of a problem on Joinsight's side. It retries automatically." and the details stay in Joinsight's logs. That way a message on the card is always one you can act on.
A failed sync is retried with growing delays (30 seconds, 2 minutes, 8 minutes, and so on). When a source rate limits Joinsight, the retry doesn't use up an attempt, and it waits as long as the source asks, between 30 seconds and 6 hours. Rejected credentials are different: retrying won't help, so the connection moves to Access revoked and waits for a new key. A revoked source, or one that has been failing for 24 hours, also raises a data-health alert (see Alerts).
Disconnecting
Select Disconnect at the bottom of the card, then confirm with Disconnect Stripe or Disconnect RevenueCat. Joinsight deletes the stored key at once. A cleanup job that runs once a day then deletes the source's customers, subscriptions, payments, Stripe price annotations and stored webhook events, and any customer left with no source records, so the data is usually gone within a day and always within 30 days. Metrics are recomputed without it. If you connect again before the cleanup has run, the old data is deleted before the first import starts, so it never mixes with the new connection's.
Disconnecting doesn't touch your Stripe or RevenueCat account. Revoke the restricted key in Stripe, and the secret key and webhook in RevenueCat, yourself. More on what is stored and deleted is in Security and privacy.