
Stripe List Pagination Previous Attributes
- 52 installs
- 50 repo stars
- Updated June 18, 2026
- josiahsiegel/claude-plugin-marketplace
Helps with ai & agent building tasks.
About
stripe-list-pagination-previous-attributes is a Claude Code skill in the AI & Agent Building category.
- stripe-list-pagination-previous-attributes
- AI & Agent Building
- AI-coding skill
Stripe List Pagination Previous Attributes by the numbers
- 52 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #7,142 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill stripe-list-pagination-previous-attributesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 52 |
|---|---|
| repo stars | ★ 50 |
| Last updated | June 18, 2026 |
| Repository | josiahsiegel/claude-plugin-marketplace ↗ |
What it does
Helps with ai & agent building tasks.
Files
Quick Reference
| Field | Semantic |
|---|---|
event.data.previous_attributes | ONLY the fields that changed; diff the current object against this |
event.data.previous_attributes.amount_refunded | Previous cumulative refund total — subtract from charge.amount_refunded for per-event delta |
charge.amount_refunded | CUMULATIVE across all refunds, NEVER per-event |
invoice.lines.has_more | true -> embedded data is page 1; paginate with starting_after |
invoice.lines.data.at(-1).id | The cursor for starting_after on the next page |
stripe.invoices.listLineItems(invoice.id, { starting_after }) | Resumes AFTER the cursor — does NOT re-scan page 1 |
When to Use This Skill
Use whenever:
- You read an embedded
.data[]array from a Stripe object and might need to paginate - You compute a "what changed" delta from a webhook event
- You resolve a plan / SKU / entitlement from invoice line items or subscription items
Related skills:
- Where
getRefundDeltais consumed in the refund handler:stripe-billing-master:stripe-refund-dispute-lifecycle - Where
resolvedViagates email rendering and audit logging:stripe-billing-master:stripe-credit-audit-trail
Pagination scan pattern
async function scanAllLineItems(invoice: Stripe.Invoice): Promise<Stripe.InvoiceLineItem[]> {
const items: Stripe.InvoiceLineItem[] = [...invoice.lines.data];
let cursor = invoice.lines.data.at(-1)?.id;
let hasMore = invoice.lines.has_more;
while (hasMore && cursor) {
const page = await stripe.invoices.listLineItems(invoice.id, {
limit: 100,
starting_after: cursor,
});
items.push(...page.data);
hasMore = page.has_more;
cursor = page.data.at(-1)?.id;
}
return items;
}Without starting_after, stripe.invoices.listLineItems(invoice.id) re-fetches page 1 — the same page you already have embedded in invoice.lines.data. The scan loops over identical content until has_more never flips, concluding "no match" on lines that never got scanned. Always thread the cursor.
Edge case: ifinvoice.lines.datais empty butinvoice.lines.has_moreistrue(rare, but Stripe can return this shape when the embeddedlimitis 0), the loop above never executes becausecursorisundefined. In that case, fall through tostripe.invoices.listLineItems(invoice.id, { limit: 100 })with nostarting_afterto start a fresh scan.
previous_attributes delta helper
type AmountRefundedChanged = { amount_refunded?: number };
export function getRefundDelta(event: Stripe.Event, charge: Stripe.Charge): number | null {
const prev = (event.data.previous_attributes as AmountRefundedChanged | undefined)?.amount_refunded;
if (typeof prev === "number") return charge.amount_refunded - prev;
return null; // caller falls back to embedded / list / skip-revocation
}Plan resolver with pagination + G6 safety fallback
export async function resolvePlanFromInvoice(invoice: Stripe.Invoice): Promise<{
plan: Plan;
credits: number;
resolvedVia: "priceMap" | "safetyFallback";
}> {
try {
const items = await scanAllLineItems(invoice); // G3 -- paginate first
for (const item of items) {
const mapped = PRICE_TO_PLAN[item.price?.id ?? ""];
if (mapped) return { ...mapped, resolvedVia: "priceMap" };
}
logEvent("credit_price_resolve_unknown", { invoiceId: invoice.id, lineCount: items.length });
return { plan: "free", credits: 0, resolvedVia: "safetyFallback" }; // G6
} catch (err) {
logEvent("credit_price_resolve_error", { invoiceId: invoice.id, err: String(err) });
return { plan: "free", credits: 0, resolvedVia: "safetyFallback" }; // G6
}
}The email renderer uses resolvedVia to gate plan names on priceMap — never renders "Welcome to Free" on the safety-fallback path (G-bonus).