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 ConverterWhat 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 path | CSV column | Notes |
|---|---|---|
| data.object.id | payment_intent_id | Unique PI identifier |
| data.object.amount | amount_usd | Divide by 100 for dollars |
| data.object.currency | currency | Lowercase ISO code |
| data.object.status | status | succeeded, requires_action, etc. |
| data.object.customer | customer_id | cus_xxx string (or object if expanded) |
| data.object.receipt_email | ||
| data.object.description | description | |
| data.object.metadata.plan | plan | Custom metadata fields vary per account |
| data.object.created | created_at | Unix timestamp — convert to ISO date |
| type | event_type | Top-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 / 100Customer 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.