Stripe to Snowflake: how to load payments data without paginating the API by hand
9 min read Data engineering The Adapters team
Last updated July 2026
Field mapping auto-plugged · tap a port to rewire
Plug a source port into
Transform on this cable
JSON in
JSON out
5 sample records ready
To move Stripe data into Snowflake, read the Stripe objects you care about (charges,
balance_transactions, payouts, invoices, subscriptions, customers) through the paginated API, land
each batch in a staging table, and MERGE into a target table keyed on the Stripe object
id. Track a watermark on created or updated so each run pulls
only new records, convert amounts out of minor units, and cast Unix timestamps to
TIMESTAMP_NTZ in UTC. The fee and net live on the balance_transaction, not the charge,
so plan to join them.
Key takeaways
- Fees live on the balance_transaction. Join it to the charge or you will report gross, not net revenue.
- Amounts are in minor units. Divide by 100 for most currencies, but JPY and KRW are already whole units, so a blanket divide is a bug.
- MERGE on the id and dedupe first. A watermark plus
ROW_NUMBER()makes retries idempotent instead of duplicating rows. - Batch your merges. Snowflake bills warehouse compute, so frequent tiny loads keep a warehouse resident and quietly raise the bill.
How do you connect Stripe to Snowflake?
You connect Stripe to Snowflake by reading Stripe objects through the paginated REST API and writing them into Snowflake tables, because there is no native Snowflake destination inside Stripe. Something sits in the middle: a managed connector, a scheduled script against the API, or Stripe's own data pipeline export.
The Stripe API returns 100 objects per page and walks forward with a starting_after
cursor, and it enforces rate limits (roughly 100 read requests per second in live mode). That
matters the moment you backfill. Pulling several years of charges by hand means thousands of
sequential page requests, careful cursor bookkeeping, and retry logic for the calls that get
throttled or time out. It is slow and brittle, which is exactly why most teams do not hand-roll
it past the first prototype.
Practically, the connection has three parts. First, a restricted Stripe API key with read scope
on the objects you replicate. Second, Snowflake landing: a warehouse, a database and schema, and
a role that can create tables and run MERGE. Third, the transfer itself, which
should page through each object type, land raw JSON or typed columns in a staging table, and
merge into the target. The managed path is the
Stripe to Snowflake integration,
which owns the pagination, the watermark, the staging table, the MERGE, and the retries. If you
are also pulling your application database, the same pattern runs through the
Postgres to Snowflake
connector, and the CRM side is covered in
sync Salesforce to Snowflake.
How do you load Stripe data into Snowflake incrementally?
Load Stripe incrementally by keeping a watermark of the last created or
updated value you saw, requesting only newer objects on each run, and MERGE-ing the
result into Snowflake keyed on the object id. You can also replay the
events stream from your last cursor when you need every state change, not just the
current row.
There are two ways to get changed records. Filtering on created is simple and works
for append-heavy objects like charges and balance_transactions. For objects that mutate after
creation (subscriptions, invoices, disputes), the events stream is more honest: it
emits a record every time something changes, so you catch the update to a subscription that was
created last year. Store the last event id or timestamp as your cursor and resume from it.
Whichever source you use, the load into Snowflake follows the same shape. Land the batch in a staging table, then collapse duplicates before you merge, because a single batch can carry several versions of the same object (an invoice that went from draft to open to paid). Keep the newest version per id:
| Step | What runs | Why it matters |
|---|---|---|
| Dedupe the batch | ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated DESC), keep rank 1 |
Stops a multi-version batch from creating duplicate target rows |
| MERGE on id | MERGE INTO target USING staging ON target.id = staging.id |
Makes retries idempotent, so a re-run after a timeout produces the same state |
| Advance the watermark | Store the max created / updated or last event id |
Next run resumes exactly where this one stopped |
Overlap the window slightly on each run (re-pull the last few minutes) so an object committed out of clock order is not skipped. Because the MERGE is keyed on the id and idempotent, re-reading a handful of rows costs nothing and buys you correctness.
How do you calculate Stripe fees and net payouts in Snowflake?
Calculate real fees and net revenue by joining balance_transactions to the charge,
not by reading the charge alone. The charge tells you the gross amount, but the processing fee
and the net amount that actually settles live on the balance_transaction, along with the link to
the payout that swept it into your bank account.
Every charge, refund, and dispute produces a balance_transaction. That row carries three numbers
you want: amount (gross), fee (what Stripe kept), and net
(what you actually earned). It also carries a payout reference once the money is
paid out. So the finance-grade model is a join: charge to balance_transaction on the charge's
balance_transaction id, then balance_transaction to payout on the
payout id. Sum net grouped by payout to reconcile a single bank deposit
down to the individual charges that composed it.
Two traps here. First, all three amounts are in minor units, so divide by 100 before you sum, and respect zero-decimal currencies (more on that below). Second, refunds and disputes create their own negative balance_transactions, so net revenue for a period is the sum of every balance_transaction, positive and negative, not just the charges. If your dashboard shows gross charges as revenue, it is overstating the number by the entire Stripe fee line plus refunds.
How do you build MRR from Stripe data in Snowflake?
Build MRR from active subscriptions and their invoice line items, normalizing every billing interval to a monthly amount and excluding one-time charges. A yearly plan contributes its annual price divided by twelve, a monthly plan contributes its price directly, and canceled or past-due subscriptions drop out. Then reconcile the total against Stripe's own reporting.
The mechanics: read subscriptions for status and the current plan, and read invoice
line items for the amount and interval actually billed. For each active subscription, convert the
recurring amount to a monthly figure based on its interval (month stays as is,
year divides by 12, week multiplies by roughly 4.33). Sum across active
subscriptions and you have committed MRR. Keep one-time invoice items, setup fees, and proration
adjustments out of the recurring number, or your MRR will jump every time someone upgrades
mid-cycle.
Handle discounts and currency explicitly. A 20% coupon reduces the recurring amount, so apply it before you roll up. If you sell in multiple currencies, convert to a single reporting currency at a defined rate rather than adding raw amounts. Finally, reconcile: your computed MRR should track Stripe's billing dashboard within a small margin. When it drifts, the usual culprits are trials counted as active, canceled subscriptions still summed, or an interval you forgot to normalize.
How do Stripe fields map to Snowflake types?
Map Stripe ids to VARCHAR, amounts in minor units to NUMBER after
dividing by 100, Unix timestamps to TIMESTAMP_NTZ in UTC, and the nested
metadata object to VARIANT so you can query keys without flattening
first. The two that cause silent errors are amounts (minor units, and zero-decimal currencies)
and timestamps (epoch seconds, not a date string).
Start with the object-to-table layout, then the field types.
| Stripe object | Snowflake table | Notes |
|---|---|---|
| charge / payment_intent | STRIPE_CHARGES | Gross amount and status. Join to balance_transaction for fee and net |
| balance_transaction | STRIPE_BALANCE_TRANSACTIONS | Holds fee, net, and the payout link. The source of truth for real revenue |
| payout | STRIPE_PAYOUTS | One bank deposit. Reconcile against summed balance_transaction net |
| refund | STRIPE_REFUNDS | Creates a negative balance_transaction. Subtract from period revenue |
| invoice | STRIPE_INVOICES | Line items drive MRR. Separate recurring items from one-time charges |
| subscription | STRIPE_SUBSCRIPTIONS | Status and interval. Filter to active when computing MRR |
| customer | STRIPE_CUSTOMERS | Join key for everything. Watch for deleted customers flagged in the object |
| Stripe field / type | Snowflake type | Notes |
|---|---|---|
| amount (minor units) | NUMBER(18,2) | Divide by 100 for most currencies to get major units. Never store money as FLOAT |
| currency (ISO code) | VARCHAR(3) | Lowercase in Stripe (usd, jpy). Drives whether you divide by 100 at all |
| zero-decimal amount (JPY, KRW) | NUMBER(18,0) | Already whole units. Do not divide by 100, or you shrink revenue 100x |
| created / updated (Unix seconds) | TIMESTAMP_NTZ | Epoch seconds. Use TO_TIMESTAMP_NTZ(created) and treat as UTC |
| id (ch_, pi_, in_) | VARCHAR | The natural MERGE key. Stable and unique per object |
| metadata (key-value object) | VARIANT | Store raw so you can query metadata:order_id without a reload |
| status (enum-like) | VARCHAR | New values appear over time, so validate downstream rather than constraining |
| livemode (boolean) | BOOLEAN | Keep it so test-mode rows never leak into finance reports |
The zero-decimal case is worth repeating because it is a real financial bug, not a rounding nuisance. A JPY charge of 5000 means 5000 yen, not 50. If your loader divides every amount by 100, Japanese revenue silently reads at one percent of actual. Branch on the currency code, or store the minor-unit value untouched and apply the divisor in a reporting view where the logic is visible and testable.
How much does it cost to sync Stripe to Snowflake?
Snowflake bills on two axes: warehouse compute measured in credits, and storage for the bytes you keep. Stripe volumes are usually modest, so the driver is not row count, it is how often you run merges. Frequent tiny loads keep a warehouse from ever suspending, and a resident warehouse burns credits whether or not there is work to do.
Batch sensibly. A warehouse that wakes every minute to merge four new charges costs far more than one that wakes every fifteen minutes to merge sixty, for the same data. Let the warehouse auto-suspend quickly (60 seconds is common) and size it small; Stripe merges are light. Storage is a rounding error at Stripe scale, so optimize compute first.
On the pipeline side, our pricing is flat by plan (Starter $49, Growth $149, Scale $399, Enterprise custom) rather than metered per row, so a heavy sales month does not produce a surprise invoice. See pricing for the full breakdown. One adjacent note: if part of your revenue picture lives on a marketing site or portal rather than in the Stripe API, you can turn a website with no API into clean, structured data and land it alongside your Stripe tables. To see the exact object mapping and schedule against your own Stripe account, start a demo.
Sync Stripe into Snowflake with fees, payouts, and MRR already reconciled
Paginated reads, watermark loads, dedupe, MERGE, and retries. Map the pair in the browser and pay a flat price from $49 a month.
No credit card required.