csvjson
Stripe API

Convert Stripe Webhook JSON to CSV

Stripe API responses wrap everything in envelopes and use Unix timestamps, cents-based amounts, and deeply nested customer objects. Here's how to get clean CSV from any Stripe endpoint.

The Stripe API is one of the most common sources of JSON-to-CSV conversion tasks — payment analysis, subscription reporting, churn dashboards. The challenge isn't fetching the data; it's that Stripe wraps every response in an object envelope, stores amounts in cents (not dollars), uses Unix timestamps for dates, and expands related objects (customers, payment methods) inside the main record. This guide handles all of that.

Want to skip the code? Paste your JSON directly into the converter — it handles nested objects, arrays, and large files automatically.

Open JSON to CSV Converter

What the API returns

Stripe payment_intent.succeeded webhook event (trimmed)

{
  "id": "evt_1OqY2LKZ2eZvKYlo2GXy8Pmj",
  "object": "event",
  "type": "payment_intent.succeeded",
  "created": 1707926400,
  "data": {
    "object": {
      "id": "pi_3OqY2LKZ2eZvKYlo2X1234567",
      "amount": 4999,
      "currency": "usd",
      "status": "succeeded",
      "customer": "cus_PxAbc123",
      "receipt_email": "alice@example.com",
      "description": "Subscription upgrade",
      "metadata": { "user_id": "usr_789", "plan": "pro" },
      "created": 1707926400
    }
  }
}

Field mapping: JSON path → CSV column

JSON pathCSV columnNotes
data.object.idpayment_intent_idUnique PI identifier
data.object.amountamount_usdDivide by 100 for dollars
data.object.currencycurrencyLowercase ISO code
data.object.statusstatussucceeded, requires_action, etc.
data.object.customercustomer_idcus_xxx string (or object if expanded)
data.object.receipt_emailemail
data.object.descriptiondescription
data.object.metadata.planplanCustom metadata fields vary per account
data.object.createdcreated_atUnix timestamp — convert to ISO date
typeevent_typeTop-level event type

Stripe amounts are always in the smallest currency unit — cents for USD. Divide by 100 before writing to CSV.

Python conversion

Convert Stripe charges export (JSON array from dashboard or API)

import pandas as pd
import json

with open("stripe_charges.json") as f:
    data = json.load(f)

# Stripe API list response: {"data": [...], "has_more": false}
charges = data["data"] if "data" in data else data

df = pd.json_normalize(charges, sep=".")

# Cents → dollars
if "amount" in df.columns:
    df["amount_usd"] = df["amount"] / 100
    df = df.drop(columns=["amount"])

# Unix timestamp → readable date
for col in ["created", "updated"]:
    if col in df.columns:
        df[col] = pd.to_datetime(df[col], unit="s").dt.strftime("%Y-%m-%d %H:%M")

# Keep useful columns (drop Stripe internal metadata)
keep = ["id", "amount_usd", "currency", "status", "customer",
        "receipt_email", "description", "created"]
df = df[[c for c in keep if c in df.columns]]

df.to_csv("charges.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(df)} rows")

Flatten webhook events from a webhook log (array of events)

import pandas as pd
import json

with open("webhook_events.json") as f:
    events = json.load(f)

rows = []
for event in events:
    obj = event.get("data", {}).get("object", {})
    rows.append({
        "event_id":         event.get("id"),
        "event_type":       event.get("type"),
        "event_created":    pd.to_datetime(event.get("created"), unit="s").strftime("%Y-%m-%d %H:%M"),
        "payment_id":       obj.get("id"),
        "amount_usd":       obj.get("amount", 0) / 100,
        "currency":         obj.get("currency"),
        "status":           obj.get("status"),
        "email":            obj.get("receipt_email"),
        "customer_id":      obj.get("customer"),
        "plan":             obj.get("metadata", {}).get("plan"),
    })

pd.DataFrame(rows).to_csv("webhook_log.csv", index=False, encoding="utf-8-sig")

Common issues with this API

Amounts are in cents, not dollars

Every Stripe amount field (amount, amount_received, amount_capturable) is in the smallest currency unit — cents for USD, pence for GBP, yen for JPY (which has no subunit). Divide by 100 for most currencies, but JPY, KRW, and a handful of others are already in their base unit.

# Safe cents-to-dollars conversion
ZERO_DECIMAL_CURRENCIES = {"jpy", "krw", "vnd", "clp", "gnf", "mga", "pyg", "rwf", "ugx", "xaf", "xof"}

def to_major(amount, currency):
    if currency.lower() in ZERO_DECIMAL_CURRENCIES:
        return amount
    return amount / 100

Customer field is a string ID or an expanded object

If you call the API without expansion, customer is a string like 'cus_PxAbc123'. If you expand it (?expand[]=customer), it becomes a full object. json_normalize handles the object case, but you'll get an empty customer column in the string case.

# Handle both string and expanded object
def extract_customer(row):
    c = row.get("customer")
    if isinstance(c, dict):
        return c.get("id"), c.get("email"), c.get("name")
    return c, None, None

df["customer_id"], df["customer_email"], df["customer_name"] = zip(
    *df.apply(lambda r: extract_customer(r.to_dict()), axis=1)
)

Paginated API — you need all pages, not just the first

The Stripe API returns at most 100 records per request. Use the has_more flag and starting_after cursor to fetch all pages before converting to CSV.

import stripe
stripe.api_key = "sk_live_..."

charges = []
page = stripe.Charge.list(limit=100)
while True:
    charges.extend(page.data)
    if not page.has_more:
        break
    page = stripe.Charge.list(limit=100, starting_after=page.data[-1].id)

print(f"Fetched {len(charges)} charges")

Frequently asked questions

Can I convert a Stripe CSV export instead of JSON?

Stripe's dashboard exports are already CSV — you don't need to convert them. The JSON conversion workflow is for data fetched via the API or received via webhooks, where the data arrives as JSON.

How do I include subscription data alongside charges?

Expand the subscription field in your API call: stripe.Charge.list(expand=['data.invoice.subscription']). The subscription object will nest inside each charge, and json_normalize will flatten it to columns like invoice.subscription.id and invoice.subscription.status.

Why do some rows have empty columns after json_normalize?

Stripe records don't always have the same fields — a charge with no customer is common, and metadata keys vary per account. json_normalize fills missing fields with NaN, which becomes an empty cell in the CSV. This is expected behavior for sparse data.

Does the online converter handle Stripe JSON?

Yes. Paste the JSON from the Stripe dashboard export or a saved API response into the JSON to CSV converter. It automatically flattens the data envelope and nested objects using dot-notation column names.

Related tools