Skip to content

API: Payments & refunds

Controllers: admin-payments.controller.ts + admin-refunds.controller.ts.

Mount admin/payments.

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.

Permission: payments:read.

Detail with webhook payload, ledger entries, refund history, related unlocks.

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 */ ]
}

Mount admin/refunds.

MethodPathPerm
POST/admin/refundspayments:refund OR payments:refund_request
GET/admin/refundspayments:read
GET/admin/refunds/:idpayments:read
POST/admin/refunds/:id/rejectpayments:refund
{
"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": "..."
}

status, dateFrom, dateTo, userId, cursor, limit.

POST /admin/refunds/:id/reject:

{ "reason": "Wrong purchase: user meant another transaction" }

Executing a refund inserts a matched double-entry pair:

  • wallet_paid (or wallet_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.