Skip to main content

Stripe Table Data Architecture

Overview​

The FinMatch Admin Dashboard displays Stripe customer data in a table that allows you to view, search, and link Stripe billing profiles to FinMatch merchants. This document explains how the data is fetched, displayed, and managed.

Data Fetching​

Current Implementation​

The Stripe table fetches data fresh from Stripe's API every time the page loads.

When you navigate to the Stripe page in the admin dashboard:

  1. Frontend loads the live list: paginated GET /api/customers on stripe-api (Cloud Run service finmatch-stripe-api, europe-west2).
  2. The table shows every live customer (not the newest 100). While the tab stays open it polls about every 60 seconds.
  3. Stripe webhooks upsert one customer so a billing change can appear on the next poll without Create Snapshot.
  4. Create Snapshot (POST /api/snapshots/create) is optional archive only. It is not how the table loads.

Do not hard-code a Cloud Run revision host. The service name is finmatch-stripe-api.

Data Flow​

Admin Dashboard (Frontend)
↓
Stripe API Service (Cloud Run)
↓
Stripe API (External)
↓
Data Transformation
↓
Table Display

Refresh Capability​

The Stripe tab polls GET /api/customers about every 60 seconds while it is open. Reload the tab if you need the list immediately. Create Snapshot is not a refresh control.

Optional snapshot archive​

The Stripe table is the live GET /api/customers list. Create Snapshot writes an optional GCS archive (POST /api/snapshots/create). Do not click that button to see billed customers. Snapshots live on stripe-api / GCS, not in the admin static site.

Table Columns Explained​

Customer ID​

Stripe Field: customer.id
Display: Full Stripe customer ID (e.g., cus_R5CyAAFzShIf5Q)

This is the unique identifier for the Stripe customer record. It's used to link the Stripe billing profile to a FinMatch merchant.


Name​

Stripe Field: customer.name or customer.description
Display: Customer name as stored in Stripe

Important Notes:

  • The Name field in FinMatch Admin can include either individual names or company names
  • Stripe allows customers to be individuals or businesses
  • If a customer is a business, the name will be the company name
  • If a customer is an individual, the name will be the person's name
  • The field may also contain a description if no name is set

Example values:

  • "John Smith" (individual)
  • "Acme Corporation Ltd" (company)
  • "John Smith - Acme Corp" (combined)

Email​

Stripe Field: customer.email
Display: Customer email address

The primary email address associated with the Stripe customer account.


Subscription Plan​

Stripe Field: subscription.items.data[0].price.id
Display: Stripe Price ID (e.g., price_1SnL3oRrNO9w4XvzhYpLvknw)

What it shows:

  • The Price ID of the active subscription plan
  • This is Stripe's unique identifier for the pricing tier

Understanding the values:

  • If a price ID is shown: The customer has an active subscription with billing
  • If "N/A" is shown: The customer does not have an active subscription
    • This could mean:
      • No subscription was ever created
      • The subscription was canceled
      • The subscription expired
      • The subscription is in a trial period that ended

Note: The Subscription Plan column is designed to show whether a Stripe billing record has an active subscription. If it shows "N/A", it means there is no active subscription plan associated with this customer.


Status​

Stripe Field: subscription.status
Display: Color-coded badge showing subscription status

Status Options:

StatusBadge ColorMeaning
ActiveGreenSubscription is active and billing normally
TrialingBlueCustomer is in trial period
Past DueRedPayment failed, subscription is past due
CanceledGraySubscription has been canceled
NoneGrayNo subscription exists for this customer

How it's determined:

  • The system looks for an active subscription (status === 'active')
  • If no active subscription exists, it checks for any subscription and uses its status
  • If no subscriptions exist at all, status is 'none'

Status Badge Colors:

  • Active: Green (#10b981)
  • Past Due: Red (#ef4444)
  • Canceled: Gray (#6b7280)
  • None: Light Gray (#9ca3af)

Next Billing​

Stripe Field: subscription.current_period_end
Display: Formatted date (e.g., "12/31/2024")

What it shows:

  • The date when the current billing period ends
  • Calculated from Stripe's current_period_end timestamp (Unix epoch seconds)

Display:

  • If an active subscription exists: Shows the next billing date
  • If no active subscription: Shows "N/A"

Note: This column may have been removed in recent updates. Check the current table structure.


Created​

Stripe Field: customer.created
Display: Formatted date (e.g., "01/15/2024")

What it shows:

  • The date when the Stripe customer account was created
  • Calculated from Stripe's created timestamp (Unix epoch seconds)

Note: This column may have been removed in recent updates. Check the current table structure.


FinMatch Merchant​

Source: merchants.json → stripeCustomerId field
Display: Merchant name (if linked) or "Not linked"

What it shows:

  • If linked: The name of the FinMatch merchant linked to this Stripe customer
    • Clickable link that navigates to the merchant detail page
    • Shows merchant name or company name
  • If not linked: Displays "Not linked" in gray italic text

How linking works:

  • When a merchant is linked, merchants.json stores the stripeCustomerId in the merchant's profile
  • The table finds the linked merchant by matching merchant.stripeCustomerId === customer.id
  • Only one merchant can be linked to a Stripe customer at a time

Source: merchants.json → lastUpdated field (when stripeCustomerId is set)
Display: ISO timestamp or formatted date

What it shows:

  • The last time the merchant was linked to this Stripe customer
  • This is the lastUpdated timestamp from the merchant profile

Important Notes:

  • Only captures the last linking date - if a link is added and removed multiple times, only the most recent linking date is stored
  • No history is maintained - previous linking/unlinking events are not tracked
  • The timestamp is updated whenever:
    • A merchant is linked to a Stripe customer
    • A merchant is unlinked from a Stripe customer (set to null)

Note: This column may be a future addition. Currently, the lastUpdated field tracks when the merchant profile was last modified, which includes Stripe linking changes.


Linking Merchants to Stripe Customers​

How It Works​

  1. Click "Link" button in the Action column for a Stripe customer
  2. Modal opens showing all available FinMatch merchants
  3. Select a merchant from the dropdown
  4. Click "Link" to create the connection

Data Refresh During Linking​

Current behavior:

  • When you open the link modal, it uses already loaded Stripe data
  • It does not fetch fresh data from Stripe for the specific customer
  • It does not refresh the entire Stripe customer list

What happens:

  1. Stripe data is loaded when the page first loads (loadStripeCustomers())
  2. The link modal displays the current Stripe customer data
  3. When you link a merchant, it:
    • Updates merchants.json in GCS with the stripeCustomerId
    • Refreshes the merchant data display
    • Updates the table to show the linked merchant name

Future enhancement:

  • Option to fetch fresh Stripe data for the specific customer being linked
  • Option to refresh entire Stripe customer list before linking

Refreshing the View​

Manual Refresh:

  • Use the refresh button in the dashboard settings
  • Calls loadStripeCustomers() which fetches all Stripe customers fresh from the API
  • Updates the entire table with latest data

Automatic Refresh:

  • Currently, data is only refreshed on page load
  • No automatic polling or background refresh exists

Data Mapping Reference​

Stripe API → FinMatch Admin Table​

FinMatch ColumnStripe Field PathTransformation
Customer IDcustomer.idDirect mapping
Namecustomer.name or customer.descriptionFallback to description if name missing
Emailcustomer.emailDirect mapping
Subscription Plansubscription.items.data[0].price.idExtract price ID from first subscription item
Statussubscription.statusMap to display labels (Active, Past Due, etc.)
Next Billingsubscription.current_period_endConvert Unix timestamp to date
Createdcustomer.createdConvert Unix timestamp to date
FinMatch Merchantmerchants.json[merchantId].stripeCustomerIdMatch by customer ID
Last Link Datemerchants.json[merchantId].lastUpdatedWhen stripeCustomerId is set

Status Mapping​

{
'active': 'Active',
'trialing': 'Trialing',
'past_due': 'Past Due',
'canceled': 'Canceled',
'none': 'None' // No subscription exists
}

Technical Details​

API Endpoint​

Stripe API Service:

  • Service: finmatch-stripe-api (Cloud Run, europe-west2)
  • Read path: GET /api/snapshots/latest (today), else POST /api/snapshots/create, else GET /api/customers
  • Method: GET (snapshot/customers) / POST (create)
  • Customers response: { success: true, customers: [...] }

Stripe API Call: stripe-api pages through customers.list (Stripe’s page size is 100) until the live list is complete, expanding data.subscriptions on each page. The dashboard Link Stripe picker and the Stripe table use that full list, not the newest 100 only.

Data Transformation​

The frontend transforms Stripe customer objects:

{
id: customer.id,
name: customer.name || customer.description || 'N/A',
email: customer.email || 'N/A',
subscriptionPlan: price.id || 'N/A',
subscriptionPlanId: price.id,
subscriptionStatus: subscription.status || 'none',
nextBillingDate: formatDate(subscription.current_period_end),
created: customer.created
}

Performance Considerations​

  • API Rate Limits: Stripe has rate limits on API calls
  • Page size: Stripe still returns at most 100 customers per API call
  • Pagination: Implemented in stripe-api. GET /api/customers walks every page so older billed merchants (for example Access Training, cus_ST325wcTyLcFPv) appear on the Stripe tab without Create Snapshot. There is a 10,000-customer safety cap.
  • Caching: stripe-api keeps a short-lived in-memory list (about 60s) and webhook upserts. The admin table polls that live list. Do not treat Create Snapshot as the refresh control.

Best Practices​

  1. Reload the tab if the list looks stale: the Stripe tab polls about every 60 seconds. Do not press Create Snapshot to refresh.
  2. Check subscription status: Verify the Status column shows "Active" before linking to ensure valid billing
  3. Verify subscription plan: Ensure Subscription Plan is not "N/A" if you expect an active subscription
  4. Link one merchant per customer: Each Stripe customer should link to only one FinMatch merchant

Troubleshooting​

"N/A" in Subscription Plan​

  • Cause: No active subscription exists
  • Solution: Check Stripe dashboard to verify subscription status
  • Action: May need to create or reactivate subscription in Stripe

"Not linked" in FinMatch Merchant​

  • Cause: No merchant has stripeCustomerId matching this customer ID
  • Solution: Use the "Link" button to connect a merchant
  • Action: Select merchant from dropdown and confirm link

Stale Data​

  • Cause: Data was loaded earlier and Stripe has been updated
  • Solution: Click refresh button to fetch latest data from Stripe
  • Action: Refresh will update entire customer list