Skip to content
adapters.io

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

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.

Try the live demo

No credit card required.