API: Payments & refunds
Controllers: admin-payments.controller.ts + admin-refunds.controller.ts.
Transactions
Section titled “Transactions”Mount admin/payments.
GET /v1/admin/payments/transactions
Section titled “GET /v1/admin/payments/transactions”Permission: payments:read.
Query filters: rail[], status, country, dateFrom, dateTo, currency, userId, cursor, limit.
Response:
{ "items": [ /* transactions */ ], "nextCursor": "opaque"}Transaction shape:
{ "id": "...", "kind": "coin_pack", "userId": "...", "platform": "apple", "productId": "tc_coins_500_bonus100", "transactionId": "1000000_original", "coinsGranted": 500, "bonusCoins": 50, "amountCents": 499, "currency": "USD", "region": "PH", "status": "completed", "refundReason": null, "createdAt": "..."}GET /v1/admin/payments/transactions/export.csv
Section titled “GET /v1/admin/payments/transactions/export.csv”Permission: payments:read.
Streams CSV of the current filters. Content-Type text/csv.
GET /v1/admin/payments/transactions/:id
Section titled “GET /v1/admin/payments/transactions/:id”Permission: payments:read.
Detail with webhook payload, ledger entries, refund history, related unlocks.
Reconciliation
Section titled “Reconciliation”POST /v1/admin/payments/reconciliation
Section titled “POST /v1/admin/payments/reconciliation”Permission: payments:read.
Multipart form: rail (string), file (CSV). Returns diff report inline:
{ "rail": "apple", "period": { "start": "...", "end": "..." }, "matched": 1204, "missingOnOurSide": [ /* rows */ ], "missingOnProviderSide": [ /* rows */ ], "amountMismatch": [ /* rows */ ]}Refunds
Section titled “Refunds”Mount admin/refunds.
| Method | Path | Perm |
|---|---|---|
| POST | /admin/refunds | payments:refund OR payments:refund_request |
| GET | /admin/refunds | payments:read |
| GET | /admin/refunds/:id | payments:read |
| POST | /admin/refunds/:id/reject | payments:refund |
Create request
Section titled “Create request”{ "purchaseId": "...", "amountCents": 499, "reason": "duplicate_purchase", "reverseUnlocks": false}Response 200 (auto-executed, ≤$50, executor has payments:refund):
{ "id": "...", "status": "executed", "amountCents": 499, "executedAt": "...", "ledgerEventId": "...", "underpaidCents": 0}Response 200 (pending approval, >$50):
{ "id": "...", "status": "pending", "approvalId": "..."}List query filters
Section titled “List query filters”status, dateFrom, dateTo, userId, cursor, limit.
Reject
Section titled “Reject”POST /admin/refunds/:id/reject:
{ "reason": "Wrong purchase: user meant another transaction" }Ledger reversal (on execute)
Section titled “Ledger reversal (on execute)”Executing a refund inserts a matched double-entry pair:
wallet_paid(orwallet_bonus) :-N(reason=‘refund’)sink_refund:+N(reason=‘refund’, ref_type=‘purchase’, ref_id=purchaseId)
If the user’s balance is < N (they spent the coins), the deduction is clamped and underpaidCents is recorded on the refund record.
Related
Section titled “Related”- Approvals API: for approving pending refunds.
- Refund guide: the operator flow.