From 5037126acf253e2da65099a1e991934dda8ee35a Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 16:01:48 -0700 Subject: [PATCH 01/14] docs(cards): add periodic statements guide Add a reference page for card periodic statements under Cards > Statements. It shows a sample consumer statement, when a statement is sent, what it contains, how each field maps to Grid transaction data, and an example that builds a statement at period close. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 182 ++++++++++++++++++ mintlify/docs.json | 6 + mintlify/snippets/cards/statement-example.mdx | 115 +++++++++++ 3 files changed, 303 insertions(+) create mode 100644 mintlify/cards/statements.mdx create mode 100644 mintlify/snippets/cards/statement-example.mdx diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx new file mode 100644 index 000000000..75144c438 --- /dev/null +++ b/mintlify/cards/statements.mdx @@ -0,0 +1,182 @@ +--- +title: "Periodic statements" +description: "What a consumer card periodic statement contains, and how to build one from Grid transaction data" +icon: "/images/icons/receipt-check.svg" +--- + +import { StatementExample } from '/snippets/cards/statement-example.mdx'; + +If you issue cards through Grid, each cardholder account is held at **Lead Bank**, the account-holding institution, and Lightspark is the program manager. Regulation E requires a periodic statement for every consumer account that can send or receive electronic fund transfers. + +This guide explains when a statement is sent, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. It covers consumer cardholder accounts. + + +Periodic statements are a regulatory requirement for consumer card accounts. Your platform agreement sets out who prepares and delivers them. Use this page as a reference for what a statement contains and how to build it from Grid transaction data. + + +## Sample statement layout + +Your statement's visual design is up to you, but the content and field set are fixed. The layout below shows a consumer statement with sample data filled in: + + + +The asterisk marks the transfers the error-resolution notice covers, and the same asterisk keys the notice heading. The marker is a reading aid, not a required field. + +## When a statement is sent + +A statement period closes once a month, at the same point in each month. Then: + +- **A statement goes out for every month with electronic fund transfer activity**, for example an ACH deposit, an ACH debit, or a debit card purchase. +- **A statement goes out at least quarterly** for accounts with no electronic fund transfer activity in the month. + +A monthly statement for every open account satisfies both rules, so there is no need for a separate quarterly variant. + +The statement goes to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** the statement (for example, a downloadable PDF or an email they can save). Record the issue date. The cardholder's window to report an error runs from the issue date of the first statement showing the error. Your platform agreement sets out who delivers the statement and who keeps the delivery record. + + +List every transaction that moved the balance in the period, including transfers that Regulation E does not cover, such as wires. Without them, the opening and closing balances would not reconcile. Once a statement is issued, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. + + +## What a statement must contain + +A statement combines **fixed disclosures** (provider and error-resolution language) with **data-driven fields** populated from the account and its transactions for the period. + +### Lightspark and Lead Bank disclosures + +Every consumer statement carries these disclosures exactly as written: + +| Field | Value | +|-------|-------| +| Provider line | "This account is held at Lead Bank, the account-holding institution. Lightspark is the program manager and is not a bank." | +| Customer service phone | (855) 516-0103 | +| Address for inquiries | 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 | +| Error-resolution notice | The full notice shown in the sample above, headed "In case of errors or questions about your electronic transfers", with the phone number and address above and the 60-day reporting window | + +### Statement fields + +| Field | Description | Required when | +|-------|-------------|---------------| +| Account holder | Name of the cardholder | Always | +| Account number | Number that identifies the account, masked to the last four digits | Always | +| Statement period | Start and end date of the period | Always | +| Issue date | Date the statement is sent | Always | +| Opening balance | Balance at the start of the period | Always | +| Closing balance | Balance at the end of the period | Always | +| Transaction date | Date each transaction posted to the account | Always | +| Transaction type | Kind of transfer, for example ACH deposit, debit card purchase, wire transfer out | Always | +| Payee or merchant | Counterparty name, or the merchant descriptor for a card purchase | Always | +| Terminal location | Merchant city and state | Card purchases at a merchant terminal, when the card network reports a location | +| Transaction amount | Signed amount in the account currency | Always | +| Fee line items | Each fee charged in the period, as its own line | When a fee was charged | +| Total fees for the period | Sum of all fees charged in the period | Always | + +## Mapping Grid data to statement fields + +Statement data comes from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Filter the transaction list by `accountIdentifier`, `startDate`, and `endDate`, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. + +| Statement field | Grid source | +|-----------------|-------------| +| Account holder | `fullName` on the customer (`customerType: INDIVIDUAL`) | +| Account number | `fundingPaymentInstructions[].accountOrWalletInfo.accountNumber` on the internal account, masked to the last four digits | +| Statement period | The `startDate` and `endDate` you pass to List transactions | +| Issue date | You supply it: the date the statement is sent | +| Closing balance | `balance` on the internal account, read at period close. Grid returns the current balance only, so read it when the period closes and store it | +| Opening balance | The previous period's closing balance from your records | +| Transaction date | `settledAt` on `INCOMING` and `OUTGOING` transactions; `authorizedAt` on `CARD` transactions | +| Transaction type | `type` (`INCOMING` / `OUTGOING` / `CARD`) and `direction` (`CREDIT` / `DEBIT`); on `OUTGOING` transactions, `paymentRail` names the rail | +| Payee or merchant | `counterpartyInformation` or `description` on `INCOMING` and `OUTGOING` transactions; `merchant.descriptor` on `CARD` transactions | +| Terminal location | `merchant.city` and `merchant.state` on `CARD` transactions, when present | +| Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`. Sign the amount by `direction`: a `CREDIT` adds to the balance and a `DEBIT` subtracts from it | +| Fee line items | `fees` on `INCOMING` and `OUTGOING` transactions, in the smallest unit of the sending currency. Card transactions carry no `fees` field | +| Total fees for the period | Sum of `fees` across the period's transactions | + + +Exclude `CARD` transactions with `status: DECLINED`; no money moved. A `VOIDED` card transaction moved no money either and has no `settledAmount`. A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). + + +## Example: build a statement at period close + +At period close, read the account holder and account, list the period's transactions, derive the lines and totals, render the statement, and hand it to your delivery channel. + +```javascript +// Run once per account when the statement period closes +async function buildStatement(customerId, accountId, startDate, endDate) { + const customer = await grid.get(`/customers/${customerId}`); + const { data: accounts } = await grid.get( + `/customers/internal-accounts?customerId=${customerId}¤cy=USD` + ); + const account = accounts.find((a) => a.id === accountId); + + // Page through every transaction in the period, oldest first + const transactions = []; + let cursor; + do { + const page = await grid.get( + `/transactions?accountIdentifier=${accountId}&startDate=${startDate}` + + `&endDate=${endDate}&sortOrder=asc&limit=100` + + (cursor ? `&cursor=${cursor}` : '') + ); + transactions.push(...page.data); + cursor = page.hasMore ? page.nextCursor : undefined; + } while (cursor); + + const format = (money) => + (money.amount / 10 ** money.currency.decimals).toFixed(money.currency.decimals); + + const lines = []; + let totalFees = 0; + for (const tx of transactions) { + if (tx.type === 'CARD' && (tx.status === 'DECLINED' || tx.status === 'VOIDED')) continue; + + const money = + tx.type === 'INCOMING' ? tx.receivedAmount : + tx.type === 'OUTGOING' ? tx.sentAmount : + tx.settledAmount; + const sign = tx.direction === 'CREDIT' ? 1 : -1; + + lines.push({ + date: tx.type === 'CARD' ? tx.authorizedAt : tx.settledAt, + type: tx.type, + direction: tx.direction, + payee: tx.type === 'CARD' ? tx.merchant.descriptor : tx.description, + terminalLocation: + tx.type === 'CARD' && tx.merchant.city && tx.merchant.state + ? `${tx.merchant.city}, ${tx.merchant.state}` + : undefined, + amount: sign * Number(format(money)), + }); + + if (tx.fees) { + // fees is an integer in the smallest unit of the sending currency + const fee = tx.fees / 10 ** money.currency.decimals; + totalFees += fee; + lines.push({ date: tx.settledAt, type: 'FEE', amount: -fee }); + } + } + + const closingBalance = Number(format(account.balance)); // read at period close + const openingBalance = await previousClosingBalance(accountId); // from your records + + const statement = { + // Fixed disclosures + providerLine: + 'This account is held at Lead Bank, the account-holding institution. ' + + 'Lightspark is the program manager and is not a bank.', + customerServicePhone: '(855) 516-0103', + address: '8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069', + // Account fields + accountHolder: customer.fullName, + accountNumber: maskAccountNumber(account.fundingPaymentInstructions), + period: { startDate, endDate }, + issueDate: new Date().toISOString(), + openingBalance, + closingBalance, + lines, + totalFees, + }; + + // Delivery and the delivery record follow your platform agreement + await sendStatementEmail(customer.platformCustomerId, statement); + await recordStatementDelivery(accountId, statement.issueDate); +} +``` diff --git a/mintlify/docs.json b/mintlify/docs.json index 745425b75..886a192f3 100644 --- a/mintlify/docs.json +++ b/mintlify/docs.json @@ -328,6 +328,12 @@ "cards/transactions/reconciliation" ] }, + { + "group": "Statements", + "pages": [ + "cards/statements" + ] + }, { "group": "Card design", "pages": [ diff --git a/mintlify/snippets/cards/statement-example.mdx b/mintlify/snippets/cards/statement-example.mdx new file mode 100644 index 000000000..80c1f4fee --- /dev/null +++ b/mintlify/snippets/cards/statement-example.mdx @@ -0,0 +1,115 @@ +export const StatementExample = () => { + const font = "'Suisse Intl', 'Helvetica Neue', Helvetica, Arial, sans-serif"; + const primary = '#1a1a1a'; + const secondary = '#4f5960'; + const hairline = '0.5px solid rgba(26,26,26,0.1)'; + const muted70 = 'rgba(26,26,26,0.7)'; + const muted78 = 'rgba(26,26,26,0.78)'; + const muted56 = 'rgba(26,26,26,0.56)'; + const ledgerColumns = '48px minmax(0, 1fr) 96px'; + const definitionRow = { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '24px', padding: '7px 0' }; + const definitionLabel = { color: muted70 }; + const definitionValue = { textAlign: 'right' }; + const currency = { marginLeft: '6px', color: muted70, fontSize: '10px' }; + const ledgerGrid = { display: 'grid', gridTemplateColumns: ledgerColumns, columnGap: '12px' }; + const flag = { marginLeft: '2px', color: muted56, fontSize: '0.72em', fontVariantNumeric: 'tabular-nums', lineHeight: 0, verticalAlign: 'super' }; + const legalFlag = { marginLeft: '2px', fontSize: '0.72em', lineHeight: 0, verticalAlign: 'super' }; + const footerP = { margin: 0 }; + const details = [ + ['Statement period', '09/01/2026 – 09/30/2026'], + ['Issued', '10/01/2026'], + ['Account holder', 'Marcus Chen'], + ['Account type', 'Consumer prepaid account'], + ['Account number', '****4821'], + ]; + const rows = [ + { id: 'c1', day: '03', type: 'ACH deposit', party: 'Acme Corp Payroll', amount: '+$1,850.00', disputable: true }, + { id: 'c2', day: '08', type: 'Debit card purchase', party: 'Blue Bottle Coffee', terminal: 'Los Angeles, CA', amount: '-$18.75', disputable: true }, + { id: 'c3', day: '12', type: 'ACH debit', party: 'Pacific Gas & Electric', amount: '-$142.30', disputable: true }, + { id: 'c4', day: '18', type: 'Wire transfer out', party: 'First National Escrow', amount: '-$1,000.00', disputable: false }, + { id: 'c4-fee', day: '18', type: 'Wire transfer fee', party: 'Lead Bank', amount: '-$15.00', disputable: false }, + { id: 'c5', day: '24', type: 'RTP received', party: 'Sofía Herrera', amount: '+$250.00', disputable: false }, + ]; + const noticeSteps = [ + '(1) Tell us your name and account number (if any).', + '(2) Describe the error or the transfer you are unsure about, and explain as clearly as you can why you believe it is an error or why you need more information.', + '(3) Tell us the dollar amount of the suspected error.', + ]; + return ( +
+
+ +
+
+ +
+ September statement +
+ +
+ {details.map((row) => ( +
+ {row[0]} + {row[1]} +
+ ))} +
+ Opening balance + $2,450.00USD +
+
+ Closing balance + $3,373.95USD +
+
+ +
+
+ Transactions + Amount +
+ {rows.map((row) => ( +
+ 09/{row.day} + + {row.type} + {row.party} + {row.terminal ? · {row.terminal} : null} + + + {row.amount} + {row.disputable ? * : null} + +
+ ))} +
+ Total fees for period + $15.00 +
+
* See below in case of errors or questions
+
+ +
+
+
+ In case of errors or questions about your electronic transfers* +
+

Telephone us at (855) 516-0103 or Write us at 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 as soon as you can, if you think your statement or receipt is wrong or if you need more information about a transfer on the statement or receipt. We must hear from you no later than 60 days after we sent you the FIRST statement on which the error or problem appeared.

+
    + {noticeSteps.map((step) => ( +
  1. {step}
  2. + ))} +
+

We will investigate your complaint and will correct any error promptly. If we take more than 10 business days to do this, we will credit your account for the amount you think is in error, so that you will have the use of the money during the time it takes us to complete our investigation.

+

Report errors within 60 days after we send this statement.

+
+

This account is held at Lead Bank, the account-holding institution. Lightspark is the program manager and is not a bank.

+
+ +
+
+ ); +}; From 9f8ab8040315a699b1e5d63df48c7bb64a382c52 Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 16:28:40 -0700 Subject: [PATCH 02/14] docs(cards): fix statement example ledger math Only settled card rows and completed payments become statement lines. Outgoing fees are shown as a separate line inside sentAmount instead of being added on top. Amounts stay integers until render, query values are URL-encoded, and the example checks that opening balance plus lines equals closing balance. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 107 +++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index 75144c438..68776aad9 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -86,12 +86,12 @@ Statement data comes from three calls: [Get customer by ID](/api-reference/custo | Transaction type | `type` (`INCOMING` / `OUTGOING` / `CARD`) and `direction` (`CREDIT` / `DEBIT`); on `OUTGOING` transactions, `paymentRail` names the rail | | Payee or merchant | `counterpartyInformation` or `description` on `INCOMING` and `OUTGOING` transactions; `merchant.descriptor` on `CARD` transactions | | Terminal location | `merchant.city` and `merchant.state` on `CARD` transactions, when present | -| Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`. Sign the amount by `direction`: a `CREDIT` adds to the balance and a `DEBIT` subtracts from it | -| Fee line items | `fees` on `INCOMING` and `OUTGOING` transactions, in the smallest unit of the sending currency. Card transactions carry no `fees` field | -| Total fees for the period | Sum of `fees` across the period's transactions | +| Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`. A card row is a statement line only once `settledAmount` is present (`status` is `SETTLED` or `PARTIALLY_SETTLED`); an `AUTHORIZED` hold is not a statement line. Sign the amount by `direction`: a `CREDIT` adds to the balance and a `DEBIT` subtracts from it | +| Fee line items | `fees` on `OUTGOING` transactions, in the smallest unit of the sending currency. `fees` is included in `sentAmount`, so show `sentAmount.amount - fees` as the transfer line and `fees` as its own line. On `INCOMING` transactions, `fees` is denominated in the sending currency and is deducted before `receivedAmount` lands, so it is not a line on this account's statement. Card transactions carry no `fees` field | +| Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions | -Exclude `CARD` transactions with `status: DECLINED`; no money moved. A `VOIDED` card transaction moved no money either and has no `settledAmount`. A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). +Only settled money becomes a statement line. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). ## Example: build a statement at period close @@ -99,11 +99,14 @@ Exclude `CARD` transactions with `status: DECLINED`; no money moved. A `VOIDED` At period close, read the account holder and account, list the period's transactions, derive the lines and totals, render the statement, and hand it to your delivery channel. ```javascript +// `grid` is your HTTP client for https://api.lightspark.com/grid/2025-10-13 +// with Basic auth. Amounts stay integers in the smallest unit until render. + // Run once per account when the statement period closes async function buildStatement(customerId, accountId, startDate, endDate) { const customer = await grid.get(`/customers/${customerId}`); const { data: accounts } = await grid.get( - `/customers/internal-accounts?customerId=${customerId}¤cy=USD` + `/customers/internal-accounts?${new URLSearchParams({ customerId, currency: 'USD' })}` ); const account = accounts.find((a) => a.id === accountId); @@ -111,51 +114,75 @@ async function buildStatement(customerId, accountId, startDate, endDate) { const transactions = []; let cursor; do { - const page = await grid.get( - `/transactions?accountIdentifier=${accountId}&startDate=${startDate}` + - `&endDate=${endDate}&sortOrder=asc&limit=100` + - (cursor ? `&cursor=${cursor}` : '') - ); + const query = new URLSearchParams({ + accountIdentifier: accountId, startDate, endDate, sortOrder: 'asc', limit: '100', + }); + if (cursor) query.set('cursor', cursor); + const page = await grid.get(`/transactions?${query}`); transactions.push(...page.data); cursor = page.hasMore ? page.nextCursor : undefined; } while (cursor); - const format = (money) => - (money.amount / 10 ** money.currency.decimals).toFixed(money.currency.decimals); - + // One line per movement of settled money, signed by direction const lines = []; - let totalFees = 0; for (const tx of transactions) { - if (tx.type === 'CARD' && (tx.status === 'DECLINED' || tx.status === 'VOIDED')) continue; - - const money = - tx.type === 'INCOMING' ? tx.receivedAmount : - tx.type === 'OUTGOING' ? tx.sentAmount : - tx.settledAmount; const sign = tx.direction === 'CREDIT' ? 1 : -1; + if (tx.type === 'CARD') { + if (!tx.settledAmount) continue; // AUTHORIZED hold, DECLINED, or VOIDED + lines.push({ + date: tx.authorizedAt, + type: 'CARD', + direction: tx.direction, + payee: tx.merchant.descriptor, + terminalLocation: + tx.merchant.city && tx.merchant.state + ? `${tx.merchant.city}, ${tx.merchant.state}` + : undefined, + amount: sign * tx.settledAmount.amount, + }); + continue; + } + + if (tx.status !== 'COMPLETED') continue; + + if (tx.type === 'INCOMING') { + lines.push({ + date: tx.settledAt, + type: 'INCOMING', + direction: tx.direction, + payee: tx.description, + amount: sign * tx.receivedAmount.amount, + }); + continue; + } + + // OUTGOING: sentAmount already includes fees, so split it into two lines + const fees = tx.fees ?? 0; lines.push({ - date: tx.type === 'CARD' ? tx.authorizedAt : tx.settledAt, - type: tx.type, + date: tx.settledAt, + type: 'OUTGOING', direction: tx.direction, - payee: tx.type === 'CARD' ? tx.merchant.descriptor : tx.description, - terminalLocation: - tx.type === 'CARD' && tx.merchant.city && tx.merchant.state - ? `${tx.merchant.city}, ${tx.merchant.state}` - : undefined, - amount: sign * Number(format(money)), + payee: tx.description, + amount: sign * (tx.sentAmount.amount - fees), }); - - if (tx.fees) { - // fees is an integer in the smallest unit of the sending currency - const fee = tx.fees / 10 ** money.currency.decimals; - totalFees += fee; - lines.push({ date: tx.settledAt, type: 'FEE', amount: -fee }); + if (fees > 0) { + lines.push({ date: tx.settledAt, type: 'FEE', direction: tx.direction, amount: sign * fees }); } } - const closingBalance = Number(format(account.balance)); // read at period close - const openingBalance = await previousClosingBalance(accountId); // from your records + const closingBalance = account.balance.amount; // read at period close + const openingBalance = await previousClosingBalance(accountId); // from your records, same unit + const movement = lines.reduce((sum, line) => sum + line.amount, 0); + if (openingBalance + movement !== closingBalance) { + throw new Error('Statement does not reconcile: opening balance plus lines is not the closing balance'); + } + const totalFees = -lines + .filter((line) => line.type === 'FEE') + .reduce((sum, line) => sum + line.amount, 0); + + const { currency } = account.balance; + const format = (amount) => (amount / 10 ** currency.decimals).toFixed(currency.decimals); const statement = { // Fixed disclosures @@ -169,10 +196,10 @@ async function buildStatement(customerId, accountId, startDate, endDate) { accountNumber: maskAccountNumber(account.fundingPaymentInstructions), period: { startDate, endDate }, issueDate: new Date().toISOString(), - openingBalance, - closingBalance, - lines, - totalFees, + openingBalance: format(openingBalance), + closingBalance: format(closingBalance), + lines: lines.map((line) => ({ ...line, amount: format(line.amount) })), + totalFees: format(totalFees), }; // Delivery and the delivery record follow your platform agreement From 300477d31ee60c8a89fadf2bc6fa62c8c3b5329e Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 16:40:49 -0700 Subject: [PATCH 03/14] docs(cards): harden statement example lookups and card filter Page through the customer's internal accounts instead of reading the first USD page, and throw when the account is not found. Card rows become lines only when status is SETTLED or PARTIALLY_SETTLED, so EXCEPTION rows stay off the statement until reconciled. The statement object now carries the error-resolution notice with the other fixed disclosures. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 46 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index 68776aad9..4d0697e7b 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -91,7 +91,7 @@ Statement data comes from three calls: [Get customer by ID](/api-reference/custo | Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions | -Only settled money becomes a statement line. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). +Only settled money becomes a statement line. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. An `EXCEPTION` row settled at the card network but the pull from the account failed, so keep it off the statement until it is reconciled. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). ## Example: build a statement at period close @@ -102,34 +102,41 @@ At period close, read the account holder and account, list the period's transact // `grid` is your HTTP client for https://api.lightspark.com/grid/2025-10-13 // with Basic auth. Amounts stay integers in the smallest unit until render. -// Run once per account when the statement period closes -async function buildStatement(customerId, accountId, startDate, endDate) { - const customer = await grid.get(`/customers/${customerId}`); - const { data: accounts } = await grid.get( - `/customers/internal-accounts?${new URLSearchParams({ customerId, currency: 'USD' })}` - ); - const account = accounts.find((a) => a.id === accountId); - - // Page through every transaction in the period, oldest first - const transactions = []; +// Yield every item of a paginated list, following nextCursor while hasMore is true +async function* listAll(path, params) { let cursor; do { - const query = new URLSearchParams({ - accountIdentifier: accountId, startDate, endDate, sortOrder: 'asc', limit: '100', - }); + const query = new URLSearchParams({ ...params, limit: '100' }); if (cursor) query.set('cursor', cursor); - const page = await grid.get(`/transactions?${query}`); - transactions.push(...page.data); + const page = await grid.get(`${path}?${query}`); + yield* page.data; cursor = page.hasMore ? page.nextCursor : undefined; } while (cursor); +} + +// Run once per account when the statement period closes +async function buildStatement(customerId, accountId, startDate, endDate) { + const customer = await grid.get(`/customers/${customerId}`); + + let account; + for await (const candidate of listAll('/customers/internal-accounts', { customerId })) { + if (candidate.id === accountId) { + account = candidate; + break; + } + } + if (!account) throw new Error(`Account ${accountId} does not belong to customer ${customerId}`); - // One line per movement of settled money, signed by direction + // One line per movement of settled money, signed by direction, oldest first const lines = []; - for (const tx of transactions) { + const period = { accountIdentifier: accountId, startDate, endDate, sortOrder: 'asc' }; + for await (const tx of listAll('/transactions', period)) { const sign = tx.direction === 'CREDIT' ? 1 : -1; if (tx.type === 'CARD') { - if (!tx.settledAmount) continue; // AUTHORIZED hold, DECLINED, or VOIDED + // Only cleared rows moved money. AUTHORIZED is a hold, DECLINED and VOIDED + // moved nothing, and EXCEPTION needs manual reconciliation first + if (tx.status !== 'SETTLED' && tx.status !== 'PARTIALLY_SETTLED') continue; lines.push({ date: tx.authorizedAt, type: 'CARD', @@ -191,6 +198,7 @@ async function buildStatement(customerId, accountId, startDate, endDate) { 'Lightspark is the program manager and is not a bank.', customerServicePhone: '(855) 516-0103', address: '8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069', + errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // the full notice from the disclosures table // Account fields accountHolder: customer.fullName, accountNumber: maskAccountNumber(account.fundingPaymentInstructions), From 2299f4f7ac743f8c43ebb5f09ada3cb6734b2516 Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 17:03:27 -0700 Subject: [PATCH 04/14] docs(cards): name the reader-supplied notice constant in the example The header comment now lists ERROR_RESOLUTION_NOTICE with grid and the delivery helpers as values the reader supplies, and points to the disclosures table for its text. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index 4d0697e7b..c1cc4577b 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -100,7 +100,9 @@ At period close, read the account holder and account, list the period's transact ```javascript // `grid` is your HTTP client for https://api.lightspark.com/grid/2025-10-13 -// with Basic auth. Amounts stay integers in the smallest unit until render. +// with Basic auth. ERROR_RESOLUTION_NOTICE holds the notice text from the +// disclosures table above, and the delivery helpers are yours as well. +// Amounts stay integers in the smallest unit until render. // Yield every item of a paginated list, following nextCursor while hasMore is true async function* listAll(path, params) { From 220d762d343cda266ed29df79dc6e3636f5adb8f Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 17:23:27 -0700 Subject: [PATCH 05/14] docs(cards): align statements guide with the statement definition State the monthly cadence for every account, list non-covered transfers without the disputable marker, order the provider block as program manager then bank, use the ledger balance for opening and closing figures, and fetch card rows by customer or card id. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 74 ++++++++++--------- mintlify/snippets/cards/statement-example.mdx | 4 +- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index c1cc4577b..1a902cd93 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -6,9 +6,9 @@ icon: "/images/icons/receipt-check.svg" import { StatementExample } from '/snippets/cards/statement-example.mdx'; -If you issue cards through Grid, each cardholder account is held at **Lead Bank**, the account-holding institution, and Lightspark is the program manager. Regulation E requires a periodic statement for every consumer account that can send or receive electronic fund transfers. +If you issue cards through Grid, Lightspark is the program manager for each cardholder account, and the account is held at **Lead Bank**, the account-holding institution. Regulation E requires a periodic statement for every consumer account that can send or receive electronic fund transfers. -This guide explains when a statement is sent, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. It covers consumer cardholder accounts. +This guide explains when a statement is sent, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. It covers consumer cardholder accounts. A commercial statement uses the same layout without the consumer-only items: the error-resolution notice, the asterisk markers, and the terminal location. Periodic statements are a regulatory requirement for consumer card accounts. Your platform agreement sets out who prepares and delivers them. Use this page as a reference for what a statement contains and how to build it from Grid transaction data. @@ -20,21 +20,16 @@ Your statement's visual design is up to you, but the content and field set are f -The asterisk marks the transfers the error-resolution notice covers, and the same asterisk keys the notice heading. The marker is a reading aid, not a required field. +The asterisk marks the transfers the error-resolution notice covers, and the same asterisk keys the notice heading. RTP, wire, FedNow, and crypto or stablecoin transfers are listed so the balances reconcile, but they are not marked disputable. The marker is a reading aid, not a required field. ## When a statement is sent -A statement period closes once a month, at the same point in each month. Then: +A statement period closes once a month, at the same point in each month. A statement is issued monthly for every account, with or without activity in the period. -- **A statement goes out for every month with electronic fund transfer activity**, for example an ACH deposit, an ACH debit, or a debit card purchase. -- **A statement goes out at least quarterly** for accounts with no electronic fund transfer activity in the month. - -A monthly statement for every open account satisfies both rules, so there is no need for a separate quarterly variant. - -The statement goes to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** the statement (for example, a downloadable PDF or an email they can save). Record the issue date. The cardholder's window to report an error runs from the issue date of the first statement showing the error. Your platform agreement sets out who delivers the statement and who keeps the delivery record. +The statement goes to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** the statement (for example, a downloadable PDF or an email they can save). Record the issue date. The cardholder must report an error within 60 days of the statement date, counted from the first statement showing the error. Your platform agreement sets out who delivers the statement and who keeps the delivery record. -List every transaction that moved the balance in the period, including transfers that Regulation E does not cover, such as wires. Without them, the opening and closing balances would not reconcile. Once a statement is issued, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. +List every transaction that moved the balance in the period, including transfers that Regulation E does not cover, such as wires and RTP. Without them, the opening and closing balances would not reconcile. Once a statement is issued, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. ## What a statement must contain @@ -43,14 +38,13 @@ A statement combines **fixed disclosures** (provider and error-resolution langua ### Lightspark and Lead Bank disclosures -Every consumer statement carries these disclosures exactly as written: +Every consumer statement carries these disclosures exactly as written. The statement names Lightspark as the program manager and never as a bank. | Field | Value | |-------|-------| -| Provider line | "This account is held at Lead Bank, the account-holding institution. Lightspark is the program manager and is not a bank." | -| Customer service phone | (855) 516-0103 | -| Address for inquiries | 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 | -| Error-resolution notice | The full notice shown in the sample above, headed "In case of errors or questions about your electronic transfers", with the phone number and address above and the 60-day reporting window | +| Provider line | "Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution." | +| Contact line | "Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103." | +| Error-resolution notice | The full notice shown in the sample above, headed "In case of errors or questions about your electronic transfers", with the address and phone number above and the closing line "Report errors within 60 days of the statement date." | ### Statement fields @@ -60,8 +54,8 @@ Every consumer statement carries these disclosures exactly as written: | Account number | Number that identifies the account, masked to the last four digits | Always | | Statement period | Start and end date of the period | Always | | Issue date | Date the statement is sent | Always | -| Opening balance | Balance at the start of the period | Always | -| Closing balance | Balance at the end of the period | Always | +| Opening balance | Ledger balance at the start of the period | Always | +| Closing balance | Ledger balance at the end of the period | Always | | Transaction date | Date each transaction posted to the account | Always | | Transaction type | Kind of transfer, for example ACH deposit, debit card purchase, wire transfer out | Always | | Payee or merchant | Counterparty name, or the merchant descriptor for a card purchase | Always | @@ -72,7 +66,7 @@ Every consumer statement carries these disclosures exactly as written: ## Mapping Grid data to statement fields -Statement data comes from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Filter the transaction list by `accountIdentifier`, `startDate`, and `endDate`, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. +Statement data comes from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Bound the period with `startDate` and `endDate`, both inclusive, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. List payment transactions with `accountIdentifier`. List card rows with `customerId` or `cardId`, and keep the rows whose `accountId` is the statement account; do not combine `status` with `type=CARD`, because the request is rejected. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. | Statement field | Grid source | |-----------------|-------------| @@ -80,18 +74,18 @@ Statement data comes from three calls: [Get customer by ID](/api-reference/custo | Account number | `fundingPaymentInstructions[].accountOrWalletInfo.accountNumber` on the internal account, masked to the last four digits | | Statement period | The `startDate` and `endDate` you pass to List transactions | | Issue date | You supply it: the date the statement is sent | -| Closing balance | `balance` on the internal account, read at period close. Grid returns the current balance only, so read it when the period closes and store it | +| Closing balance | `totalBalance` on the internal account, read at period close. This is the ledger figure and includes pending and held funds; `balance` excludes them. Grid returns the current figure only, so read it when the period closes and store it | | Opening balance | The previous period's closing balance from your records | -| Transaction date | `settledAt` on `INCOMING` and `OUTGOING` transactions; `authorizedAt` on `CARD` transactions | +| Transaction date | `settledAt` on `INCOMING` and `OUTGOING` transactions; `authorizedAt` on `CARD` transactions. `CardTransaction` has no settlement timestamp, so the authorization date dates the card line | | Transaction type | `type` (`INCOMING` / `OUTGOING` / `CARD`) and `direction` (`CREDIT` / `DEBIT`); on `OUTGOING` transactions, `paymentRail` names the rail | | Payee or merchant | `counterpartyInformation` or `description` on `INCOMING` and `OUTGOING` transactions; `merchant.descriptor` on `CARD` transactions | | Terminal location | `merchant.city` and `merchant.state` on `CARD` transactions, when present | | Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`. A card row is a statement line only once `settledAmount` is present (`status` is `SETTLED` or `PARTIALLY_SETTLED`); an `AUTHORIZED` hold is not a statement line. Sign the amount by `direction`: a `CREDIT` adds to the balance and a `DEBIT` subtracts from it | | Fee line items | `fees` on `OUTGOING` transactions, in the smallest unit of the sending currency. `fees` is included in `sentAmount`, so show `sentAmount.amount - fees` as the transfer line and `fees` as its own line. On `INCOMING` transactions, `fees` is denominated in the sending currency and is deducted before `receivedAmount` lands, so it is not a line on this account's statement. Card transactions carry no `fees` field | -| Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions | +| Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions. Card rows have no `fees` field, so this total covers payment transactions only | -Only settled money becomes a statement line. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. An `EXCEPTION` row settled at the card network but the pull from the account failed, so keep it off the statement until it is reconciled. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). +Only settled money becomes a statement line. The opening and closing balances are the ledger figures (`totalBalance`), so settled card rows and completed payments reconcile against them, and holds do not appear as lines. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. A `PARTIALLY_SETTLED` card row is listed at its `settledAmount` as of period close, and later clearings appear on the next statement as their own lines. An `EXCEPTION` row settled at the card network but the pull from the account failed, so keep it off the statement until it is reconciled. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). ## Example: build a statement at period close @@ -129,10 +123,21 @@ async function buildStatement(customerId, accountId, startDate, endDate) { } if (!account) throw new Error(`Account ${accountId} does not belong to customer ${customerId}`); - // One line per movement of settled money, signed by direction, oldest first + // Payment rows are listed by account. Card rows are listed by cardholder and + // kept when this account funded them. Do not combine status with type=CARD + const window = { startDate, endDate, sortOrder: 'asc' }; + async function* periodRows() { + for await (const tx of listAll('/transactions', { ...window, accountIdentifier: accountId })) { + if (tx.type !== 'CARD') yield tx; + } + for await (const tx of listAll('/transactions', { ...window, customerId, type: 'CARD' })) { + if (tx.accountId === accountId) yield tx; + } + } + + // One line per movement of settled money, signed by direction const lines = []; - const period = { accountIdentifier: accountId, startDate, endDate, sortOrder: 'asc' }; - for await (const tx of listAll('/transactions', period)) { + for await (const tx of periodRows()) { const sign = tx.direction === 'CREDIT' ? 1 : -1; if (tx.type === 'CARD') { @@ -179,8 +184,10 @@ async function buildStatement(customerId, accountId, startDate, endDate) { lines.push({ date: tx.settledAt, type: 'FEE', direction: tx.direction, amount: sign * fees }); } } + lines.sort((a, b) => a.date.localeCompare(b.date)); // oldest first across both lists - const closingBalance = account.balance.amount; // read at period close + // The ledger balance, including pending and held funds, read at period close + const closingBalance = account.totalBalance.amount; const openingBalance = await previousClosingBalance(accountId); // from your records, same unit const movement = lines.reduce((sum, line) => sum + line.amount, 0); if (openingBalance + movement !== closingBalance) { @@ -190,16 +197,17 @@ async function buildStatement(customerId, accountId, startDate, endDate) { .filter((line) => line.type === 'FEE') .reduce((sum, line) => sum + line.amount, 0); - const { currency } = account.balance; + const { currency } = account.totalBalance; const format = (amount) => (amount / 10 ** currency.decimals).toFixed(currency.decimals); const statement = { - // Fixed disclosures + // Fixed disclosures, in statement order providerLine: - 'This account is held at Lead Bank, the account-holding institution. ' + - 'Lightspark is the program manager and is not a bank.', - customerServicePhone: '(855) 516-0103', - address: '8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069', + 'Lightspark is the program manager for this account. ' + + 'This account is held at Lead Bank, the account-holding institution.', + contactLine: + 'Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 ' + + 'or (855) 516-0103.', errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // the full notice from the disclosures table // Account fields accountHolder: customer.fullName, diff --git a/mintlify/snippets/cards/statement-example.mdx b/mintlify/snippets/cards/statement-example.mdx index 80c1f4fee..4af986836 100644 --- a/mintlify/snippets/cards/statement-example.mdx +++ b/mintlify/snippets/cards/statement-example.mdx @@ -104,9 +104,9 @@ export const StatementExample = () => { ))}

We will investigate your complaint and will correct any error promptly. If we take more than 10 business days to do this, we will credit your account for the amount you think is in error, so that you will have the use of the money during the time it takes us to complete our investigation.

-

Report errors within 60 days after we send this statement.

+

Report errors within 60 days of the statement date.

-

This account is held at Lead Bank, the account-holding institution. Lightspark is the program manager and is not a bank.

+

Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution. Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.

From dfe87f2706584b10e0406cc11c35b1c4a37fd585 Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 17:27:28 -0700 Subject: [PATCH 06/14] docs(cards): use typographic quotes in the disclosures table Mintlify turned straight quotes at the start of a table cell into closing quotes. Use explicit opening and closing quotes instead. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index 1a902cd93..1f9e0fce1 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -42,9 +42,9 @@ Every consumer statement carries these disclosures exactly as written. The state | Field | Value | |-------|-------| -| Provider line | "Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution." | -| Contact line | "Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103." | -| Error-resolution notice | The full notice shown in the sample above, headed "In case of errors or questions about your electronic transfers", with the address and phone number above and the closing line "Report errors within 60 days of the statement date." | +| Provider line | “Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution.” | +| Contact line | “Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.” | +| Error-resolution notice | The full notice shown in the sample above, headed “In case of errors or questions about your electronic transfers”, with the address and phone number above and the closing line “Report errors within 60 days of the statement date.” | ### Statement fields From 644ff6e8dc3f53b773489afa87415d314a84e3c2 Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 17:35:45 -0700 Subject: [PATCH 07/14] docs: list periodic statements under Payouts & B2B Move the guide body into a shared snippet and add a second entry under Sending payments after List transactions, following the Receipts page pattern. The Cards entry stays. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 222 +----------------- mintlify/docs.json | 1 + .../payment-flow/statements.mdx | 10 + mintlify/snippets/statements.mdx | 221 +++++++++++++++++ 4 files changed, 234 insertions(+), 220 deletions(-) create mode 100644 mintlify/payouts-and-b2b/payment-flow/statements.mdx create mode 100644 mintlify/snippets/statements.mdx diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index 1f9e0fce1..1afd4a72e 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -4,224 +4,6 @@ description: "What a consumer card periodic statement contains, and how to build icon: "/images/icons/receipt-check.svg" --- -import { StatementExample } from '/snippets/cards/statement-example.mdx'; +import Statements from '/snippets/statements.mdx' -If you issue cards through Grid, Lightspark is the program manager for each cardholder account, and the account is held at **Lead Bank**, the account-holding institution. Regulation E requires a periodic statement for every consumer account that can send or receive electronic fund transfers. - -This guide explains when a statement is sent, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. It covers consumer cardholder accounts. A commercial statement uses the same layout without the consumer-only items: the error-resolution notice, the asterisk markers, and the terminal location. - - -Periodic statements are a regulatory requirement for consumer card accounts. Your platform agreement sets out who prepares and delivers them. Use this page as a reference for what a statement contains and how to build it from Grid transaction data. - - -## Sample statement layout - -Your statement's visual design is up to you, but the content and field set are fixed. The layout below shows a consumer statement with sample data filled in: - - - -The asterisk marks the transfers the error-resolution notice covers, and the same asterisk keys the notice heading. RTP, wire, FedNow, and crypto or stablecoin transfers are listed so the balances reconcile, but they are not marked disputable. The marker is a reading aid, not a required field. - -## When a statement is sent - -A statement period closes once a month, at the same point in each month. A statement is issued monthly for every account, with or without activity in the period. - -The statement goes to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** the statement (for example, a downloadable PDF or an email they can save). Record the issue date. The cardholder must report an error within 60 days of the statement date, counted from the first statement showing the error. Your platform agreement sets out who delivers the statement and who keeps the delivery record. - - -List every transaction that moved the balance in the period, including transfers that Regulation E does not cover, such as wires and RTP. Without them, the opening and closing balances would not reconcile. Once a statement is issued, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. - - -## What a statement must contain - -A statement combines **fixed disclosures** (provider and error-resolution language) with **data-driven fields** populated from the account and its transactions for the period. - -### Lightspark and Lead Bank disclosures - -Every consumer statement carries these disclosures exactly as written. The statement names Lightspark as the program manager and never as a bank. - -| Field | Value | -|-------|-------| -| Provider line | “Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution.” | -| Contact line | “Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.” | -| Error-resolution notice | The full notice shown in the sample above, headed “In case of errors or questions about your electronic transfers”, with the address and phone number above and the closing line “Report errors within 60 days of the statement date.” | - -### Statement fields - -| Field | Description | Required when | -|-------|-------------|---------------| -| Account holder | Name of the cardholder | Always | -| Account number | Number that identifies the account, masked to the last four digits | Always | -| Statement period | Start and end date of the period | Always | -| Issue date | Date the statement is sent | Always | -| Opening balance | Ledger balance at the start of the period | Always | -| Closing balance | Ledger balance at the end of the period | Always | -| Transaction date | Date each transaction posted to the account | Always | -| Transaction type | Kind of transfer, for example ACH deposit, debit card purchase, wire transfer out | Always | -| Payee or merchant | Counterparty name, or the merchant descriptor for a card purchase | Always | -| Terminal location | Merchant city and state | Card purchases at a merchant terminal, when the card network reports a location | -| Transaction amount | Signed amount in the account currency | Always | -| Fee line items | Each fee charged in the period, as its own line | When a fee was charged | -| Total fees for the period | Sum of all fees charged in the period | Always | - -## Mapping Grid data to statement fields - -Statement data comes from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Bound the period with `startDate` and `endDate`, both inclusive, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. List payment transactions with `accountIdentifier`. List card rows with `customerId` or `cardId`, and keep the rows whose `accountId` is the statement account; do not combine `status` with `type=CARD`, because the request is rejected. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. - -| Statement field | Grid source | -|-----------------|-------------| -| Account holder | `fullName` on the customer (`customerType: INDIVIDUAL`) | -| Account number | `fundingPaymentInstructions[].accountOrWalletInfo.accountNumber` on the internal account, masked to the last four digits | -| Statement period | The `startDate` and `endDate` you pass to List transactions | -| Issue date | You supply it: the date the statement is sent | -| Closing balance | `totalBalance` on the internal account, read at period close. This is the ledger figure and includes pending and held funds; `balance` excludes them. Grid returns the current figure only, so read it when the period closes and store it | -| Opening balance | The previous period's closing balance from your records | -| Transaction date | `settledAt` on `INCOMING` and `OUTGOING` transactions; `authorizedAt` on `CARD` transactions. `CardTransaction` has no settlement timestamp, so the authorization date dates the card line | -| Transaction type | `type` (`INCOMING` / `OUTGOING` / `CARD`) and `direction` (`CREDIT` / `DEBIT`); on `OUTGOING` transactions, `paymentRail` names the rail | -| Payee or merchant | `counterpartyInformation` or `description` on `INCOMING` and `OUTGOING` transactions; `merchant.descriptor` on `CARD` transactions | -| Terminal location | `merchant.city` and `merchant.state` on `CARD` transactions, when present | -| Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`. A card row is a statement line only once `settledAmount` is present (`status` is `SETTLED` or `PARTIALLY_SETTLED`); an `AUTHORIZED` hold is not a statement line. Sign the amount by `direction`: a `CREDIT` adds to the balance and a `DEBIT` subtracts from it | -| Fee line items | `fees` on `OUTGOING` transactions, in the smallest unit of the sending currency. `fees` is included in `sentAmount`, so show `sentAmount.amount - fees` as the transfer line and `fees` as its own line. On `INCOMING` transactions, `fees` is denominated in the sending currency and is deducted before `receivedAmount` lands, so it is not a line on this account's statement. Card transactions carry no `fees` field | -| Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions. Card rows have no `fees` field, so this total covers payment transactions only | - - -Only settled money becomes a statement line. The opening and closing balances are the ledger figures (`totalBalance`), so settled card rows and completed payments reconcile against them, and holds do not appear as lines. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. A `PARTIALLY_SETTLED` card row is listed at its `settledAmount` as of period close, and later clearings appear on the next statement as their own lines. An `EXCEPTION` row settled at the card network but the pull from the account failed, so keep it off the statement until it is reconciled. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). - - -## Example: build a statement at period close - -At period close, read the account holder and account, list the period's transactions, derive the lines and totals, render the statement, and hand it to your delivery channel. - -```javascript -// `grid` is your HTTP client for https://api.lightspark.com/grid/2025-10-13 -// with Basic auth. ERROR_RESOLUTION_NOTICE holds the notice text from the -// disclosures table above, and the delivery helpers are yours as well. -// Amounts stay integers in the smallest unit until render. - -// Yield every item of a paginated list, following nextCursor while hasMore is true -async function* listAll(path, params) { - let cursor; - do { - const query = new URLSearchParams({ ...params, limit: '100' }); - if (cursor) query.set('cursor', cursor); - const page = await grid.get(`${path}?${query}`); - yield* page.data; - cursor = page.hasMore ? page.nextCursor : undefined; - } while (cursor); -} - -// Run once per account when the statement period closes -async function buildStatement(customerId, accountId, startDate, endDate) { - const customer = await grid.get(`/customers/${customerId}`); - - let account; - for await (const candidate of listAll('/customers/internal-accounts', { customerId })) { - if (candidate.id === accountId) { - account = candidate; - break; - } - } - if (!account) throw new Error(`Account ${accountId} does not belong to customer ${customerId}`); - - // Payment rows are listed by account. Card rows are listed by cardholder and - // kept when this account funded them. Do not combine status with type=CARD - const window = { startDate, endDate, sortOrder: 'asc' }; - async function* periodRows() { - for await (const tx of listAll('/transactions', { ...window, accountIdentifier: accountId })) { - if (tx.type !== 'CARD') yield tx; - } - for await (const tx of listAll('/transactions', { ...window, customerId, type: 'CARD' })) { - if (tx.accountId === accountId) yield tx; - } - } - - // One line per movement of settled money, signed by direction - const lines = []; - for await (const tx of periodRows()) { - const sign = tx.direction === 'CREDIT' ? 1 : -1; - - if (tx.type === 'CARD') { - // Only cleared rows moved money. AUTHORIZED is a hold, DECLINED and VOIDED - // moved nothing, and EXCEPTION needs manual reconciliation first - if (tx.status !== 'SETTLED' && tx.status !== 'PARTIALLY_SETTLED') continue; - lines.push({ - date: tx.authorizedAt, - type: 'CARD', - direction: tx.direction, - payee: tx.merchant.descriptor, - terminalLocation: - tx.merchant.city && tx.merchant.state - ? `${tx.merchant.city}, ${tx.merchant.state}` - : undefined, - amount: sign * tx.settledAmount.amount, - }); - continue; - } - - if (tx.status !== 'COMPLETED') continue; - - if (tx.type === 'INCOMING') { - lines.push({ - date: tx.settledAt, - type: 'INCOMING', - direction: tx.direction, - payee: tx.description, - amount: sign * tx.receivedAmount.amount, - }); - continue; - } - - // OUTGOING: sentAmount already includes fees, so split it into two lines - const fees = tx.fees ?? 0; - lines.push({ - date: tx.settledAt, - type: 'OUTGOING', - direction: tx.direction, - payee: tx.description, - amount: sign * (tx.sentAmount.amount - fees), - }); - if (fees > 0) { - lines.push({ date: tx.settledAt, type: 'FEE', direction: tx.direction, amount: sign * fees }); - } - } - lines.sort((a, b) => a.date.localeCompare(b.date)); // oldest first across both lists - - // The ledger balance, including pending and held funds, read at period close - const closingBalance = account.totalBalance.amount; - const openingBalance = await previousClosingBalance(accountId); // from your records, same unit - const movement = lines.reduce((sum, line) => sum + line.amount, 0); - if (openingBalance + movement !== closingBalance) { - throw new Error('Statement does not reconcile: opening balance plus lines is not the closing balance'); - } - const totalFees = -lines - .filter((line) => line.type === 'FEE') - .reduce((sum, line) => sum + line.amount, 0); - - const { currency } = account.totalBalance; - const format = (amount) => (amount / 10 ** currency.decimals).toFixed(currency.decimals); - - const statement = { - // Fixed disclosures, in statement order - providerLine: - 'Lightspark is the program manager for this account. ' + - 'This account is held at Lead Bank, the account-holding institution.', - contactLine: - 'Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 ' + - 'or (855) 516-0103.', - errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // the full notice from the disclosures table - // Account fields - accountHolder: customer.fullName, - accountNumber: maskAccountNumber(account.fundingPaymentInstructions), - period: { startDate, endDate }, - issueDate: new Date().toISOString(), - openingBalance: format(openingBalance), - closingBalance: format(closingBalance), - lines: lines.map((line) => ({ ...line, amount: format(line.amount) })), - totalFees: format(totalFees), - }; - - // Delivery and the delivery record follow your platform agreement - await sendStatementEmail(customer.platformCustomerId, statement); - await recordStatementDelivery(accountId, statement.issueDate); -} -``` + diff --git a/mintlify/docs.json b/mintlify/docs.json index 886a192f3..968399f71 100644 --- a/mintlify/docs.json +++ b/mintlify/docs.json @@ -156,6 +156,7 @@ "payouts-and-b2b/payment-flow/send-payment", "payouts-and-b2b/payment-flow/assessing-fees", "payouts-and-b2b/payment-flow/list-transactions", + "payouts-and-b2b/payment-flow/statements", "payouts-and-b2b/payment-flow/receipts", "payouts-and-b2b/payment-flow/reconciliation", "payouts-and-b2b/payment-flow/error-handling" diff --git a/mintlify/payouts-and-b2b/payment-flow/statements.mdx b/mintlify/payouts-and-b2b/payment-flow/statements.mdx new file mode 100644 index 000000000..9bdec65d0 --- /dev/null +++ b/mintlify/payouts-and-b2b/payment-flow/statements.mdx @@ -0,0 +1,10 @@ +--- +title: "Periodic statements" +description: "What a consumer card periodic statement contains, and how to build one from Grid transaction data" +icon: "/images/icons/receipt-check.svg" +"og:image": "/images/og/og-payouts-b2b.png" +--- + +import Statements from '/snippets/statements.mdx' + + diff --git a/mintlify/snippets/statements.mdx b/mintlify/snippets/statements.mdx new file mode 100644 index 000000000..d0181d6c8 --- /dev/null +++ b/mintlify/snippets/statements.mdx @@ -0,0 +1,221 @@ +import { StatementExample } from '/snippets/cards/statement-example.mdx'; + +If you issue cards through Grid, Lightspark is the program manager for each cardholder account, and the account is held at **Lead Bank**, the account-holding institution. Regulation E requires a periodic statement for every consumer account that can send or receive electronic fund transfers. + +This guide explains when a statement is sent, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. It covers consumer cardholder accounts. A commercial statement uses the same layout without the consumer-only items: the error-resolution notice, the asterisk markers, and the terminal location. + + +Periodic statements are a regulatory requirement for consumer card accounts. Your platform agreement sets out who prepares and delivers them. Use this page as a reference for what a statement contains and how to build it from Grid transaction data. + + +## Sample statement layout + +Your statement's visual design is up to you, but the content and field set are fixed. The layout below shows a consumer statement with sample data filled in: + + + +The asterisk marks the transfers the error-resolution notice covers, and the same asterisk keys the notice heading. RTP, wire, FedNow, and crypto or stablecoin transfers are listed so the balances reconcile, but they are not marked disputable. The marker is a reading aid, not a required field. + +## When a statement is sent + +A statement period closes once a month, at the same point in each month. A statement is issued monthly for every account, with or without activity in the period. + +The statement goes to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** the statement (for example, a downloadable PDF or an email they can save). Record the issue date. The cardholder must report an error within 60 days of the statement date, counted from the first statement showing the error. Your platform agreement sets out who delivers the statement and who keeps the delivery record. + + +List every transaction that moved the balance in the period, including transfers that Regulation E does not cover, such as wires and RTP. Without them, the opening and closing balances would not reconcile. Once a statement is issued, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. + + +## What a statement must contain + +A statement combines **fixed disclosures** (provider and error-resolution language) with **data-driven fields** populated from the account and its transactions for the period. + +### Lightspark and Lead Bank disclosures + +Every consumer statement carries these disclosures exactly as written. The statement names Lightspark as the program manager and never as a bank. + +| Field | Value | +|-------|-------| +| Provider line | “Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution.” | +| Contact line | “Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.” | +| Error-resolution notice | The full notice shown in the sample above, headed “In case of errors or questions about your electronic transfers”, with the address and phone number above and the closing line “Report errors within 60 days of the statement date.” | + +### Statement fields + +| Field | Description | Required when | +|-------|-------------|---------------| +| Account holder | Name of the cardholder | Always | +| Account number | Number that identifies the account, masked to the last four digits | Always | +| Statement period | Start and end date of the period | Always | +| Issue date | Date the statement is sent | Always | +| Opening balance | Ledger balance at the start of the period | Always | +| Closing balance | Ledger balance at the end of the period | Always | +| Transaction date | Date each transaction posted to the account | Always | +| Transaction type | Kind of transfer, for example ACH deposit, debit card purchase, wire transfer out | Always | +| Payee or merchant | Counterparty name, or the merchant descriptor for a card purchase | Always | +| Terminal location | Merchant city and state | Card purchases at a merchant terminal, when the card network reports a location | +| Transaction amount | Signed amount in the account currency | Always | +| Fee line items | Each fee charged in the period, as its own line | When a fee was charged | +| Total fees for the period | Sum of all fees charged in the period | Always | + +## Mapping Grid data to statement fields + +Statement data comes from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Bound the period with `startDate` and `endDate`, both inclusive, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. List payment transactions with `accountIdentifier`. List card rows with `customerId` or `cardId`, and keep the rows whose `accountId` is the statement account; do not combine `status` with `type=CARD`, because the request is rejected. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. + +| Statement field | Grid source | +|-----------------|-------------| +| Account holder | `fullName` on the customer (`customerType: INDIVIDUAL`) | +| Account number | `fundingPaymentInstructions[].accountOrWalletInfo.accountNumber` on the internal account, masked to the last four digits | +| Statement period | The `startDate` and `endDate` you pass to List transactions | +| Issue date | You supply it: the date the statement is sent | +| Closing balance | `totalBalance` on the internal account, read at period close. This is the ledger figure and includes pending and held funds; `balance` excludes them. Grid returns the current figure only, so read it when the period closes and store it | +| Opening balance | The previous period's closing balance from your records | +| Transaction date | `settledAt` on `INCOMING` and `OUTGOING` transactions; `authorizedAt` on `CARD` transactions. `CardTransaction` has no settlement timestamp, so the authorization date dates the card line | +| Transaction type | `type` (`INCOMING` / `OUTGOING` / `CARD`) and `direction` (`CREDIT` / `DEBIT`); on `OUTGOING` transactions, `paymentRail` names the rail | +| Payee or merchant | `counterpartyInformation` or `description` on `INCOMING` and `OUTGOING` transactions; `merchant.descriptor` on `CARD` transactions | +| Terminal location | `merchant.city` and `merchant.state` on `CARD` transactions, when present | +| Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`. A card row is a statement line only once `settledAmount` is present (`status` is `SETTLED` or `PARTIALLY_SETTLED`); an `AUTHORIZED` hold is not a statement line. Sign the amount by `direction`: a `CREDIT` adds to the balance and a `DEBIT` subtracts from it | +| Fee line items | `fees` on `OUTGOING` transactions, in the smallest unit of the sending currency. `fees` is included in `sentAmount`, so show `sentAmount.amount - fees` as the transfer line and `fees` as its own line. On `INCOMING` transactions, `fees` is denominated in the sending currency and is deducted before `receivedAmount` lands, so it is not a line on this account's statement. Card transactions carry no `fees` field | +| Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions. Card rows have no `fees` field, so this total covers payment transactions only | + + +Only settled money becomes a statement line. The opening and closing balances are the ledger figures (`totalBalance`), so settled card rows and completed payments reconcile against them, and holds do not appear as lines. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. A `PARTIALLY_SETTLED` card row is listed at its `settledAmount` as of period close, and later clearings appear on the next statement as their own lines. An `EXCEPTION` row settled at the card network but the pull from the account failed, so keep it off the statement until it is reconciled. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). + + +## Example: build a statement at period close + +At period close, read the account holder and account, list the period's transactions, derive the lines and totals, render the statement, and hand it to your delivery channel. + +```javascript +// `grid` is your HTTP client for https://api.lightspark.com/grid/2025-10-13 +// with Basic auth. ERROR_RESOLUTION_NOTICE holds the notice text from the +// disclosures table above, and the delivery helpers are yours as well. +// Amounts stay integers in the smallest unit until render. + +// Yield every item of a paginated list, following nextCursor while hasMore is true +async function* listAll(path, params) { + let cursor; + do { + const query = new URLSearchParams({ ...params, limit: '100' }); + if (cursor) query.set('cursor', cursor); + const page = await grid.get(`${path}?${query}`); + yield* page.data; + cursor = page.hasMore ? page.nextCursor : undefined; + } while (cursor); +} + +// Run once per account when the statement period closes +async function buildStatement(customerId, accountId, startDate, endDate) { + const customer = await grid.get(`/customers/${customerId}`); + + let account; + for await (const candidate of listAll('/customers/internal-accounts', { customerId })) { + if (candidate.id === accountId) { + account = candidate; + break; + } + } + if (!account) throw new Error(`Account ${accountId} does not belong to customer ${customerId}`); + + // Payment rows are listed by account. Card rows are listed by cardholder and + // kept when this account funded them. Do not combine status with type=CARD + const window = { startDate, endDate, sortOrder: 'asc' }; + async function* periodRows() { + for await (const tx of listAll('/transactions', { ...window, accountIdentifier: accountId })) { + if (tx.type !== 'CARD') yield tx; + } + for await (const tx of listAll('/transactions', { ...window, customerId, type: 'CARD' })) { + if (tx.accountId === accountId) yield tx; + } + } + + // One line per movement of settled money, signed by direction + const lines = []; + for await (const tx of periodRows()) { + const sign = tx.direction === 'CREDIT' ? 1 : -1; + + if (tx.type === 'CARD') { + // Only cleared rows moved money. AUTHORIZED is a hold, DECLINED and VOIDED + // moved nothing, and EXCEPTION needs manual reconciliation first + if (tx.status !== 'SETTLED' && tx.status !== 'PARTIALLY_SETTLED') continue; + lines.push({ + date: tx.authorizedAt, + type: 'CARD', + direction: tx.direction, + payee: tx.merchant.descriptor, + terminalLocation: + tx.merchant.city && tx.merchant.state + ? `${tx.merchant.city}, ${tx.merchant.state}` + : undefined, + amount: sign * tx.settledAmount.amount, + }); + continue; + } + + if (tx.status !== 'COMPLETED') continue; + + if (tx.type === 'INCOMING') { + lines.push({ + date: tx.settledAt, + type: 'INCOMING', + direction: tx.direction, + payee: tx.description, + amount: sign * tx.receivedAmount.amount, + }); + continue; + } + + // OUTGOING: sentAmount already includes fees, so split it into two lines + const fees = tx.fees ?? 0; + lines.push({ + date: tx.settledAt, + type: 'OUTGOING', + direction: tx.direction, + payee: tx.description, + amount: sign * (tx.sentAmount.amount - fees), + }); + if (fees > 0) { + lines.push({ date: tx.settledAt, type: 'FEE', direction: tx.direction, amount: sign * fees }); + } + } + lines.sort((a, b) => a.date.localeCompare(b.date)); // oldest first across both lists + + // The ledger balance, including pending and held funds, read at period close + const closingBalance = account.totalBalance.amount; + const openingBalance = await previousClosingBalance(accountId); // from your records, same unit + const movement = lines.reduce((sum, line) => sum + line.amount, 0); + if (openingBalance + movement !== closingBalance) { + throw new Error('Statement does not reconcile: opening balance plus lines is not the closing balance'); + } + const totalFees = -lines + .filter((line) => line.type === 'FEE') + .reduce((sum, line) => sum + line.amount, 0); + + const { currency } = account.totalBalance; + const format = (amount) => (amount / 10 ** currency.decimals).toFixed(currency.decimals); + + const statement = { + // Fixed disclosures, in statement order + providerLine: + 'Lightspark is the program manager for this account. ' + + 'This account is held at Lead Bank, the account-holding institution.', + contactLine: + 'Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 ' + + 'or (855) 516-0103.', + errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // the full notice from the disclosures table + // Account fields + accountHolder: customer.fullName, + accountNumber: maskAccountNumber(account.fundingPaymentInstructions), + period: { startDate, endDate }, + issueDate: new Date().toISOString(), + openingBalance: format(openingBalance), + closingBalance: format(closingBalance), + lines: lines.map((line) => ({ ...line, amount: format(line.amount) })), + totalFees: format(totalFees), + }; + + // Delivery and the delivery record follow your platform agreement + await sendStatementEmail(customer.platformCustomerId, statement); + await recordStatementDelivery(accountId, statement.issueDate); +} +``` From 25630a77c825028dae48f0a460690376bb6fbe8e Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 17:43:29 -0700 Subject: [PATCH 08/14] docs: tighten the periodic statements guide Write in the same voice as the Receipts page, state each rule once, and remove rationale that a partner does not need to build the statement. Co-authored-by: Cursor --- mintlify/snippets/statements.mdx | 64 ++++++++++++++------------------ 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/mintlify/snippets/statements.mdx b/mintlify/snippets/statements.mdx index d0181d6c8..139ba893e 100644 --- a/mintlify/snippets/statements.mdx +++ b/mintlify/snippets/statements.mdx @@ -2,11 +2,7 @@ import { StatementExample } from '/snippets/cards/statement-example.mdx'; If you issue cards through Grid, Lightspark is the program manager for each cardholder account, and the account is held at **Lead Bank**, the account-holding institution. Regulation E requires a periodic statement for every consumer account that can send or receive electronic fund transfers. -This guide explains when a statement is sent, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. It covers consumer cardholder accounts. A commercial statement uses the same layout without the consumer-only items: the error-resolution notice, the asterisk markers, and the terminal location. - - -Periodic statements are a regulatory requirement for consumer card accounts. Your platform agreement sets out who prepares and delivers them. Use this page as a reference for what a statement contains and how to build it from Grid transaction data. - +This guide covers consumer cardholder accounts: when to send a statement, what it must contain, how to map Grid account and transaction data to each statement field, and how to build one at period close. A commercial statement uses the same layout without the error-resolution notice and the terminal location. ## Sample statement layout @@ -14,37 +10,38 @@ Your statement's visual design is up to you, but the content and field set are f -The asterisk marks the transfers the error-resolution notice covers, and the same asterisk keys the notice heading. RTP, wire, FedNow, and crypto or stablecoin transfers are listed so the balances reconcile, but they are not marked disputable. The marker is a reading aid, not a required field. +The asterisk is optional. In the sample it marks the lines the error-resolution notice applies to. -## When a statement is sent +## When to send a statement -A statement period closes once a month, at the same point in each month. A statement is issued monthly for every account, with or without activity in the period. +Close your statement period on the same day each month and issue a statement for every account, with or without activity in the period. -The statement goes to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** the statement (for example, a downloadable PDF or an email they can save). Record the issue date. The cardholder must report an error within 60 days of the statement date, counted from the first statement showing the error. Your platform agreement sets out who delivers the statement and who keeps the delivery record. +Send the statement to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** it, and record the issue date. Your platform agreement sets out who delivers the statement and who keeps the delivery record. -List every transaction that moved the balance in the period, including transfers that Regulation E does not cover, such as wires and RTP. Without them, the opening and closing balances would not reconcile. Once a statement is issued, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. +List every transaction that moved the balance in the period, including wires and RTP. Without them, your opening and closing balances do not reconcile. Once you issue a statement, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. ## What a statement must contain -A statement combines **fixed disclosures** (provider and error-resolution language) with **data-driven fields** populated from the account and its transactions for the period. +A statement combines **fixed disclosures** (provider and error-resolution language) with **data-driven fields** you populate from the account and its transactions for the period. ### Lightspark and Lead Bank disclosures -Every consumer statement carries these disclosures exactly as written. The statement names Lightspark as the program manager and never as a bank. +Include these on every consumer statement exactly as written. Do not describe Lightspark as a bank. | Field | Value | |-------|-------| | Provider line | “Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution.” | | Contact line | “Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.” | -| Error-resolution notice | The full notice shown in the sample above, headed “In case of errors or questions about your electronic transfers”, with the address and phone number above and the closing line “Report errors within 60 days of the statement date.” | +| Error-resolution notice | The notice text shown in the sample above, verbatim. | ### Statement fields | Field | Description | Required when | |-------|-------------|---------------| | Account holder | Name of the cardholder | Always | +| Account type | Kind of account, for example “Consumer prepaid account” | Always | | Account number | Number that identifies the account, masked to the last four digits | Always | | Statement period | Start and end date of the period | Always | | Issue date | Date the statement is sent | Always | @@ -60,39 +57,38 @@ Every consumer statement carries these disclosures exactly as written. The state ## Mapping Grid data to statement fields -Statement data comes from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Bound the period with `startDate` and `endDate`, both inclusive, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. List payment transactions with `accountIdentifier`. List card rows with `customerId` or `cardId`, and keep the rows whose `accountId` is the statement account; do not combine `status` with `type=CARD`, because the request is rejected. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. +You build a statement from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Bound the period with `startDate` and `endDate`, both inclusive, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. List payment transactions by `accountIdentifier`. Card transactions are listed by `customerId` or `cardId`, so keep the rows whose `accountId` is the statement account. `type=CARD` cannot be combined with `status`. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. | Statement field | Grid source | |-----------------|-------------| | Account holder | `fullName` on the customer (`customerType: INDIVIDUAL`) | +| Account type | `type` on the internal account (`INTERNAL_FIAT` for a cardholder account), shown as a plain-language label | | Account number | `fundingPaymentInstructions[].accountOrWalletInfo.accountNumber` on the internal account, masked to the last four digits | | Statement period | The `startDate` and `endDate` you pass to List transactions | | Issue date | You supply it: the date the statement is sent | -| Closing balance | `totalBalance` on the internal account, read at period close. This is the ledger figure and includes pending and held funds; `balance` excludes them. Grid returns the current figure only, so read it when the period closes and store it | +| Closing balance | `totalBalance` on the internal account (includes pending and held funds; `balance` does not). Grid has no historical balance, so read it at period close and store it | | Opening balance | The previous period's closing balance from your records | -| Transaction date | `settledAt` on `INCOMING` and `OUTGOING` transactions; `authorizedAt` on `CARD` transactions. `CardTransaction` has no settlement timestamp, so the authorization date dates the card line | +| Transaction date | `settledAt` on `INCOMING` and `OUTGOING`; `authorizedAt` on `CARD` | | Transaction type | `type` (`INCOMING` / `OUTGOING` / `CARD`) and `direction` (`CREDIT` / `DEBIT`); on `OUTGOING` transactions, `paymentRail` names the rail | | Payee or merchant | `counterpartyInformation` or `description` on `INCOMING` and `OUTGOING` transactions; `merchant.descriptor` on `CARD` transactions | | Terminal location | `merchant.city` and `merchant.state` on `CARD` transactions, when present | -| Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`. A card row is a statement line only once `settledAmount` is present (`status` is `SETTLED` or `PARTIALLY_SETTLED`); an `AUTHORIZED` hold is not a statement line. Sign the amount by `direction`: a `CREDIT` adds to the balance and a `DEBIT` subtracts from it | -| Fee line items | `fees` on `OUTGOING` transactions, in the smallest unit of the sending currency. `fees` is included in `sentAmount`, so show `sentAmount.amount - fees` as the transfer line and `fees` as its own line. On `INCOMING` transactions, `fees` is denominated in the sending currency and is deducted before `receivedAmount` lands, so it is not a line on this account's statement. Card transactions carry no `fees` field | -| Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions. Card rows have no `fees` field, so this total covers payment transactions only | +| Transaction amount | `receivedAmount` on `INCOMING`, `sentAmount` on `OUTGOING`, `settledAmount` on `CARD`, signed by `direction` (`CREDIT` adds, `DEBIT` subtracts) | +| Fee line items | `fees` on `OUTGOING` transactions. `fees` is included in `sentAmount`, so show `sentAmount.amount - fees` as the transfer line and `fees` as its own line. `INCOMING` and `CARD` transactions have no fee line | +| Total fees for the period | Sum of `fees` across the period's `OUTGOING` transactions | -Only settled money becomes a statement line. The opening and closing balances are the ledger figures (`totalBalance`), so settled card rows and completed payments reconcile against them, and holds do not appear as lines. An `AUTHORIZED` card row is a hold. No clearing has posted and `settledAmount` is absent. `DECLINED` and `VOIDED` rows moved no money either. A `PARTIALLY_SETTLED` card row is listed at its `settledAmount` as of period close, and later clearings appear on the next statement as their own lines. An `EXCEPTION` row settled at the card network but the pull from the account failed, so keep it off the statement until it is reconciled. Among `INCOMING` and `OUTGOING` transactions, only `status: COMPLETED` moved the balance; skip the rest (`FAILED`, `EXPIRED`, `REJECTED`, `REFUNDED`, and in-flight statuses). A merchant refund is its own `CARD` transaction with `direction: CREDIT`, and `originalTransactionId` names the purchase it credits back against. List it as its own dated line and don't net it against the purchase. For the card status model, see [Reconciliation](/cards/transactions/reconciliation). +Only settled money is a statement line. Card rows count when `status` is `SETTLED` or `PARTIALLY_SETTLED`, at their `settledAmount` as of period close; later clearings appear on the next statement. Skip `AUTHORIZED`, `DECLINED`, and `VOIDED` rows, and hold `EXCEPTION` rows until they are reconciled. Payment transactions count only when `status` is `COMPLETED`. A merchant refund is its own `CARD` row (`direction: CREDIT`, `originalTransactionId` set). For the status model, see [Reconciliation](/cards/transactions/reconciliation). ## Example: build a statement at period close -At period close, read the account holder and account, list the period's transactions, derive the lines and totals, render the statement, and hand it to your delivery channel. +When your period closes, read the account holder and account, list the period's transactions, derive the lines and totals, render the statement, and hand it to your delivery channel. ```javascript -// `grid` is your HTTP client for https://api.lightspark.com/grid/2025-10-13 -// with Basic auth. ERROR_RESOLUTION_NOTICE holds the notice text from the -// disclosures table above, and the delivery helpers are yours as well. -// Amounts stay integers in the smallest unit until render. +// `grid` is your authenticated HTTP client for the Grid base URL. +// ERROR_RESOLUTION_NOTICE, maskAccountNumber, and the delivery helpers are yours. -// Yield every item of a paginated list, following nextCursor while hasMore is true +// Follow nextCursor until hasMore is false async function* listAll(path, params) { let cursor; do { @@ -104,7 +100,6 @@ async function* listAll(path, params) { } while (cursor); } -// Run once per account when the statement period closes async function buildStatement(customerId, accountId, startDate, endDate) { const customer = await grid.get(`/customers/${customerId}`); @@ -117,8 +112,7 @@ async function buildStatement(customerId, accountId, startDate, endDate) { } if (!account) throw new Error(`Account ${accountId} does not belong to customer ${customerId}`); - // Payment rows are listed by account. Card rows are listed by cardholder and - // kept when this account funded them. Do not combine status with type=CARD + // Card rows are listed by cardholder; keep the ones funded by this account const window = { startDate, endDate, sortOrder: 'asc' }; async function* periodRows() { for await (const tx of listAll('/transactions', { ...window, accountIdentifier: accountId })) { @@ -129,14 +123,12 @@ async function buildStatement(customerId, accountId, startDate, endDate) { } } - // One line per movement of settled money, signed by direction + // One line per settled movement const lines = []; for await (const tx of periodRows()) { const sign = tx.direction === 'CREDIT' ? 1 : -1; if (tx.type === 'CARD') { - // Only cleared rows moved money. AUTHORIZED is a hold, DECLINED and VOIDED - // moved nothing, and EXCEPTION needs manual reconciliation first if (tx.status !== 'SETTLED' && tx.status !== 'PARTIALLY_SETTLED') continue; lines.push({ date: tx.authorizedAt, @@ -180,7 +172,6 @@ async function buildStatement(customerId, accountId, startDate, endDate) { } lines.sort((a, b) => a.date.localeCompare(b.date)); // oldest first across both lists - // The ledger balance, including pending and held funds, read at period close const closingBalance = account.totalBalance.amount; const openingBalance = await previousClosingBalance(accountId); // from your records, same unit const movement = lines.reduce((sum, line) => sum + line.amount, 0); @@ -195,14 +186,14 @@ async function buildStatement(customerId, accountId, startDate, endDate) { const format = (amount) => (amount / 10 ** currency.decimals).toFixed(currency.decimals); const statement = { - // Fixed disclosures, in statement order + // Fixed disclosures providerLine: 'Lightspark is the program manager for this account. ' + 'This account is held at Lead Bank, the account-holding institution.', contactLine: 'Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 ' + 'or (855) 516-0103.', - errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // the full notice from the disclosures table + errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // Account fields accountHolder: customer.fullName, accountNumber: maskAccountNumber(account.fundingPaymentInstructions), @@ -214,8 +205,7 @@ async function buildStatement(customerId, accountId, startDate, endDate) { totalFees: format(totalFees), }; - // Delivery and the delivery record follow your platform agreement - await sendStatementEmail(customer.platformCustomerId, statement); + await sendStatementEmail(customer.platformCustomerId, statement); // your delivery channel await recordStatementDelivery(accountId, statement.issueDate); } ``` From b1df3b013f10ae7397bc57640d52258800e904ad Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 17:48:02 -0700 Subject: [PATCH 09/14] docs: include account type in the statement example The mapping table lists account type as always required, so the statement object now carries `accountType` from the internal account. Co-authored-by: Cursor --- mintlify/snippets/statements.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/mintlify/snippets/statements.mdx b/mintlify/snippets/statements.mdx index 139ba893e..421c9cd88 100644 --- a/mintlify/snippets/statements.mdx +++ b/mintlify/snippets/statements.mdx @@ -196,6 +196,7 @@ async function buildStatement(customerId, accountId, startDate, endDate) { errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // Account fields accountHolder: customer.fullName, + accountType: account.type, // render as a label, for example "Consumer prepaid account" accountNumber: maskAccountNumber(account.fundingPaymentInstructions), period: { startDate, endDate }, issueDate: new Date().toISOString(), From f4f44a728beeb26b76f43edb67c85988e37906ba Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 18:05:32 -0700 Subject: [PATCH 10/14] docs: frame the statements guide as reference guidance Point to the platform agreement for cadence, delivery, records, and disclosure text. Scope the provider lines to every statement, keep the error-resolution markers on the consumer sample, and note that card rows are dated by authorization. Co-authored-by: Cursor --- mintlify/snippets/cards/statement-example.mdx | 4 +-- mintlify/snippets/statements.mdx | 32 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/mintlify/snippets/cards/statement-example.mdx b/mintlify/snippets/cards/statement-example.mdx index 4af986836..6f29f270d 100644 --- a/mintlify/snippets/cards/statement-example.mdx +++ b/mintlify/snippets/cards/statement-example.mdx @@ -27,7 +27,7 @@ export const StatementExample = () => { { id: 'c2', day: '08', type: 'Debit card purchase', party: 'Blue Bottle Coffee', terminal: 'Los Angeles, CA', amount: '-$18.75', disputable: true }, { id: 'c3', day: '12', type: 'ACH debit', party: 'Pacific Gas & Electric', amount: '-$142.30', disputable: true }, { id: 'c4', day: '18', type: 'Wire transfer out', party: 'First National Escrow', amount: '-$1,000.00', disputable: false }, - { id: 'c4-fee', day: '18', type: 'Wire transfer fee', party: 'Lead Bank', amount: '-$15.00', disputable: false }, + { id: 'c4-fee', day: '18', type: 'Wire transfer fee', party: '', amount: '-$15.00', disputable: false }, { id: 'c5', day: '24', type: 'RTP received', party: 'Sofía Herrera', amount: '+$250.00', disputable: false }, ]; const noticeSteps = [ @@ -76,7 +76,7 @@ export const StatementExample = () => { 09/{row.day} {row.type} - {row.party} + {row.party ? {row.party} : null} {row.terminal ? · {row.terminal} : null} diff --git a/mintlify/snippets/statements.mdx b/mintlify/snippets/statements.mdx index 421c9cd88..6b34154a3 100644 --- a/mintlify/snippets/statements.mdx +++ b/mintlify/snippets/statements.mdx @@ -1,44 +1,44 @@ import { StatementExample } from '/snippets/cards/statement-example.mdx'; -If you issue cards through Grid, Lightspark is the program manager for each cardholder account, and the account is held at **Lead Bank**, the account-holding institution. Regulation E requires a periodic statement for every consumer account that can send or receive electronic fund transfers. +If you issue consumer cards through Grid, each cardholder account gets a periodic statement. Your platform agreement sets out what the statement contains and who delivers it. Use this page as a reference for a typical consumer statement and how to build one from Grid data. -This guide covers consumer cardholder accounts: when to send a statement, what it must contain, how to map Grid account and transaction data to each statement field, and how to build one at period close. A commercial statement uses the same layout without the error-resolution notice and the terminal location. +This guide covers consumer cardholder accounts: when to send a statement, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. ## Sample statement layout -Your statement's visual design is up to you, but the content and field set are fixed. The layout below shows a consumer statement with sample data filled in: +Your statement's visual design is up to you. The layout below shows a consumer statement with sample data filled in. Check your platform agreement for the fields and disclosures your program requires: -The asterisk is optional. In the sample it marks the lines the error-resolution notice applies to. +In the sample, an asterisk marks each line the error-resolution notice covers. A commercial statement omits the markers, the notice, and the terminal location. Your platform agreement sets out which transaction types carry the marker. ## When to send a statement -Close your statement period on the same day each month and issue a statement for every account, with or without activity in the period. +A typical statement period closes on the same day each month, with a statement for every account, whether or not it had activity. -Send the statement to the cardholder as soon as the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** it, and record the issue date. Your platform agreement sets out who delivers the statement and who keeps the delivery record. +Send the statement to the cardholder when the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** it, and record the issue date. Your platform agreement sets out the cadence, who delivers the statement, and who keeps the delivery record and for how long. -List every transaction that moved the balance in the period, including wires and RTP. Without them, your opening and closing balances do not reconcile. Once you issue a statement, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. +List every transaction that moved the balance in the period. Without them, your opening and closing balances do not reconcile. Once you issue a statement, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. -## What a statement must contain +## What a statement contains -A statement combines **fixed disclosures** (provider and error-resolution language) with **data-driven fields** you populate from the account and its transactions for the period. +A statement combines **disclosures** (provider and error-resolution language) with **data-driven fields** you populate from the account and its transactions for the period. -### Lightspark and Lead Bank disclosures +### Provider disclosures -Include these on every consumer statement exactly as written. Do not describe Lightspark as a bank. +Every statement, consumer or commercial, carries the provider and contact lines. The sample uses the wording below; your platform agreement provides the disclosure text for your program. Lightspark is not a bank, so the provider line names the account-holding institution separately. | Field | Value | |-------|-------| | Provider line | “Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution.” | | Contact line | “Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.” | -| Error-resolution notice | The notice text shown in the sample above, verbatim. | +| Error-resolution notice | The notice text shown in the sample above. | ### Statement fields -| Field | Description | Required when | +| Field | Description | Included when | |-------|-------------|---------------| | Account holder | Name of the cardholder | Always | | Account type | Kind of account, for example “Consumer prepaid account” | Always | @@ -67,8 +67,8 @@ You build a statement from three calls: [Get customer by ID](/api-reference/cust | Statement period | The `startDate` and `endDate` you pass to List transactions | | Issue date | You supply it: the date the statement is sent | | Closing balance | `totalBalance` on the internal account (includes pending and held funds; `balance` does not). Grid has no historical balance, so read it at period close and store it | -| Opening balance | The previous period's closing balance from your records | -| Transaction date | `settledAt` on `INCOMING` and `OUTGOING`; `authorizedAt` on `CARD` | +| Opening balance | The previous period's closing balance from your records. For the first statement, read `totalBalance` at period open | +| Transaction date | `settledAt` on `INCOMING` and `OUTGOING`. `authorizedAt` on `CARD`; Grid has no card posting date. A card row that settles after period close appears on the next statement, dated by its authorization | | Transaction type | `type` (`INCOMING` / `OUTGOING` / `CARD`) and `direction` (`CREDIT` / `DEBIT`); on `OUTGOING` transactions, `paymentRail` names the rail | | Payee or merchant | `counterpartyInformation` or `description` on `INCOMING` and `OUTGOING` transactions; `merchant.descriptor` on `CARD` transactions | | Terminal location | `merchant.city` and `merchant.state` on `CARD` transactions, when present | @@ -186,7 +186,7 @@ async function buildStatement(customerId, accountId, startDate, endDate) { const format = (amount) => (amount / 10 ** currency.decimals).toFixed(currency.decimals); const statement = { - // Fixed disclosures + // Disclosures providerLine: 'Lightspark is the program manager for this account. ' + 'This account is held at Lead Bank, the account-holding institution.', From 2b5770dc7f0ba7ff6b627fcd9f1635ed759cf66b Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 18:20:02 -0700 Subject: [PATCH 11/14] docs: align the sample statement footer with the statement definition Restore the provider line wording, keep contact details inside the error-resolution notice, and stop the flag markers from rendering above the baseline. Co-authored-by: Cursor --- mintlify/snippets/cards/statement-example.mdx | 10 +++++----- mintlify/snippets/statements.mdx | 15 ++++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/mintlify/snippets/cards/statement-example.mdx b/mintlify/snippets/cards/statement-example.mdx index 6f29f270d..ded0f3948 100644 --- a/mintlify/snippets/cards/statement-example.mdx +++ b/mintlify/snippets/cards/statement-example.mdx @@ -12,8 +12,8 @@ export const StatementExample = () => { const definitionValue = { textAlign: 'right' }; const currency = { marginLeft: '6px', color: muted70, fontSize: '10px' }; const ledgerGrid = { display: 'grid', gridTemplateColumns: ledgerColumns, columnGap: '12px' }; - const flag = { marginLeft: '2px', color: muted56, fontSize: '0.72em', fontVariantNumeric: 'tabular-nums', lineHeight: 0, verticalAlign: 'super' }; - const legalFlag = { marginLeft: '2px', fontSize: '0.72em', lineHeight: 0, verticalAlign: 'super' }; + const flag = { marginLeft: '2px', color: muted56, fontSize: '0.72em', fontVariantNumeric: 'tabular-nums', lineHeight: 0, verticalAlign: 'super', position: 'static', top: 'auto' }; + const legalFlag = { marginLeft: '2px', fontSize: '0.72em', lineHeight: 0, verticalAlign: 'super', position: 'static', top: 'auto' }; const footerP = { margin: 0 }; const details = [ ['Statement period', '09/01/2026 – 09/30/2026'], @@ -27,7 +27,7 @@ export const StatementExample = () => { { id: 'c2', day: '08', type: 'Debit card purchase', party: 'Blue Bottle Coffee', terminal: 'Los Angeles, CA', amount: '-$18.75', disputable: true }, { id: 'c3', day: '12', type: 'ACH debit', party: 'Pacific Gas & Electric', amount: '-$142.30', disputable: true }, { id: 'c4', day: '18', type: 'Wire transfer out', party: 'First National Escrow', amount: '-$1,000.00', disputable: false }, - { id: 'c4-fee', day: '18', type: 'Wire transfer fee', party: '', amount: '-$15.00', disputable: false }, + { id: 'c4-fee', day: '18', type: 'Wire transfer fee', party: 'Lead Bank', amount: '-$15.00', disputable: false }, { id: 'c5', day: '24', type: 'RTP received', party: 'Sofía Herrera', amount: '+$250.00', disputable: false }, ]; const noticeSteps = [ @@ -104,9 +104,9 @@ export const StatementExample = () => { ))}

We will investigate your complaint and will correct any error promptly. If we take more than 10 business days to do this, we will credit your account for the amount you think is in error, so that you will have the use of the money during the time it takes us to complete our investigation.

-

Report errors within 60 days of the statement date.

+

Report errors within 60 days after we send this statement.

-

Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution. Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.

+

This account is held at Lead Bank, the account-holding institution. Lightspark is the program manager and is not a bank.

diff --git a/mintlify/snippets/statements.mdx b/mintlify/snippets/statements.mdx index 6b34154a3..88b5d527d 100644 --- a/mintlify/snippets/statements.mdx +++ b/mintlify/snippets/statements.mdx @@ -28,13 +28,13 @@ A statement combines **disclosures** (provider and error-resolution language) wi ### Provider disclosures -Every statement, consumer or commercial, carries the provider and contact lines. The sample uses the wording below; your platform agreement provides the disclosure text for your program. Lightspark is not a bank, so the provider line names the account-holding institution separately. +Every statement, consumer or commercial, carries the provider line. A consumer statement also carries the error-resolution notice, which gives the phone number and mailing address. The sample uses the wording below; your platform agreement provides the disclosure text for your program. The provider line names the account-holding institution and the program manager. | Field | Value | |-------|-------| -| Provider line | “Lightspark is the program manager for this account. This account is held at Lead Bank, the account-holding institution.” | -| Contact line | “Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.” | +| Provider line | “This account is held at Lead Bank, the account-holding institution. Lightspark is the program manager and is not a bank.” | | Error-resolution notice | The notice text shown in the sample above. | +| Phone and address | Inside the error-resolution notice (“Telephone us at … or Write us at …”). | ### Statement fields @@ -188,12 +188,9 @@ async function buildStatement(customerId, accountId, startDate, endDate) { const statement = { // Disclosures providerLine: - 'Lightspark is the program manager for this account. ' + - 'This account is held at Lead Bank, the account-holding institution.', - contactLine: - 'Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 ' + - 'or (855) 516-0103.', - errorResolutionNotice: ERROR_RESOLUTION_NOTICE, + 'This account is held at Lead Bank, the account-holding institution. ' + + 'Lightspark is the program manager and is not a bank.', + errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // includes the phone number and mailing address // Account fields accountHolder: customer.fullName, accountType: account.type, // render as a label, for example "Consumer prepaid account" From 5e1a842e04c5878c0461b7f588f88bbecb6cc735 Mon Sep 17 00:00:00 2001 From: jaymantri Date: Wed, 23 Sep 2026 18:20:03 -0700 Subject: [PATCH 12/14] docs: give the statements page its own sidebar icon Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 2 +- mintlify/images/icons/statement.svg | 3 +++ mintlify/payouts-and-b2b/payment-flow/statements.mdx | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 mintlify/images/icons/statement.svg diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index 1afd4a72e..5d2cdf390 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -1,7 +1,7 @@ --- title: "Periodic statements" description: "What a consumer card periodic statement contains, and how to build one from Grid transaction data" -icon: "/images/icons/receipt-check.svg" +icon: "/images/icons/statement.svg" --- import Statements from '/snippets/statements.mdx' diff --git a/mintlify/images/icons/statement.svg b/mintlify/images/icons/statement.svg new file mode 100644 index 000000000..a584b52d1 --- /dev/null +++ b/mintlify/images/icons/statement.svg @@ -0,0 +1,3 @@ + + + diff --git a/mintlify/payouts-and-b2b/payment-flow/statements.mdx b/mintlify/payouts-and-b2b/payment-flow/statements.mdx index 9bdec65d0..2d8983731 100644 --- a/mintlify/payouts-and-b2b/payment-flow/statements.mdx +++ b/mintlify/payouts-and-b2b/payment-flow/statements.mdx @@ -1,7 +1,7 @@ --- title: "Periodic statements" description: "What a consumer card periodic statement contains, and how to build one from Grid transaction data" -icon: "/images/icons/receipt-check.svg" +icon: "/images/icons/statement.svg" "og:image": "/images/og/og-payouts-b2b.png" --- From fffb57f001a767b1a53760042c091ccdaab51dbb Mon Sep 17 00:00:00 2001 From: jaymantri Date: Thu, 24 Sep 2026 09:51:07 -0700 Subject: [PATCH 13/14] docs: scope the statements guide to every Lead Bank account Statements apply to every account, consumer or commercial. Describe the base layout for all accounts and call out the consumer-only items where they apply. Add the commercial contact line and plain wording for the two parts of a statement. Co-authored-by: Cursor --- mintlify/cards/statements.mdx | 2 +- .../payment-flow/statements.mdx | 2 +- mintlify/snippets/statements.mdx | 41 ++++++++++--------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/mintlify/cards/statements.mdx b/mintlify/cards/statements.mdx index 5d2cdf390..a84f76010 100644 --- a/mintlify/cards/statements.mdx +++ b/mintlify/cards/statements.mdx @@ -1,6 +1,6 @@ --- title: "Periodic statements" -description: "What a consumer card periodic statement contains, and how to build one from Grid transaction data" +description: "What a periodic statement for a Grid account contains, and how to build one from Grid transaction data" icon: "/images/icons/statement.svg" --- diff --git a/mintlify/payouts-and-b2b/payment-flow/statements.mdx b/mintlify/payouts-and-b2b/payment-flow/statements.mdx index 2d8983731..d455c7677 100644 --- a/mintlify/payouts-and-b2b/payment-flow/statements.mdx +++ b/mintlify/payouts-and-b2b/payment-flow/statements.mdx @@ -1,6 +1,6 @@ --- title: "Periodic statements" -description: "What a consumer card periodic statement contains, and how to build one from Grid transaction data" +description: "What a periodic statement for a Grid account contains, and how to build one from Grid transaction data" icon: "/images/icons/statement.svg" "og:image": "/images/og/og-payouts-b2b.png" --- diff --git a/mintlify/snippets/statements.mdx b/mintlify/snippets/statements.mdx index 88b5d527d..feca552f3 100644 --- a/mintlify/snippets/statements.mdx +++ b/mintlify/snippets/statements.mdx @@ -1,22 +1,22 @@ import { StatementExample } from '/snippets/cards/statement-example.mdx'; -If you issue consumer cards through Grid, each cardholder account gets a periodic statement. Your platform agreement sets out what the statement contains and who delivers it. Use this page as a reference for a typical consumer statement and how to build one from Grid data. +Every account held at Lead Bank gets a periodic statement each month, whether or not it had activity. Your platform agreement sets out what the statement contains and who delivers it. Use this page as a reference for a typical statement and how to build one from Grid data. -This guide covers consumer cardholder accounts: when to send a statement, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. +This guide covers when to send a statement, what it contains, how to map Grid account and transaction data to each statement field, and how to build one at period close. Consumer accounts carry a few extra items, called out where they apply. ## Sample statement layout -Your statement's visual design is up to you. The layout below shows a consumer statement with sample data filled in. Check your platform agreement for the fields and disclosures your program requires: +Your statement's visual design is up to you. The layout below shows a consumer account statement with sample data filled in. Check your platform agreement for the fields and disclosures your program requires: -In the sample, an asterisk marks each line the error-resolution notice covers. A commercial statement omits the markers, the notice, and the terminal location. Your platform agreement sets out which transaction types carry the marker. +The sample is a consumer statement. It carries three items a commercial statement omits: the asterisk on each line the error-resolution notice covers, the notice itself, and the terminal location on card purchases. Your platform agreement sets out which transaction types carry the marker. ## When to send a statement -A typical statement period closes on the same day each month, with a statement for every account, whether or not it had activity. +Close the statement period on the same day each month and issue a statement for every account, with or without activity. -Send the statement to the cardholder when the period closes, by email, in-app notification, or any channel that lets the cardholder **retain** it, and record the issue date. Your platform agreement sets out the cadence, who delivers the statement, and who keeps the delivery record and for how long. +Send the statement to the account holder when the period closes, by email, in-app notification, or any channel that lets the account holder **retain** it, and record the issue date. Your platform agreement sets out the cadence, who delivers the statement, and who keeps the delivery record and for how long. List every transaction that moved the balance in the period. Without them, your opening and closing balances do not reconcile. Once you issue a statement, **freeze its contents**. A refund that posts later appears on the next statement as its own dated line. @@ -24,23 +24,23 @@ List every transaction that moved the balance in the period. Without them, your ## What a statement contains -A statement combines **disclosures** (provider and error-resolution language) with **data-driven fields** you populate from the account and its transactions for the period. +A statement has two parts: disclosure text, and fields you fill from the account and its transactions. ### Provider disclosures -Every statement, consumer or commercial, carries the provider line. A consumer statement also carries the error-resolution notice, which gives the phone number and mailing address. The sample uses the wording below; your platform agreement provides the disclosure text for your program. The provider line names the account-holding institution and the program manager. +Every statement carries the provider line. A consumer statement also carries the error-resolution notice, which gives the phone number and mailing address. A commercial statement has no notice, so it carries a contact line instead. The sample uses the wording below; your platform agreement provides the disclosure text for your program. | Field | Value | |-------|-------| | Provider line | “This account is held at Lead Bank, the account-holding institution. Lightspark is the program manager and is not a bank.” | -| Error-resolution notice | The notice text shown in the sample above. | -| Phone and address | Inside the error-resolution notice (“Telephone us at … or Write us at …”). | +| Error-resolution notice | Consumer statements. The notice text shown in the sample above. | +| Phone and address | Consumer statements: inside the error-resolution notice (“Telephone us at … or Write us at …”). Commercial statements: a line reading “Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.” | ### Statement fields | Field | Description | Included when | |-------|-------------|---------------| -| Account holder | Name of the cardholder | Always | +| Account holder | Name of the account holder | Always | | Account type | Kind of account, for example “Consumer prepaid account” | Always | | Account number | Number that identifies the account, masked to the last four digits | Always | | Statement period | Start and end date of the period | Always | @@ -50,19 +50,19 @@ Every statement, consumer or commercial, carries the provider line. A consumer s | Transaction date | Date each transaction posted to the account | Always | | Transaction type | Kind of transfer, for example ACH deposit, debit card purchase, wire transfer out | Always | | Payee or merchant | Counterparty name, or the merchant descriptor for a card purchase | Always | -| Terminal location | Merchant city and state | Card purchases at a merchant terminal, when the card network reports a location | +| Terminal location | Merchant city and state | Consumer statements, for card purchases at a merchant terminal, when the card network reports a location | | Transaction amount | Signed amount in the account currency | Always | | Fee line items | Each fee charged in the period, as its own line | When a fee was charged | | Total fees for the period | Sum of all fees charged in the period | Always | ## Mapping Grid data to statement fields -You build a statement from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Bound the period with `startDate` and `endDate`, both inclusive, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. List payment transactions by `accountIdentifier`. Card transactions are listed by `customerId` or `cardId`, so keep the rows whose `accountId` is the statement account. `type=CARD` cannot be combined with `status`. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. +You build a statement from three calls: [Get customer by ID](/api-reference/customers/get-customer-by-id) for the account holder, [List Customer internal accounts](/api-reference/internal-accounts/list-customer-internal-accounts) for the account, and [List transactions](/api-reference/transactions/list-transactions) for the period's activity. Bound the period with `startDate` and `endDate`, both inclusive, sort with `sortOrder=asc`, and page with `cursor` while `hasMore` is `true`. List payment transactions by `accountIdentifier`. If the account funds cards, card transactions are listed by `customerId` or `cardId`, so keep the rows whose `accountId` is the statement account. `type=CARD` cannot be combined with `status`. All amounts are integers in the smallest unit of their currency (for example, cents), so format them using the currency's `decimals`. | Statement field | Grid source | |-----------------|-------------| -| Account holder | `fullName` on the customer (`customerType: INDIVIDUAL`) | -| Account type | `type` on the internal account (`INTERNAL_FIAT` for a cardholder account), shown as a plain-language label | +| Account holder | `fullName` on the customer (`customerType: INDIVIDUAL`) or `businessInfo.legalName` (`customerType: BUSINESS`) | +| Account type | `type` on the internal account (`INTERNAL_FIAT` for the platform-managed fiat account), shown as a plain-language label | | Account number | `fundingPaymentInstructions[].accountOrWalletInfo.accountNumber` on the internal account, masked to the last four digits | | Statement period | The `startDate` and `endDate` you pass to List transactions | | Issue date | You supply it: the date the statement is sent | @@ -102,6 +102,7 @@ async function* listAll(path, params) { async function buildStatement(customerId, accountId, startDate, endDate) { const customer = await grid.get(`/customers/${customerId}`); + const commercial = customer.customerType === 'BUSINESS'; let account; for await (const candidate of listAll('/customers/internal-accounts', { customerId })) { @@ -112,7 +113,7 @@ async function buildStatement(customerId, accountId, startDate, endDate) { } if (!account) throw new Error(`Account ${accountId} does not belong to customer ${customerId}`); - // Card rows are listed by cardholder; keep the ones funded by this account + // Card rows are listed by customer; keep the ones funded by this account const window = { startDate, endDate, sortOrder: 'asc' }; async function* periodRows() { for await (const tx of listAll('/transactions', { ...window, accountIdentifier: accountId })) { @@ -186,13 +187,15 @@ async function buildStatement(customerId, accountId, startDate, endDate) { const format = (amount) => (amount / 10 ** currency.decimals).toFixed(currency.decimals); const statement = { - // Disclosures + // Disclosures: a consumer statement carries the notice, a commercial statement the contact line providerLine: 'This account is held at Lead Bank, the account-holding institution. ' + 'Lightspark is the program manager and is not a bank.', - errorResolutionNotice: ERROR_RESOLUTION_NOTICE, // includes the phone number and mailing address + ...(commercial + ? { contactLine: 'Direct inquiries to 8605 Santa Monica Blvd, PMB 64461, West Hollywood, CA 90069 or (855) 516-0103.' } + : { errorResolutionNotice: ERROR_RESOLUTION_NOTICE }), // the notice includes the phone number and mailing address // Account fields - accountHolder: customer.fullName, + accountHolder: commercial ? customer.businessInfo.legalName : customer.fullName, accountType: account.type, // render as a label, for example "Consumer prepaid account" accountNumber: maskAccountNumber(account.fundingPaymentInstructions), period: { startDate, endDate }, From dd9fd0a500e8d89d7f161294bf2bafe63ad13af5 Mon Sep 17 00:00:00 2001 From: jaymantri Date: Thu, 24 Sep 2026 09:53:02 -0700 Subject: [PATCH 14/14] docs: omit terminal location on commercial statements in the example Co-authored-by: Cursor --- mintlify/snippets/statements.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mintlify/snippets/statements.mdx b/mintlify/snippets/statements.mdx index feca552f3..f89b11046 100644 --- a/mintlify/snippets/statements.mdx +++ b/mintlify/snippets/statements.mdx @@ -137,7 +137,7 @@ async function buildStatement(customerId, accountId, startDate, endDate) { direction: tx.direction, payee: tx.merchant.descriptor, terminalLocation: - tx.merchant.city && tx.merchant.state + !commercial && tx.merchant.city && tx.merchant.state ? `${tx.merchant.city}, ${tx.merchant.state}` : undefined, amount: sign * tx.settledAmount.amount,