Skip to content
adapters.io

PayPal to BigQuery: how to load payments data your team can actually query

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 load PayPal into BigQuery, pull transactions from the PayPal Transaction Search API (GET /v1/reporting/transactions, authenticated with OAuth 2.0 client credentials) in date-range windows, then land each row in BigQuery. Map each field to a typed column, store gross, fee, and net as NUMERIC, keep timestamps in UTC, partition by transaction date, and MERGE on transaction_id so retries and re-pulled windows never double-count. Re-pull the recent window on every run so refunds and chargebacks that post later against an earlier sale get captured.

Key takeaways

  • Pull in date-range windows. The Transaction Search API pages results and caps each request to a bounded range (commonly up to 31 days), so you loop windows rather than asking for everything at once.
  • Store gross, fee, and net separately. PayPal reports gross and fee; net is gross minus fee. Compute net explicitly as NUMERIC and keep the ISO 4217 currency_code so multi-currency accounts stay correct.
  • MERGE on transaction_id. Refunds, chargebacks, and reversals land later against an earlier sale, so upserting by key on a re-pulled settlement window catches adjustments without duplicating rows.
  • Partition and cluster for cost. Partition by transaction date and cluster by transaction_id so window queries and the MERGE scan only the slice they need instead of the whole table.

How do I connect PayPal to BigQuery?

Connect PayPal to BigQuery by calling the PayPal Transaction Search API (GET /v1/reporting/transactions) with an OAuth 2.0 client-credentials token, then writing the returned rows into a BigQuery table. There is no native PayPal export into BigQuery, so you either script the extract or use a managed connector that handles auth, paging, and loading for you.

The API returns a nested payload. The block you care about most is transaction_info, which holds transaction_id, transaction_amount (a value plus a currency_code), fee_amount, transaction_status, transaction_initiation_date, and transaction_updated_date. Alongside it you get payer_info, cart_info, and shipping_info. A first-time load walks the full available history in windows and writes each transaction as a row; after that you only fetch new and recently changed windows. If you would rather skip the plumbing, the PayPal to BigQuery integration runs the extract, the typing, and the upsert on a schedule so your team just queries the table.

Two practical notes before you write a line of code. First, PayPal data is near-real-time but not instant: a transaction can take hours to appear in the reporting API, so a window you pulled five minutes after a sale may miss it. Second, requests are rate limited and each call is bounded to a date range, so a naive "give me everything" loop will fail. Both facts push you toward the same design: pull bounded windows, and re-pull recent ones.

How do I load PayPal transactions incrementally into BigQuery?

Load incrementally by tracking a watermark on transaction_updated_date, then on each run pulling every window from a lookback point up to now in date-range chunks (commonly 31 days per request). Land the rows in a staging table and MERGE into the target on transaction_id so new sales insert and changed transactions update in place. This keeps loads cheap and idempotent.

The reason you key on transaction_updated_date rather than the initiation date is adjustments. A sale from three weeks ago can change today when a buyer opens a dispute, so an extract that only looked at new initiation dates would never see it. Set a lookback that comfortably covers your settlement and dispute window, re-pull that whole span every run, and let the MERGE collapse duplicates. Because the write is an upsert keyed on transaction_id, re-pulling the same window is safe: a retry after a timeout, or an overlapping window, updates the existing row instead of inserting a second copy.

Step What happens Why it matters
Read watermark Load the last successful transaction_updated_date, minus a lookback buffer The buffer re-pulls recent activity so late refunds and disputes are not missed
Window the range Split from the watermark to now into chunks (commonly up to 31 days each) Each API call is bounded to a date range, so you cannot request the full history at once
Page each window Follow paging until the window is exhausted, respecting rate limits Results are paged, so one call rarely returns every transaction in a window
Stage rows Write typed rows into a staging table in BigQuery Staging lets you validate and dedupe before touching the production table
MERGE on transaction_id Upsert staging into the target keyed on transaction_id Retries and overlapping windows update in place instead of double-counting
Advance watermark Save the max transaction_updated_date seen on success The next run starts where this one ended, minus the buffer

Wrap the whole thing so the watermark only advances after the MERGE commits. If a run fails halfway, the next run repeats the window rather than skipping it, and the upsert makes that repeat harmless. The same pattern powers Stripe to BigQuery loads, and if you also send PayPal into your books it carries over to PayPal to QuickBooks syncing too.

How do I reconcile PayPal gross, fees, and net in BigQuery?

Reconcile by storing three explicit columns: gross (transaction_amount.value), fee (fee_amount), and net computed as gross minus fee. PayPal reports gross and fee separately and does not hand you a single net figure, so compute it in the load or in your first model. Keep currency_code on every row so you never sum across currencies by accident.

Sign conventions trip people up. A sale carries a positive gross and a fee that reduces your net; a refund posts as a negative amount that gives back some or all of the original. If you always derive net as gross minus fee at the row level and sum from there, your net-settlement total will line up with what actually hits your bank, because that is exactly what PayPal deposits. Store the amounts as NUMERIC, not floats, so cents do not drift over millions of rows. The reconciliation model below is the backbone of every revenue and fee-rate report downstream.

Column Source Type / rule
gross transaction_amount.value NUMERIC. Positive for sales, negative for refunds and reversals
fee fee_amount.value NUMERIC. PayPal's processing fee for the transaction
net gross minus fee NUMERIC, computed. Matches what settles to your bank
currency transaction_amount.currency_code STRING, ISO 4217. Group by it before summing
fee_rate fee divided by gross NUMERIC, computed per row for effective-rate reporting

Build a fct_transaction model with gross, fee, net, currency, and status, plus a dim_payer from payer_info. Layer revenue, net-settlement, fee-rate, and cohort models on top of that one clean fact. Because net is derived once at the base, every report above it agrees on the number, which is the whole point of putting PayPal in a warehouse instead of eyeballing the Activity screen.

How do I handle PayPal refunds and chargebacks in BigQuery?

Handle them by treating refunds, chargebacks, disputes, and reversals as later events against an earlier sale, then capturing those events with an incremental load that re-pulls the recent settlement window by transaction_updated_date. Because each adjustment carries its own transaction_id or updates an existing status, the MERGE upsert folds them into the table without you deleting or hand-editing anything.

The failure mode to avoid is a load that only looks forward. A chargeback filed two weeks after a sale changes settled revenue in a period you already reported, so if your extract never revisits that window your revenue numbers quietly drift from reality. Re-pulling by transaction_updated_date fixes it: the sale's row updates when its status changes, and any separate refund or reversal row lands as its own negative-amount transaction. Keep transaction_status as a STRING column and split your models by event type, since authorization, capture, sale, refund, and reversal mean different things to revenue. Sales and captures recognize revenue; authorizations do not; refunds and reversals claw it back. Finance and ops teams that track income across your PayPal payouts care most about this layer, because a refund booked in the wrong period throws off both the revenue report and the payout reconciliation.

How far back can I pull PayPal transaction history?

You can pull a bounded history: the Transaction Search API exposes transactions for a limited lookback window, not the entire lifetime of the account, and each request is further capped to a date range (commonly up to 31 days). So the practical limit is both how far back the API will serve and how many windowed calls you are willing to make to walk that span.

For a backfill, start at the oldest date the API still returns and step forward one window at a time until you reach today, respecting rate limits between calls. If you need history older than the API's window, that data has to come from a PayPal Activity CSV export or your own prior records, loaded once as a seed and then kept fresh by the incremental job. Plan the backfill as a one-time batch: it is slow because of paging and rate limits, but you only pay that cost once, and every run after that only touches recent windows.

Can I load PayPal to BigQuery without code?

Yes. A managed connector handles the OAuth token, the windowed extraction, the type mapping, and the MERGE upsert for you, so you get a queryable BigQuery table without writing or maintaining an extract script. That is the trade: build it yourself for full control, or use a connector and spend your time on models instead of paging logic.

The hidden cost of the DIY path is not the first version, it is the upkeep: token refresh, paging edge cases, rate-limit backoff, the re-pull window for adjustments, and idempotent writes all have to keep working as volume grows. A managed PayPal to BigQuery connector ships those pieces already tuned: incremental loads, idempotent MERGE writes, retries, and per-record logs come standard. If you want to see it move real data before committing, the live demo walks a PayPal window into BigQuery end to end, and the broader data integration platform runs the same pattern for every source you connect. The mapping table below is the reference either way, whether you write the loader or just want to know what lands in each column.

PayPal field BigQuery column BigQuery type
transaction_info.transaction_id transaction_id STRING (MERGE key, cluster on it)
transaction_info.transaction_amount.value gross NUMERIC
transaction_info.transaction_amount.currency_code currency STRING (ISO 4217)
transaction_info.fee_amount.value fee NUMERIC
(computed) gross minus fee net NUMERIC
transaction_info.transaction_status status STRING
transaction_info.transaction_initiation_date initiated_at TIMESTAMP (UTC, partition on its date)
transaction_info.transaction_updated_date updated_at TIMESTAMP (UTC, incremental watermark)
payer_info.payer_id / email payer_id, payer_email STRING (feeds dim_payer)

Whichever route you take, the shape of the table is the same: typed columns, gross and fee and net kept honest, timestamps in UTC, partitioned by transaction date, clustered by transaction_id, and upserted by key. Get that base right and every revenue, fee, and cohort model above it reads one table your team can actually trust.

Load PayPal into BigQuery on a flat, published price

Windowed extraction, idempotent MERGE writes, retries, and per-record logs built in. Same price regardless of connectors, from $49 a month, with no sales call to start.

Try the live demo

No credit card required.