How do I track Stripe subscriptions from my own Stripe account in RevenueDot?
Four steps: create a Stripe app in RevenueDot, save a restricted API key from your Stripe account, add a webhook endpoint in Stripe and save its signing secret, then have your backend post each purchase to POST /v1/receipts. Customers who pay on your website then get the same entitlements in your apps when they use the same app user ID. RevenueDot only reads from Stripe: it never charges, refunds or changes anything there.
1. Create the app#
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps" \
-H "Authorization: Bearer $SECRET_KEY" -H "Content-Type: application/json" \
-d '{"name":"Scanner (Web)","type":"stripe"}'The app's public key starts with strp_. Create its products with store_identifier set to the Stripe product ID (prod_…). To sell several prices of one Stripe product as separate products, use the price ID (price_…) instead; a price ID wins over its product ID. See Products and entitlements.
2. Save a restricted API key#
- In the Stripe Dashboard → Developers → API keys, click Create restricted key.
- Give it Read access to Subscriptions, Invoices, Checkout Sessions, Charges, Customers, Products and Prices. Leave everything else at None.
- In the dashboard, open the app → Stripe API key, paste it and click Check credentials. RevenueDot lists one subscription and one Checkout Session: a wrong key says so, and a key without a permission names the one it lacks.
With the API:
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps/$APP_ID" \
-H "Authorization: Bearer $SECRET_KEY" -H "Content-Type: application/json" \
-d '{"stripe":{"stripe_secret_key":"rk_live_…"}}'
curl -s -X POST "$REVENUEDOT_URL/v2/projects/$PROJECT_ID/apps/$APP_ID/actions/verify_credentials" -H "Authorization: Bearer $SECRET_KEY"A publishable key (pk_…) is refused. Using a Stripe Connect platform key for one connected account? Also save stripe_account_id (acct_…); RevenueDot sends it as Stripe-Account.
Test mode. The key decides the environment: a test-mode key (rk_test_…) or a Stripe sandbox records sandbox data, kept out of production numbers. Use one Stripe app per Stripe account, mode or sandbox, as RevenueCat recommends too.
3. Add the webhook endpoint#
- Copy the app's webhook URL,
https://revenuedot.example.com/v1/notifications/stripe/{app_id}, from the dashboard or fromstore_settings(notification_url). - In the Stripe Dashboard → Developers → Webhooks, add an endpoint with that URL and these events:
customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,customer.subscription.paused,customer.subscription.resumed,invoice.paid,invoice.payment_failed,invoice.updated,charge.refunded,checkout.session.completed. Other events are accepted and ignored. - Reveal the endpoint's Signing secret (
whsec_…) and paste it on the app page, or save it asstripe_webhook_secret.
Every event must carry a valid Stripe-Signature: RevenueDot computes the HMAC-SHA256 of "<t>.<body>" with the signing secret, the way Stripe documents it, and refuses a timestamp more than 5 minutes off. A missing secret or a bad signature answers 400. Each event is stored once (by event ID), copied to notification_forward_url when set, and applied by reading the subscription from Stripe again, so events arriving out of order cannot roll a subscription back. Temporary failures answer 500, and Stripe retries.
4. Post each purchase from your backend#
After Stripe confirms a purchase (customer.subscription.created or checkout.session.completed), post its subscription ID or Checkout Session ID with the customer's app user ID:
curl -X POST "$REVENUEDOT_URL/v1/receipts" \
-H "Authorization: Bearer strp_…" -H "X-Platform: stripe" -H "Content-Type: application/json" \
-d '{"app_user_id":"user_123","fetch_token":"sub_1Abc…"}'- A subscription ID (
sub_…) records the subscription. - A Checkout Session ID (
cs_…) records the session's subscription, or, for a payment-mode session, each line item as a one-time purchase (consumable or not, by your catalog). One-time purchases need a Checkout Session. - The answer is the customer's customer info. Posting the same ID again changes nothing.
- 400 (code 7103) means it can never succeed: an unknown ID, an expired session, a subscription whose first payment never completed. 5xx (code 7101) means try again later: Stripe was unavailable, the key was rejected, or the first invoice is not paid yet.
When a subscription counts#
By default a subscription counts once its latest invoice is paid. Set register_on to invoice_created (on the app page: "When the invoice is created") to grant access while the first invoice is still open, as RevenueCat's "Subscription purchase recognition" setting does.
Purchases you never posted#
Turn on Track new purchases from server-to-server notifications to record subscriptions and Checkout purchases that RevenueDot first hears about from a webhook. The customer is found by app_user_id_source:
metadata(default): the metadata keyapp_user_id_metadata_key(defaultapp_user_id) on the Checkout Session or the subscription. Set it in both when a session creates a subscription.customer_id: the Stripe customer ID becomes the app user ID.anonymous: an anonymous ID.
How Stripe states map to events#
| What Stripe reports | Event |
|---|---|
| An active subscription with a paid invoice | INITIAL_PURCHASE (TRIAL while trialing, at price 0) |
| A new paid invoice for the next period | RENEWAL (with is_trial_conversion after a trial) |
cancel_at_period_end or a future cancel_at; undone |
CANCELLATION (UNSUBSCRIBE); UNCANCELLATION |
past_due: the renewal invoice failed |
BILLING_ISSUE, CANCELLATION (BILLING_ERROR); access to the end of the paid period, grace until Stripe's next payment attempt |
Paid after past_due |
RENEWAL |
unpaid, or retries run out |
EXPIRATION (BILLING_ERROR) |
canceled (customer.subscription.deleted) |
CANCELLATION if not seen before, then EXPIRATION at ended_at |
pause_collection with resumes_at |
SUBSCRIPTION_PAUSED |
| Another price or product on the subscription | PRODUCT_CHANGE; a proration invoice in the middle of a period is not a renewal and adds no revenue |
charge.refunded in full for the latest period |
CANCELLATION (CUSTOMER_SUPPORT), a negative transaction, and access ends at the refund. A later paid period is a RENEWAL |
| A payment-mode Checkout Session | NON_RENEWING_PURCHASE; a full refund is CANCELLATION (CUSTOMER_SUPPORT) |
Webhooks report store: STRIPE with no store commission. Revenue is what each invoice charged (amount_paid), so coupons show in revenue; it is converted to USD at the period start's rate. Zero-decimal currencies such as JPY are not divided by 100.
Both Stripe API shapes work: before 2025-03-31 (period fields on the subscription, invoice.subscription) and after (period fields on the subscription item, invoice.parent.subscription_details).
Not supported yet#
- "Connect with Stripe" (OAuth) instead of a restricted key, and hosted checkout: these need RevenueDot's own Stripe platform and come later.
- Subscription schedules, metered and tiered prices, and subscriptions with several items.
- A scheduled re-check without webhooks: without the webhook endpoint, a cancellation shows only when your backend posts the subscription again.