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
| Param | Type | Default | Validation | Notes |
|---|---|---|---|---|
type | enum | — | IsIn(['CREDIT','DEBIT','EXPIRY','CHARGEBACK']) | Server-side allowlist. Invalid values silently fall back to "no filter". |
page | number | 1 | parseInt, server-clamped to >= 1 | 1-based page index |
limit | number | 20 | parseInt, server-clamped to [1, 20] | Items per page |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — PaginatedApiResponseOf<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
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | WalletTransaction.id |
walletId | string (UUID) | The wallet this transaction belongs to |
type | enum | One of CREDIT / DEBIT / EXPIRY / CHARGEBACK / PAYOUT (PAYOUT not filterable here — see callout) |
amount | string (decimal) | Decimal as string (always 2 decimals for display; raw db precision may be higher) |
balanceBefore / balanceAfter | string (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). |
referenceType | string | What kind of object this transaction references (e.g. STRIPE_CHECKOUT, MESSAGE_PAYMENT, BALANCE_PACKAGE) |
referenceId | string | The id of the referenced object — passed by the upstream caller; opaque to clients |
description | string | Server-rendered free-form description (e.g. "Balance loaded: $25.00") |
createdAt | string (ISO 8601) | Transaction timestamp |
Top-level fields
| Field | Type | Notes |
|---|---|---|
items | array | Up to limit transactions, ordered by createdAt DESC |
total | number | Total transactions matching the filter |
page | number | Echoed page index (1-based) |
limit | number | Echoed effective per-page limit (post-clamp) |
totalPages | number | Math.ceil(total / limit) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
503 | features.payment_disabled | Admin kill switch PAYMENT is active |
Side effects
- Lookup
Walletfor(userId, type=FAN). - No wallet — short-circuit return empty page (
items: [],total: 0,page: 1,limit: 20,totalPages: 0). No mutations, no DB writes. - Validate / clamp
type(allowlistCREDIT|DEBIT|EXPIRY|CHARGEBACK—PAYOUTand unknown values fall back to no-filter). - Clamp
page = max(1, page),limit = min(max(1, limit), 20). - Two queries in parallel:
walletTransaction.findMany({ where: { walletId, ...(type && { type }) }, orderBy: { createdAt: 'desc' }, skip, take }).walletTransaction.count({ where }).
- 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
Authorization
bearer In: header
Query Parameters
Filter by transaction type
"CREDIT" | "DEBIT" | "EXPIRY" | "CHARGEBACK"Page number (1-based)
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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/payment/wallet.controller.ts | 54–73 (getActivity) |
| DTO (response item) | apps/api-core/src/modules/payment/dto/wallet-response.dto.ts | 58–94 (WalletActivityItemDto) |
| Service | apps/api-core/src/modules/payment/wallet.service.ts | 331–348 (getActivityFeed) |
| Prisma model | packages/prisma/prisma/schema.prisma | Wallet, WalletTransaction (enum WalletTransactionType), LedgerEntry (double-entry, written by service-layer transaction) |
Create Wallet Load Session
Create a Stripe Checkout session for a wallet top-up. Validates min/max/balance-cap via admin config. Idempotency-Key header recommended for retry safety. Actual credit happens server-side via webhook.
Request a Payout
Creator requests a payout from their wallet via Stripe Connect or bank transfer. Long pre-condition gate (KYC, tax form, payout method readiness, balance, cooldown). Kill-switchable. Returns just the new payoutId.