BIO.RE
Payout

Get Payout Report

Creator-scoped payout history. Optional month filter (YYYY-MM, validated). Returns up to 500 most-recent rows, total amount, and count.

GET /api/v1/payouts/report — 🔑 Bearer

Returns the creator's own payout history for the lifetime of the account or for a specific month. The response includes the raw rows (newest first, capped at 500), a sum of amounts as a number, and a count of rows returned. Every payout — regardless of status (PENDING / APPROVED / PROCESSING / PROCESSED / FAILED / CANCELLED / REJECTED) — is included in total.

total sums ALL statuses, not just successfully paid out. The aggregate is payouts.reduce((sum, p) => sum + p.amount, 0) over the unfiltered result set — REJECTED and CANCELLED rows count toward the sum. If you want "actually paid out," filter by status === 'PROCESSED' client-side and sum yourself.

Hard cap of 500 rows. take: 500 — no pagination. If a creator has more than 500 payouts in the queried window, the older rows are silently dropped. In practice this is unlikely for an individual creator, but for ?month= queries you'll get every row in the month up to 500.

month is strictly validated. Format must match ^\d{4}-(0[1-9]|1[0-2])$ — e.g. 2026-04. Anything else (?month=2026/04, ?month=2026-13, ?month=April) → 400 payment.payout.error.invalid_month. Without ?month, the report covers all-time.

Kill-switchable. Class is decorated with @RequireKillSwitch('PAYOUT') — when admin engages the kill_switch.PAYOUT config, this read endpoint returns 503 too (the switch is class-wide, not per-handler).

Request

Query parameters

ParamTypeRequiredNotes
monthstringoptionalYYYY-MM format (e.g. 2026-04). Strictly validated server-side; mismatch → 400. Filters createdAt >= 2026-04-01 AND < 2026-05-01.

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>Global JwtAuthGuard. Creator identity comes from JWT.

Response

200 OKApiResponseOf<PayoutReportResponseDto>

{
  "success": true,
  "data": {
    "payouts": [
      {
        "id": "po-uuid-1",
        "creatorId": "cr-uuid-1",
        "amount": "50.00",
        "method": "STRIPE_CONNECT",
        "status": "PROCESSED",
        "createdAt": "2026-04-20T09:00:00.000Z",
        "approvedAt": "2026-04-20T11:00:00.000Z",
        "processedAt": "2026-04-21T08:00:00.000Z",
        "stripePayoutId": "po_1ABC...",
        "commissionRate": 20,
        "commissionAmount": "10.00",
        "netAmount": "40.00"
      }
    ],
    "total": 50,
    "count": 1
  }
}

Top-level fields

FieldTypeNotes
payoutsarrayUp to 500 rows, newest first. PayoutRequest shape (see below).
totalnumberSum of payouts[].amount as JS number (via Decimal.toNumber()). Sums every status — see warn callout.
countnumberpayouts.length. Capped at 500 — equals length, not "total matching rows in DB."

Item fields (payouts[])

FieldTypeNotes
idstring (UUID)PayoutRequest.id.
creatorIdstring (UUID)CreatorProfile.id — always your own.
amountstringDecimal(10, 2) serialized as a string (e.g. "50.00"). Parse with Number() or a decimal lib.
methodenumSTRIPE_CONNECT / BANK_TRANSFER.
statusenumPENDING / APPROVED / PROCESSING / PROCESSED / FAILED / CANCELLED / REJECTED.
createdAtstring (ISO 8601)When the request was made.
approvedAtstring (ISO 8601) | nullWhen admin approved. null for PENDING / REJECTED.
processedAtstring (ISO 8601) | nullWhen the actual payout cleared. null until PROCESSED.
stripePayoutIdstring | nullStripe payout id (only for STRIPE_CONNECT payouts that reached PROCESSING+).
commissionRatenumber | nullCommission percentage applied (e.g. 20 = 20%). Persisted at approval time for audit.
commissionAmountstring | nullDecimal(10, 2) as string — commission deducted.
netAmountstring | nullDecimal(10, 2) as string — amount - commissionAmount.

Errors

HTTPi18nKeyWhen
400payment.payout.error.invalid_month?month=... doesn't match `^\d4-(0[1-9]
401(guard)Missing / invalid bearer token.
404payment.payout.error.profile_not_foundThe authenticated user has no CreatorProfile (controller resolves this before calling the service).
503(kill switch)kill_switch.PAYOUT is engaged.

Side effects

  1. KillSwitchGuard (PAYOUT system); engaged → 503.
  2. JwtAuthGuard (global). userId = req.user.id.
  3. Controller resolves the creator: prisma.creatorProfile.findUnique({ where: { userId } }). Missing → 404 profile_not_found.
  4. Service getPayoutReport(creator.id, month?):
    • If month set, validate regex; build where.createdAt = { gte: firstDayOfMonth, lt: firstDayOfNextMonth }. (Boundaries computed in the API server's local time zone via new Date(year, m - 1, 1).)
    • prisma.payoutRequest.findMany({ where, orderBy: { createdAt: 'desc' }, take: 500 }).
    • total = sum(amount) as Decimal.toNumber().
  5. Return { payouts, total, count: payouts.length }. No mutations.

Code samples

curl https://api.bio.re/api/v1/payouts/report \
  -H "Authorization: Bearer $ACCESS_TOKEN"
curl 'https://api.bio.re/api/v1/payouts/report?month=2026-04' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type PayoutStatus =
  | 'PENDING' | 'APPROVED' | 'PROCESSING' | 'PROCESSED'
  | 'FAILED' | 'CANCELLED' | 'REJECTED';

type PayoutReportItem = {
  id: string;
  creatorId: string;
  amount: string;            // Decimal as string
  method: 'STRIPE_CONNECT' | 'BANK_TRANSFER';
  status: PayoutStatus;
  createdAt: string;
  approvedAt: string | null;
  processedAt: string | null;
  stripePayoutId: string | null;
  commissionRate: number | null;
  commissionAmount: string | null;
  netAmount: string | null;
};

type PayoutReport = {
  payouts: PayoutReportItem[];
  total: number;             // sums ALL statuses
  count: number;
};

async function getPayoutReport(
  accessToken: string,
  month?: string,            // YYYY-MM
): Promise<PayoutReport> {
  const url = new URL('https://api.bio.re/api/v1/payouts/report');
  if (month) url.searchParams.set('month', month);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Report failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}

// "Actually paid out" — server's `total` includes rejected/cancelled, so derive yourself:
function actuallyPaid(report: PayoutReport): number {
  return report.payouts
    .filter(p => p.status === 'PROCESSED')
    .reduce((s, p) => s + Number(p.netAmount ?? p.amount), 0);
}
import { useQuery } from '@tanstack/react-query';

export const payoutKeys = {
  report: (month?: string) => ['payout', 'report', month ?? 'all'] as const,
};

export function usePayoutReport(month?: string) {
  return useQuery({
    queryKey: payoutKeys.report(month),
    queryFn: async () => {
      const url = new URL('/api/v1/payouts/report', location.origin);
      if (month) url.searchParams.set('month', month);
      const res = await fetch(url);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Report failed'), {
          code: json?.error?.code,
        });
      }
      return json.data as PayoutReport;
    },
    staleTime: 60_000,
  });
}

Try it

GET
/api/v1/payouts/report
AuthorizationBearer <token>

In: header

Query Parameters

month?string

Month in YYYY-MM format

Response Body

application/json

application/json

application/json

curl -X GET "https://loading/api/v1/payouts/report"
{
  "success": true,
  "data": {
    "payouts": [
      {
        "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
        "creatorId": "688ebf54-d343-4104-8711-82c2feac534a",
        "amount": "50.00",
        "method": "STRIPE_CONNECT",
        "status": "PENDING",
        "createdAt": "2019-08-24T14:15:22Z",
        "approvedAt": "2019-08-24T14:15:22Z",
        "processedAt": "2019-08-24T14:15:22Z",
        "stripePayoutId": "string",
        "commissionRate": 0,
        "commissionAmount": "10.00",
        "netAmount": "40.00"
      }
    ],
    "total": 42,
    "count": 10
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/payout/payout.controller.ts43–53 (getReport — creator lookup + month forwarding)
DTO (response)apps/api-core/src/modules/payout/dto/payout-client-response.dto.ts15–65 (PayoutReportResponseDto, PayoutReportItemDto)
Serviceapps/api-core/src/modules/payout/payout.service.ts524–537 (getPayoutReport — month regex validation, 500-row cap, total sum)
Kill switchapps/api-core/src/common/guards/kill-switch.guard.ts17–33 (@RequireKillSwitch)
Prisma modelpackages/prisma/prisma/schema.prismaPayoutRequest lines 1183–1210, PayoutStatus enum 772–781, PayoutMethod enum 783–786

On this page