React Package · @usethrottle/invoices
React hooks and a server proxy helper for surfacing a buyer's unified invoice list — card captures, Net-N issuances, and subscription charges — inside your storefront portal.
sk_* key and resolves the authenticated buyer's identity before forwarding the read.Install
pnpm add @usethrottle/invoicesBackend proxy · /server entrypoint
Mount createInvoiceProxyHandler from @usethrottle/invoices/server on a catch-all route. The handler accepts any framework that exposes a standard Request object (Next.js App Router, Hono, Remix, etc.).
createInvoiceProxyHandler(options)
apiKey: string— yoursk_*secret key.getCustomerId(request): string | null | Promise<string | null>— called on every request. Return the buyer's external id (e.g. your auth framework's user id). Returningnullcauses the proxy to reply401 unauthorized.baseUrl?: string— defaults tohttps://api.usethrottle.dev.
The proxy allows three read-only paths: /api/v1/invoices (list), /api/v1/invoices/:id (get), and /api/v1/invoices/:id/download (PDF URL). All other paths and non-GET methods return an error. List requests are transparently rewritten to /api/v1/customers/:customerId/invoices so the buyer can only read their own invoices.
// app/api/throttle/invoices/[...path]/route.ts
import { createInvoiceProxyHandler } from '@usethrottle/invoices/server';
import { auth } from '@/lib/auth';
const handler = createInvoiceProxyHandler({
apiKey: process.env.THROTTLE_SECRET_KEY!,
async getCustomerId(request) {
// Return your internal user / customer id (e.g. a Clerk userId).
// The proxy resolves it via GET /api/v1/customers/by-external/:externalId.
const user = await auth();
return user?.id ?? null;
},
});
// Only GET is required; the proxy rejects everything else.
export { handler as GET };Provider
Mount InvoiceProvider above any component tree that uses invoice hooks. The fetcher prop is the bridge to your backend proxy — every hook routes through it.
'use client';
import { InvoiceProvider } from '@usethrottle/invoices';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<InvoiceProvider
fetcher={async (path, init) => fetch(`/api/throttle/invoices${path}`, init)}
>
{children}
</InvoiceProvider>
);
}Hooks
useInvoices(params?, options?)
Lists invoices for the authenticated buyer. Returns a QueryState<PaginatedInvoices> with { data, isLoading, isFetching, error, refetch }.
params: status? (paid | open | refunded | partially_refunded | void), type? (order | net30 | subscription), cursor?, limit?, sort? (asc | desc).
Note: The SDK normalizes the REST meta.pagination envelope into a top-level pagination on the hook result — so with the hooks you read result.data.pagination.cursor / .hasMore, whereas raw REST responses carry it under meta.pagination.
import { useInvoices } from '@usethrottle/invoices';
function InvoiceList() {
const { data, isLoading, error } = useInvoices({
limit: 20,
sort: 'desc',
// Optional filters:
// status: 'paid',
// type: 'subscription',
// cursor: '...',
});
if (isLoading) return <Spinner />;
if (error) return <ErrorView error={error} />;
if (!data) return null;
return (
<>
{data.data.map((invoice) => (
<InvoiceRow key={invoice.id} invoice={invoice} />
))}
{data.pagination?.hasMore && (
<LoadMoreButton cursor={data.pagination.cursor} />
)}
</>
);
}useInvoice(id, options?)
Fetches a single invoice by id. Skips the fetch when id is null or undefined.
import { useInvoice } from '@usethrottle/invoices';
function InvoiceDetail({ id }: { id: string }) {
const { data: invoice, isLoading } = useInvoice(id);
if (isLoading || !invoice) return <Spinner />;
return (
<article>
<h2>{invoice.number}</h2>
<p>Status: {invoice.status}</p>
<p>Total: {invoice.total / 100} {invoice.currency}</p>
{invoice.amountRefunded ? (
<p>Refunded: {invoice.amountRefunded / 100} {invoice.currency}</p>
) : null}
</article>
);
}useDownloadInvoice()
Returns an async function (id: string) => Promise<string> that fetches the signed PDF URL and opens it in a new tab. Returns the raw URL so your UI can also use it as an anchor href.
import { useDownloadInvoice } from '@usethrottle/invoices';
function DownloadButton({ invoiceId }: { invoiceId: string }) {
const download = useDownloadInvoice();
return (
<button onClick={() => download(invoiceId)}>
Download PDF
</button>
);
}To capture the returned URL for custom usage (e.g., render as an anchor href):
import { useDownloadInvoice } from '@usethrottle/invoices';
function InvoiceLink({ invoice }: { invoice: Invoice }) {
const download = useDownloadInvoice();
return (
<a
href="#"
onClick={async (e) => {
e.preventDefault();
await download(invoice.id); // Opens PDF in new tab
}}
>
View PDF
</a>
// Or capture the URL for custom use:
// const url = await download(invoice.id);
// href={url} download={invoice.number}
);
}Cursor pagination
Responses carry a pagination object inside data.pagination. When hasMore is true, pass the opaque cursor string in the next useInvoices call.
import { useState } from 'react';
import { useInvoices } from '@usethrottle/invoices';
function PaginatedList() {
const [cursor, setCursor] = useState<string | null>(null);
const { data, isLoading } = useInvoices({ cursor: cursor ?? undefined, limit: 20 });
return (
<>
{data?.data.map((inv) => <Row key={inv.id} invoice={inv} />)}
{data?.pagination?.hasMore && (
<button
disabled={isLoading}
onClick={() => setCursor(data.pagination!.cursor)}
>
Load more
</button>
)}
</>
);
}Cache invalidation
All hooks share a cache version. Call useInvoiceInvalidation() to bump the version and cause every mounted hook to re-fetch. Use this after a refund or a payment action that changes invoice state.
import { useInvoiceInvalidation } from '@usethrottle/invoices';
function RefundButton({ paymentId }: { paymentId: string }) {
const invalidate = useInvoiceInvalidation();
async function handleRefund() {
await fetch(`/api/payments/${paymentId}/refund`, { method: 'POST' });
// Tell all useInvoices / useInvoice hooks to re-fetch.
invalidate();
}
return <button onClick={handleRefund}>Refund</button>;
}REST routes
The following merchant-facing routes require an invoices:read API key scope.
GET /api/v1/invoices— list invoices for the application (all customers, all source types).GET /api/v1/invoices/:id— fetch a single invoice.GET /api/v1/invoices/:id/download— 302 redirect to a 10-minute signed PDF URL; append?format=jsonto get{ "data": { "url": "..." } }instead.GET /api/v1/customers/:customerId/invoices— list invoices scoped to a specific customer (used internally by the proxy).
All list endpoints accept cursor, limit, sort (asc | desc), status, and type query parameters.
Invoice object fields
id— Throttle invoice id.number— human-readable invoice number (e.g.INV-sprinter-001).status—paid | open | refunded | partially_refunded | void.sourceType—order | net30 | subscription.currency— ISO-4217 currency code.subtotal, taxTotal, shippingTotal, discountTotal, total— integer minor units.amountRefunded— integer minor units refunded (0 when not refunded).issuedAt— ISO-8601 invoice date.dueAt— ISO-8601 due date for Net-N invoices;nullfor card captures and subscriptions.periodStart, periodEnd— billing period for subscription invoices;nullotherwise.createdAt— ISO-8601 record creation timestamp.
Boundaries
- The package is read-only — it surfaces invoices and PDFs but does not create or mutate payment records.
- Payment collection stays in
@usethrottle/checkout-react. This package renders the resulting invoices. - Merchant-wide views (all customers) are available via the REST routes with a secret key; the React hooks are scoped to the authenticated buyer's invoices through the proxy.