Connect Stripe
Connect Stripe with a restricted key to read payments, refunds, disputes and subscriptions. Payment status comes from Stripe; a payment joins a visit only when matching evidence exists. Money summaries use USD only, without currency conversion.
1. Create a restricted key
In your Stripe dashboard open Developers → API keys and choose Create restricted key. The permissions below are named the way Stripe's key editor labels them. Only the first is required. Each of the others switches on one thing. When a sync finds the key refusing one of them, the settings card names that permission and says what stops without it.
- Charges: Read. Required. Payments, on the schedule.
- Webhook Endpoints: Write. Instant updates: DataStated creates a webhook in your account and payments, refunds, disputes and subscription changes land within seconds.
- Checkout Sessions: Read. The success-page match below, for sites that cannot set metadata at checkout.
- Refunds: Read and Disputes: Read. Refunds and disputes on the schedule. Not needed once instant updates are on.
- Subscriptions: Read and Invoices: Read. Subscriptions and renewals on the schedule. Not needed once instant updates are on.
- Customers: Read. The email match for subscription records, on the schedule and by webhook.
2. Paste it
Open your site's settings in the dashboard, choose the Revenue tab, find the Stripe card and paste the key into Restricted API key. Connect Stripe checks it against Stripe with one read of your charges, seals it, and never shows it again. Only a site owner can do this.
How payments arrive
The first sync after you connect creates the webhook, when the key allows it. The card then reads "Instant updates are on: a payment shows up here within seconds." Without the permission it reads "Instant updates are off, so payments arrive on the six-hour schedule", names the permission to add, and tries again after you paste a new key.
The schedule runs every six hours in either state. It pulls the charges created since the last pass and catches anything a webhook missed; the first pull reaches 90 days back. Sync now on the card runs the same pass, and so does the API:
curl -X POST -H "Authorization: Bearer dsk_YOUR_KEY" \
https://app.datastated.com/api/sites/acme.com/syncA charge counts when Stripe marks it succeeded and paid. A charge already fully refunded when first imported still enters gross revenue. Its successful refunds are separate deductions from net revenue; a fully refunded USD 100 charge therefore contributes USD 100 gross, USD 100 refunds and USD 0 net once both records are imported. Every row is keyed by the Stripe charge id, so a payment that arrives by webhook and by the pull is one order, and repeat imports of that charge do not add another order.
Large histories can take several passes. Charges, refunds, disputes, invoices and subscriptions keep separate page checkpoints. A bounded pass resumes its saved window on the next sync instead of advancing past unread pages. A sync response with partial: true means history is still being read, not that the account is fully reconciled. Later passes also revisit older records within the retained import range.
Tie payments to visits
Each order records which evidence joined it to a visitor. The API calls it match, and it takes one of five values, best first:
metadata: the PaymentIntent or Checkout Session carriedmetadata.datastated_visitor_id. Exact.client_reference: the Checkout Session'sclient_reference_idwas the visitor id. Exact.session_param: your success page handed the session id back with the visitor's own cookie. Exact for that browser; a purchase finished on another device is not seen.email: the payment's buyer email matched an earlier event on this site that carried the same email (hashed on both sides, never stored in the clear). Right when one person uses one address; two people sharing an address would be joined.none: no visitor match. The payment is still processor-confirmed, but no recorded marketing visit explains it.
The rungs can arrive in any order. A better one landing after the order was stored moves the order, and its refunds, to the real visitor and that visitor's standing credit.
Metadata at checkout
// In the browser: send window.datastated.visitorId to your server
await stripe.paymentIntents.create({
amount: 4900,
currency: "usd",
metadata: { datastated_visitor_id: visitorId },
});
// Checkout Sessions: either field works
await stripe.checkout.sessions.create({
client_reference_id: visitorId,
metadata: { datastated_visitor_id: visitorId },
});The success page, no code
Stripe Checkout sends the customer back to your success URL with ?session_id={CHECKOUT_SESSION_ID} filled in. When the tag finds a Checkout Session id on the page it lands on, it reports that id once per browser session, together with the visitor's own cookie, and leaves the URL as it is. The server records the claim, reads the session from Stripe with your own key, and only when Stripe says the session is paid does the settled charge become an order under that visitor, matched as session_param. The amount comes from Stripe, never from the page. An unpaid session leaves a claim that the payment fills in when it lands; an id Stripe does not know under your key is dropped and records nothing. Nothing on a URL is ever taken as money.
Without a Stripe key yet, or with a key that lacks Checkout Sessions: Read, the claim waits: the webhook's checkout.session.completed fills it in, and every six-hour pull re-reads waiting claims with the key, up to 50 per pass and for seven days. While the checkout-return session marker remains, a reload sends no second report. Withdrawal clears the active marker, so re-grant may report that success URL again; the server still deduplicates the charge. Stripe reads are throttled per site and per session, but a reported Checkout Session id is still untrusted until checked with Stripe.
Switch it off with data-disable-payments="true" on the snippet. If your success URL names the parameter differently, say so with data-checkout-param="checkout_session". Both are on the tag reference.
The success page, from your server
Prefer to hand the id over yourself? Send it with the visitor id from the tag to the events API. The key needs Checkout Sessions: Read; DataStated reads the session, joins it to the visitor, and pulls the settled charge in on the spot. The amount always comes from Stripe, never from the page.
curl -X POST https://app.datastated.com/api/sites/acme.com/events \
-H "Authorization: Bearer dsk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"stripe_checkout_session_id": "cs_live_...", "visitor_id": "THE_BROWSER_VISITOR_ID"}'The answer is {"stored": true, "match": "session_param", "order": "inserted"}, or {"stored": false, "match": "session_param", "order": "pending"} when the payment has not settled yet; the webhook or the next pass finishes it.
Record a signup or an order through the events API with an email field and a visitor_id. The address is hashed on arrival, and a later Stripe payment by the same address finds that visitor.
Refunds and disputes
A refund is its own row linked to the order and its visitor. Only a succeeded refund deducts money. Pending or action-required refunds carry no deduction; failed or canceled refunds carry zero. Later status updates reconcile that same refund id instead of adding another deduction. The order stays in gross revenue and is flagged fully refunded when successful refunds cover its value. A dispute has a held, won or lost state.
The window's USD-only totals carry revenue (gross, as before), refunds, disputed (money held by open disputes), disputesLost, and netRevenue, which is revenue minus refunds minus lost disputes. Each day of the series carries refunds and netRevenue too. With instant updates off the schedule needs Refunds: Read and Disputes: Read; until it has them the card says "Refunds are not being read, so refunded payments still count in revenue here", and the same for disputes.
Subscriptions
MRR is a site-wide subscription measure. It is hidden when a visitor filter is active, with mrrScope: "unavailable-filter" in the dashboard data. It does not measure recurring revenue from a selected acquisition source.
Each subscription's lifecycle is recorded as events of its own, derived from what Stripe says now against what it said last time: trial_started, trial_converted, subscription_started, subscription_renewed, subscription_upgraded, subscription_downgraded, subscription_cancel_scheduled, subscription_reactivated and subscription_ended. A renewal is a paid invoice whose billing reason is subscription_cycle, and the charge that paid it is flagged as a renewal.
The card's Count subscription renewals as revenue checkbox decides what renewals do to your totals. On, the default, every subscription payment counts. Off, a renewal order is left out of the window entirely, so revenue reads as new customers won. Renewals are recorded either way.
mrr is monthly recurring revenue at the range end, or now if that end is in the future. DataStated keeps a dated observation of each subscription's recurring amount and uses the latest observation available by the requested time. A newly observed price takes effect from that observation; today's price is not applied backwards to old periods.
The result is null when there are no relevant subscription records, a relevant subscription lacks an observation at that time, or its pricing cannot be calculated safely. Unsupported cases include discounts, tiered prices, incomplete price data and a positive recurring amount in a currency other than the dashboard's USD. No exchange rate or past price is guessed. Supported fixed recurring amounts are normalised to a month; usage-based amounts are excluded. A fully covered period can report zero, for example when its subscriptions are still in trial or have ended. The dashboard hides MRR when the result is unavailable.
Security
- The key and the webhook's signing secret are sealed with AES-256-GCM before they are stored, and the key is never rendered anywhere after you paste it.
- Every webhook delivery is checked against its signature before it is parsed, and each event is applied once.
- Buyer emails are hashed at the boundary. The address itself never lands.
- Only site owners can connect or replace the key. Roll it in Stripe at any time and paste the new one under Replace the key.
Questions? Email us at hello@datastated.com.