Free Tool

Pull every Stripe transaction into Google Sheets. Free.

One Google Apps Script, your own Stripe key, no install and nothing to buy. Paste it into any Sheet and it pulls every charge from the trailing months — succeeded, failed, refunded, all of it.

One file · copy-paste ready · runs entirely in your own Google account

What this script actually does

It calls Stripe's own API for every charge created in the trailing window you set — by default the last 3 months — and writes one row per charge to the sheet that's open when you run it: charge ID, date, amount, currency, status, customer email, description, payment method, and any refunded amount.

Every run clears that sheet and rewrites it from scratch. It's a snapshot of the trailing window, not an append log, so re-running it is always safe.

This is a raw transaction dump, on purpose — every status included, nothing calculated on top of it. If you want it running on a schedule with MRR, churn, and charts instead of a row-per-charge list, that's a different, bigger job — see the closing section below.

Setup, five steps

Ten minutes, most of it spent in Stripe's dashboard creating a key with no write access.

  1. 1

    Create a restricted Stripe key

    In your Stripe Dashboard, go to Developers → API keys → Create restricted key. Name it something like "Google Sheets read-only", set Charges to Read, and leave every other resource at None. Create the key and copy it — Stripe only shows it to you once.

  2. 2

    Open Apps Script from a blank Sheet

    Open a new (or existing) Google Sheet, then go to Extensions → Apps Script. This opens a script editor attached to that spreadsheet.

  3. 3

    Paste the script

    Delete whatever's in the default Code.gs file and paste the whole script below in its place.

  4. 4

    Fill in CONFIG

    At the top of the script, paste your restricted key into STRIPE_API_KEY, and set TRAILING_MONTHS to how many months back you want (defaults to 3).

  5. 5

    Save and run it

    Save the project, then run pullStripeTransactions from the Run button. The first run asks you to authorize — that's Google asking permission to edit this spreadsheet, not Stripe. Reload the sheet tab afterward and you'll have a "Stripe Transactions" menu for one-click re-runs.

🔑

Why a restricted key, not a secret key: a leaked restricted key scoped to "Charges: Read" can only ever read charges back to you. A leaked secret key can refund payments, issue charges, or touch payouts. The script only needs to read — so give it only that.

The script

Paste this whole thing into Code.gs, replacing whatever's there by default.

Code.gs
/**
 * Free Stripe -> Google Sheets Transaction Puller
 * By Makeinfo -- https://www.makeinfo.co/products/stripesync
 *
 * Pulls every Stripe charge -- succeeded, failed, refunded, pending, all of
 * it -- created in the trailing N months, and writes it to the active sheet.
 *
 * SETUP
 * 1. In Stripe: Developers -> API keys -> Create restricted key.
 *    Name it something like "Google Sheets read-only", grant it
 *    "Charges: Read" only, leave every other resource at "None".
 *    Copy the key (starts with rk_live_ or rk_test_) -- Stripe only shows
 *    it once. Do NOT use a secret key (sk_live_/sk_test_); this script
 *    never needs write access to your Stripe account.
 * 2. In a blank Google Sheet: Extensions -> Apps Script.
 * 3. Delete the boilerplate in Code.gs and paste this whole file in its place.
 * 4. Fill in CONFIG below.
 * 5. Save, then run pullStripeTransactions from the Run button. The first
 *    run asks you to authorize the script -- that's Google asking
 *    permission to edit this spreadsheet, not Stripe. Reload the sheet tab
 *    afterward and use the "Stripe Transactions" menu to re-run any time.
 *
 * Every run clears the active sheet and rewrites it from scratch -- this is
 * a snapshot of the trailing window, not an append log.
 */

// --------------------------- CONFIG ---------------------------
const CONFIG = {
  // Restricted key only. Stripe Dashboard -> Developers -> API keys ->
  // Create restricted key -> grant "Charges: Read" -> Create key.
  // Starts with rk_live_ or rk_test_.
  STRIPE_API_KEY: 'rk_live_your_restricted_key_here',

  // How many months back to pull, counting from right now.
  TRAILING_MONTHS: 3,
};
// ----------------------------------------------------------------

const STRIPE_API_BASE = 'https://api.stripe.com/v1';
const PAGE_SIZE = 100;

const SHEET_HEADERS = [
  'Charge ID',
  'Date',
  'Amount',
  'Currency',
  'Status',
  'Customer Email',
  'Description',
  'Payment Method',
  'Refunded Amount',
];

/**
 * Adds a menu so re-running this is one click, instead of going back into
 * the Apps Script editor every time. Runs automatically when the sheet opens.
 */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Stripe Transactions')
    .addItem('Pull trailing transactions', 'pullStripeTransactions')
    .addToUi();
}

/**
 * Entry point. Run this to pull every charge from the trailing
 * CONFIG.TRAILING_MONTHS months into the active sheet.
 */
function pullStripeTransactions() {
  const ui = SpreadsheetApp.getUi();

  const keyCheck = validateApiKey_(CONFIG.STRIPE_API_KEY);
  if (!keyCheck.ok) {
    ui.alert('Stripe key looks wrong', keyCheck.message, ui.ButtonSet.OK);
    return;
  }

  const range = getTrailingMonthsRange_(CONFIG.TRAILING_MONTHS);

  let charges;
  try {
    charges = fetchAllCharges_(CONFIG.STRIPE_API_KEY, range.gte, range.lt);
  } catch (err) {
    ui.alert('Stripe API error', err.message, ui.ButtonSet.OK);
    return;
  }

  writeChargesToActiveSheet_(charges);

  ui.alert(
    'Done',
    'Pulled ' + charges.length + ' charge' + (charges.length === 1 ? '' : 's') +
      ' from the last ' + CONFIG.TRAILING_MONTHS + ' month' + (CONFIG.TRAILING_MONTHS === 1 ? '' : 's') + '.',
    ui.ButtonSet.OK
  );
}

/**
 * A light sanity check, not real validation -- Stripe itself is the source
 * of truth on whether a key actually works. This only catches the two most
 * common mistakes: leaving the placeholder in, or pasting a full secret key.
 */
function validateApiKey_(key) {
  if (key && key.indexOf('sk_') === 0) {
    return {
      ok: false,
      message:
        'This looks like a Stripe secret key (sk_...). Use a restricted key ' +
        '(rk_...) instead -- Stripe Dashboard -> Developers -> API keys -> ' +
        'Create restricted key, grant it "Charges: Read" only, and paste that here.',
    };
  }
  if (!key || key.indexOf('rk_') !== 0) {
    return {
      ok: false,
      message:
        'CONFIG.STRIPE_API_KEY does not look like a Stripe restricted key ' +
        '(it should start with rk_live_ or rk_test_). Update CONFIG at the top of the script.',
    };
  }
  return { ok: true, message: '' };
}

/** Trailing N months, ending now, as Stripe created[gte]/created[lt] unix-seconds. */
function getTrailingMonthsRange_(months) {
  const now = new Date();
  const start = new Date(now);
  start.setMonth(start.getMonth() - months);

  return {
    gte: Math.floor(start.getTime() / 1000),
    lt: Math.floor(now.getTime() / 1000),
  };
}

/**
 * Pages through GET /v1/charges via starting_after cursor pagination,
 * 100 charges per request (Stripe's max page size), until has_more is false.
 */
function fetchAllCharges_(apiKey, createdGte, createdLt) {
  const charges = [];
  let startingAfter = null;
  let hasMore = true;

  while (hasMore) {
    const params = {
      limit: PAGE_SIZE,
      'created[gte]': createdGte,
      'created[lt]': createdLt,
    };
    if (startingAfter) params.starting_after = startingAfter;

    const page = stripeGet_(apiKey, '/charges', params);
    charges.push.apply(charges, page.data);

    hasMore = page.has_more && page.data.length > 0;
    if (hasMore) {
      startingAfter = page.data[page.data.length - 1].id;
    }
  }

  return charges;
}

/** One authenticated GET against the Stripe API using Apps Script's native UrlFetchApp. */
function stripeGet_(apiKey, path, params) {
  const query = Object.keys(params)
    .map(function (key) {
      return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
    })
    .join('&');
  const url = STRIPE_API_BASE + path + (query ? '?' + query : '');

  const response = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + apiKey },
    muteHttpExceptions: true,
  });

  const status = response.getResponseCode();
  const body = response.getContentText();

  if (status !== 200) {
    let message = body;
    try {
      const parsed = JSON.parse(body);
      if (parsed.error && parsed.error.message) message = parsed.error.message;
    } catch (e) {
      // Body wasn't JSON -- fall back to the raw text above.
    }
    throw new Error('Stripe returned ' + status + ': ' + message);
  }

  return JSON.parse(body);
}

/** Clears the active sheet and rewrites headers + one row per charge. */
function writeChargesToActiveSheet_(charges) {
  const sheet = SpreadsheetApp.getActiveSheet();
  sheet.clear();

  const rows = charges.map(chargeToRow_);
  const values = [SHEET_HEADERS].concat(rows);

  sheet.getRange(1, 1, values.length, SHEET_HEADERS.length).setValues(values);
  sheet.getRange(1, 1, 1, SHEET_HEADERS.length).setFontWeight('bold');
  sheet.setFrozenRows(1);
  sheet.autoResizeColumns(1, SHEET_HEADERS.length);
}

/**
 * Maps one raw Stripe charge object to a sheet row. Amounts are converted
 * from Stripe's smallest-currency-unit integers (e.g. cents) to a decimal
 * by dividing by 100 -- correct for two-decimal currencies (USD, EUR, GBP...)
 * but not for zero-decimal currencies (JPY, KRW...). If you bill in one of
 * those, adjust the division for that row.
 */
function chargeToRow_(charge) {
  return [
    charge.id,
    new Date(charge.created * 1000),
    charge.amount / 100,
    charge.currency ? charge.currency.toUpperCase() : '',
    charge.status,
    charge.receipt_email || (charge.billing_details && charge.billing_details.email) || '',
    charge.description || '',
    (charge.payment_method_details && charge.payment_method_details.type) || '',
    charge.amount_refunded / 100,
  ];
}

What each column means

Nine columns, written in this order every time.

Each output column and what it contains
Column What it is
Charge ID Stripe's own ID for the charge (ch_...) — useful for cross-referencing back in the Stripe Dashboard.
Date When the charge was created, converted from Stripe's unix timestamp to a real date.
Amount The charge amount, converted from Stripe's smallest-unit integer (cents) to a decimal.
Currency The three-letter currency code, uppercased (USD, EUR, GBP...).
Status Stripe's own charge status — succeeded, failed, pending, and so on. Nothing is filtered out.
Customer Email The receipt or billing email attached to the charge, when Stripe has one.
Description Whatever description you (or your checkout flow) attached to the charge, if any.
Payment Method The payment method type Stripe used — card, ideal, us_bank_account, etc.
Refunded Amount How much of the charge has been refunded, in the same decimal format as Amount.

Questions

Does Makeinfo see my Stripe key or my transaction data?

No. The script runs entirely inside your own Google Apps Script project and calls Stripe directly from there. Nothing passes through Makeinfo's servers — we never see the key or the data it pulls.

How do I change the trailing-months window?

Edit TRAILING_MONTHS in the CONFIG block at the top of the script, then run it again.

Why does it need a Stripe key at all?

Stripe's API is the only way to read your own charges programmatically. The restricted-key requirement is what keeps that access read-only — the script literally cannot refund, charge, or touch payouts with it.

Does it include failed and refunded charges, or just successful ones?

All of them. Every charge in the window comes through regardless of status, and the Status column tells you which is which — that's the point of a real ledger.

Can I make this run automatically on a schedule?

Not out of the box. You can add a time-driven trigger yourself from the Apps Script editor's Triggers panel, or use Stripesync for Sheets — the paid add-on — if you'd rather have scheduled imports, MRR and churn tracking, and native charts without wiring up your own trigger.

What happens with very large transaction volumes?

The script pages through all results automatically, but free Google accounts cap Apps Script executions at about six minutes. If a pull times out, lower TRAILING_MONTHS and run it again.

Want this running on its own?

This script is a manual pull you re-run yourself. Stripesync for Sheets does the scheduled version — weekly or monthly MRR, gross volume, new customers, and churn rate, imported automatically with native charts, still using your own Stripe key.

See Stripesync for Sheets →