BIO.RE
Payment

Get Wallet Activity

Paginated wallet transaction feed with optional type filter. Each row carries balance-before / balance-after for an audit-friendly trail. No wallet → empty page.

GET /api/v1/wallet/activity — 🔑 Bearer · Kill-switched

Returns the calling user's wallet transaction feed — CREDIT / DEBIT / EXPIRY / CHARGEBACK rows, optionally filtered by type. Each item carries balanceBefore / balanceAfter for an audit-friendly trail. Pagination via ?page / ?limit, server-clamped to [1, 20] per page.

No wallet → empty page (not 404). Users without a wallet row yet get { items: [], total: 0, page: 1, limit: 20, totalPages: 0 }. The wallet is created lazily on first POST /wallet/load — until then this endpoint is the safe "show me my history" endpoint that doesn't bootstrap a row.

PAYOUT is a service-side type, not a user-facing one. The DTO's enum lists PAYOUT (creator-side payout records) but the controller's filter validation only accepts CREDIT / DEBIT / EXPIRY / CHARGEBACK. Don't pass ?type=PAYOUT — it's silently ignored (server falls back to "no type filter"). Creator payouts live elsewhere (the payout module, separate scope).

Request

Query parameters

ParamTypeDefaultValidationNotes
typeenumIsIn(['CREDIT','DEBIT','EXPIRY','CHARGEBACK'])Server-side allowlist. Invalid values silently fall back to "no filter".
pagenumber1parseInt, server-clamped to >= 11-based page index
limitnumber20parseInt, server-clamped to [1, 20]Items per page
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKPaginatedApiResponseOf<WalletActivityItemDto>

{
  "success": true,
  "data": {
    "items": [
      {
        "id": "t1a2b3c4-d5e6-7890-abcd-ef1234567890",
        "walletId": "w1a2b3c4-d5e6-7890-abcd-ef1234567890",
        "type": "CREDIT",
        "amount": "25.00",
        "balanceBefore": "100.00",
        "balanceAfter": "125.00",
        "referenceType": "STRIPE_CHECKOUT",
        "referenceId": "cs_test_...",
        "description": "Balance loaded: $25.00",
        "createdAt": "2026-04-29T20:00:00.000Z"
      }
    ],
    "total": 42,
    "page": 1,
    "limit": 20,
    "totalPages": 3
  }
}

Item fields

FieldTypeNotes
idstring (UUID)WalletTransaction.id
walletIdstring (UUID)The wallet this transaction belongs to
typeenumOne of CREDIT / DEBIT / EXPIRY / CHARGEBACK / PAYOUT (PAYOUT not filterable here — see callout)
amountstring (decimal)Decimal as string (always 2 decimals for display; raw db precision may be higher)
balanceBefore / balanceAfterstring (decimal)Snapshot for audit. balanceAfter - balanceBefore will not always equal amount for special types (e.g. EXPIRY may not change balance — read service logic per type).
referenceTypestringWhat kind of object this transaction references (e.g. STRIPE_CHECKOUT, MESSAGE_PAYMENT, BALANCE_PACKAGE)
referenceIdstringThe id of the referenced object — passed by the upstream caller; opaque to clients
descriptionstringServer-rendered free-form description (e.g. "Balance loaded: $25.00")
createdAtstring (ISO 8601)Transaction timestamp

Top-level fields

FieldTypeNotes
itemsarrayUp to limit transactions, ordered by createdAt DESC
totalnumberTotal transactions matching the filter
pagenumberEchoed page index (1-based)
limitnumberEchoed effective per-page limit (post-clamp)
totalPagesnumberMath.ceil(total / limit)

Errors

HTTPcode / i18nKeyReason
401(guard)Missing / invalid bearer token
503features.payment_disabledAdmin kill switch PAYMENT is active

Side effects

  1. Lookup Wallet for (userId, type=FAN).
  2. No wallet — short-circuit return empty page (items: [], total: 0, page: 1, limit: 20, totalPages: 0). No mutations, no DB writes.
  3. Validate / clamp type (allowlist CREDIT|DEBIT|EXPIRY|CHARGEBACKPAYOUT and unknown values fall back to no-filter).
  4. Clamp page = max(1, page), limit = min(max(1, limit), 20).
  5. Two queries in parallel:
    • walletTransaction.findMany({ where: { walletId, ...(type && { type }) }, orderBy: { createdAt: 'desc' }, skip, take }).
    • walletTransaction.count({ where }).
  6. Return paginated envelope.

Code samples

curl 'https://api.bio.re/api/v1/wallet/activity?page=1&limit=20&type=CREDIT' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type WalletTransactionType = 'CREDIT' | 'DEBIT' | 'EXPIRY' | 'CHARGEBACK';

type WalletActivityItem = {
  id: string;
  walletId: string;
  type: WalletTransactionType | 'PAYOUT';
  amount: string;
  balanceBefore: string;
  balanceAfter: string;
  referenceType: string;
  referenceId: string;
  description: string;
  createdAt: string;
};

type WalletActivity = {
  items: WalletActivityItem[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
};

async function getWalletActivity(
  accessToken: string,
  filter: { type?: WalletTransactionType; page?: number; limit?: number } = {},
): Promise<WalletActivity> {
  const url = new URL('https://api.bio.re/api/v1/wallet/activity');
  if (filter.type) url.searchParams.set('type', filter.type);
  if (filter.page) url.searchParams.set('page', String(filter.page));
  if (filter.limit) url.searchParams.set('limit', String(filter.limit));
  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 ?? 'Wallet activity fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery, keepPreviousData } from '@tanstack/react-query';

export const walletKeys = {
  activity: (filter: { type?: string; page?: number; limit?: number }) =>
    ['wallet', 'activity', filter] as const,
};

export function useWalletActivity(filter: {
  type?: WalletTransactionType;
  page?: number;
  limit?: number;
} = {}) {
  return useQuery({
    queryKey: walletKeys.activity(filter),
    queryFn: async () => {
      const url = new URL('/api/v1/wallet/activity', window.location.origin);
      if (filter.type) url.searchParams.set('type', filter.type);
      if (filter.page) url.searchParams.set('page', String(filter.page));
      if (filter.limit) url.searchParams.set('limit', String(filter.limit));
      const res = await fetch(url);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Wallet activity fetch failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as WalletActivity;
    },
    placeholderData: keepPreviousData, // smooth pagination UX
    staleTime: 30_000,
  });
}

Try it

GET
/api/v1/wallet/activity
AuthorizationBearer <token>

In: header

Query Parameters

type?string

Filter by transaction type

Value in"CREDIT" | "DEBIT" | "EXPIRY" | "CHARGEBACK"
page?number

Page number (1-based)

limit?number

Items per page

Response Body

application/json

application/json

curl -X GET "https://loading/api/v1/wallet/activity"
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
        "walletId": "0ecad4a2-3549-43fb-807e-9ff033247480",
        "type": "CREDIT",
        "amount": "25.00",
        "balanceBefore": "100.00",
        "balanceAfter": "125.00",
        "referenceType": "string",
        "referenceId": "string",
        "description": "string",
        "createdAt": "2019-08-24T14:15:22Z"
      }
    ],
    "page": 1,
    "limit": 20,
    "total": 150,
    "totalPages": 8
  }
}
{
  "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/payment/wallet.controller.ts54–73 (getActivity)
DTO (response item)apps/api-core/src/modules/payment/dto/wallet-response.dto.ts58–94 (WalletActivityItemDto)
Serviceapps/api-core/src/modules/payment/wallet.service.ts331–348 (getActivityFeed)
Prisma modelpackages/prisma/prisma/schema.prismaWallet, WalletTransaction (enum WalletTransactionType), LedgerEntry (double-entry, written by service-layer transaction)

On this page