Guides
Commerce
Report on finances

Report on finances

Requires: A secret key with READ_FINANCE; add READ_FINANCE_PII for buyer identity.

These reads give a community its own books: every sale, the commission it earned on members' sales, refunds, donations, and what was transferred to it. They are for accounting exports, dashboards and reconciliation.

GET /api/v1/communities/:communityTag/finance/entries
GET /api/v1/communities/:communityTag/finance/payouts
GET /api/v1/communities/:communityTag/finance/summary
GET /api/v1/communities/:communityTag/finance/deposits/:depositId/statement

Secret key only, with READ_FINANCE. Add READ_FINANCE_PII only if you need buyer email and name. A publishable key answers 403 SECRET_KEY_REQUIRED. Every amount is an integer in the smallest currency unit. Reference: Finance.

Entries

let cursor: string | undefined;
do {
  const page = await cobuntu.finance.entries({ from: "2026-10-01", to: "2026-10-31", limit: 500, cursor });
  for (const e of page.data) save(e);
  cursor = page.nextCursor ?? undefined;
} while (cursor);

Oldest first. limit is clamped to 500 rather than refused. Pass back nextCursor, or an entry's own cursor to resume after it. Never build a cursor from id: a malformed cursor is 400 INVALID_CURSOR, never a silent restart. Bad dates are 400 INVALID_INPUT.

Read role before net

{
  "id": "fe_1", "cursor": "...", "type": "COMMISSION", "stream": "DIGITAL",
  "role": "COMMISSION_RECIPIENT", "currency": "EUR",
  "gross": 5000, "net": 1000,
  "fees": { "platform": -250, "stripe": -95, "stripeTax": 0, "memberSeller": -3655 },
  "settlement": { "status": "ESCROW", "payoutId": null, "paidAt": null },
  "buyerRef": "b_8f2c..."
}
  • role: "SELLER": the community sold its own thing; net is its earnings.
  • role: "COMMISSION_RECIPIENT": a member sold and the community keeps only its commission. gross here is the member's sale value. Summing gross across entries gives a number that looks like revenue and is not. Sum net.

Refunds and settlement

A refunded sale carries refund: { refundedAt, amount, status }, and a full refund already reports net: 0. Do not subtract the refund again.

settlement.status says where the money is:

StatusMeans
ESCROWInside the refund window.
HOLDReleasable, below the transfer threshold.
ELIGIBLEWaiting for the next transfer run.
PAIDTransferred; payoutId joins to /finance/payouts.
FAILEDA transfer was attempted and did not succeed.
BLOCKEDNot a sanction: the sale will never pay out because it was fully refunded. Render it as refunded.

Buyers

Without READ_FINANCE_PII, entries carry only buyerRef: opaque, stable per buyer, null for guests and after the buyer exercises erasure. With it, buyer adds email and name.

Payouts

const { data } = await cobuntu.finance.payouts({ from: "2026-10-01" });
// [{ id: "po_1", status: "PAID", currency: "EUR", amount: 120000, salesCount: 14, paidAt, stripeTransferId }]

These are transfers into the community's Stripe balance, not its bank statement. Stripe then pays out to the bank on its own schedule, often batching several of these into one bank credit.

Totals

const { data, window } = await cobuntu.finance.summary({ from: "2026-01-01", to: "2026-12-31" });
// [{ currency: "EUR", net: 845000, count: 212, byStream: { EVENT: { count, net } }, byType: { SALE: { count, net } } }]

One bucket per currency, never summed across currencies. Computed from the same entries /finance/entries serves, so the two agree.

One bank credit

The payout.deposited webhook carries a depositId. The statement lists the payouts, and their invoice numbers, behind it:

const { data } = await cobuntu.finance.depositStatement(depositId);
// { deposit: { amount: 240000, ... }, lines: [...], totals: { covered: 240000, deposit: 240000, unaccounted: 0 } }

unaccounted is normally 0; non-zero means the deposit also carried money that did not come through Cobuntu. It is a statement, not an invoice: each payout in it was already invoiced.

The client returns JSON only. For the PDF, fetch the same path yourself:

const res = await fetch(
  `https://api.cobuntu.com/api/v1/communities/your-community/finance/deposits/${depositId}/statement?format=pdf`,
  { headers: { "X-API-Key": process.env.COBUNTU_API_KEY! } },
);
const pdf = await res.arrayBuffer();

Keeping a copy in sync

Entries are append-only, but figures can be restated in place when Stripe settles a fee differently from its estimate. Follow the cursor for new entries, and periodically re-pull a recent window (for example the last 60 days) and upsert by id.