GH GambleHub

Partial and complete refands

TL; DR

Refand is the reverse operation on the captured amount. Full closes the entire transaction, partial returns a part (can be a partial series up to full). Critical: refund-to-source, strict idempotence, reason logging, and orchestration with webhooks/retras. Measure Refund Rate, TtR p95, Refund Error and eliminate duplicates/inconsistencies through auto-reconciliations.

1) Terms and fundamental differences

Full Refund - Returns the entire committed amount ('refund _ amount = capture_amount').
Partial Refund - returns a part ('0 <refund_amount <capture_amount'), allows the rest partial to the total' capture _ amount '.
Refund to Source - return to the original payment method/rails (regulatory preferred/mandatory).
Void - cancellation to capture (if supported by rails), is not considered a refand.
Reversal/Chargeback - bank/rail mechanics outside your initiative (disputes, chargebacks) - not to be confused with refand.

2) When to issue full vs partial

Full:
  • Cancel entire order/service, duplicate write-off, system error.
  • Mandatory if the service is not provided (according to the rules of the consumer/regulator).
Partial:
  • Partial cancellation of the service, proportional adjustments (discounts, compensation for delays).
  • Technical limits rails (maximum amount per operation) - partial series.
  • Post-factum commission withholding (where regulatory is allowed) - less often in iGaming.
Solution: sew up the 'reason _ code × method × jurisdiction' matrix → policy = fullpartialboth, limits, required level of approval.

3) Policies and limits

Refund-to-source = true by default; exceptions - through MLRO/compliance cases (logged).
Cut-off: refands are allowed N days from capture (by method/jurisdiction).
Max Partial Count: no more than K partial per payment (typically K ≤ 5).
Min Partial Amount: not lower than technical minimum rail/PSP.

Approval Matrix:
  • Support agent: partial ≤ X, full ≤ Y.
  • Manager/Finance: over limits, cross-method exceptions.
  • Cooling-off on repeated attempts (anti-bounce).

4) Architecture and event flow

Components:
  • Payment Orchestrator is the source of status truth.
  • Refund Service - API, idempotency, orchestration of retrays, logging.
  • PSP Adapters - Method Integrations.
  • Reconciliation - auto-reconciliations, DLQ, corrections.
  • Ledger/Accounting - postings, defectors, clearing with clearings.
  • Risk/Compliance - Sanctions/SoF checks in controversial scenarios.
Sequence (partial/full):

1. `Refund. Create '(API) validation → (limits, balance, policy, KYC/SoF if necessary).

2. Генерация idempotency_key (`hash(payment_id + refund_amount + reason + nonce)`).

3. The PSP call → 'PENDING'.

4. Webhook/polling → 'SUCCESS '/' FAILED'; when timeout - retrays with the same key.

5. Publication of the event in Kafka → Ledger, BI, alerts.

6. Auto-reconciliation: mapping 'provider _ refund _ id' to registry.

5) Idempotency and anti-takes

The same refand cannot be credited twice: all logic through idempotency storage (KV/Redis + TTL).
Keys on the payment_id × amount × reason (and, if necessary, 'partial _ index').
Retrays use the same key.
Parallel partial are protected by row-level locks/optimistic version on aggregate amounts.

Pseudocode:
python def refund(payment_id, amount, reason, idem_key):
if idem_store. exists(idem_key): return idem_store. get(idem_key)
with tx():
p = db. get_payment(payment_id, for_update=True)
assert p. captured_amount - p. refunded_amount >= amount > 0 r = p. create_refund(amount, reason, status='PENDING', idem_key=idem_key)
resp = psp. refund(p. provider_txid, amount, idem_key)
return finalize(r, resp. status, resp. ext_id)

6) Data model (minimum sufficient)

json
{
"payment_id": "pay_123",
"captured_amount": 150. 00,
"currency": "EUR",
"refunded_amount": 40. 00,
"refunds": [
{
"refund_id": "rf_001",
"type": "partial    full",
"amount": 20. 00,
"reason_code": "PARTIAL_SERVICE",
"idempotency_key": "idem_a1",
"status": "PENDING    SUCCESS    FAILED",
"provider_refund_id": "psp_rf_9xz",
"created_at": "2025-11-03T12:00:00Z",
"credited_at": "2025-11-03T15:05:00Z",
"notes": "ticket #456"
}
],
"flags": {
"refund_to_source": true,
"jurisdiction": "EEA",
"kyc_tier_required": "tier2"
}
}

7) Features on payment rails

Cards (Visa/Mastercard)

Support full/partial; often somewhat partial; TtR depends on the client's bank (T + 1... T + 5 bp).
Webhooks about success come quickly, but enrollment on discharge may be late → we explain in the support templates.

A2A/Open Banking/RTP

Often instant return (reversal/credit push); some providers only support full or 1 partial.
Strict binding to the original account; refund-to-source is required.

Electronic wallets

Normal full/partial; TtR minutes; partial and minimum amount limits.

Vouchers/Prepaid

Usually, the → policy is not available for refund-to-source: return to the internal wallet or re-issue voucher (if the provider knows how). Requires compliance clauses.

Crypto

Rails - volatile; preferably not used as a refand method. If allowed: return to the same address/exchange with documented rate and commissions; AML screening.

8) Accounting, reconciliation and finance

Ledger: 'DR Revenue/CR Cash' postings at capture; on refund - writeback. Partial is reflected proportionally.
Recognition: in iGaming, the refand reduces the GGR of the corresponding period (accounting policy).
Reconciliation: daily reconciliations' merchant _ refund _ id ↔ provider_refund_id', statuses, amounts, FX rates.
FX: fix the logic of the courses (at the time of capture or at the time of refund), where applicable; hold the spread grid.

9) KPIs, targets and alerts (Refund Health)

Refund Rate = 'Refunded _ Tx/ Captured_Tx'.
Refund Amount Ratio = `Refunded_Amount / Captured_Amount`.
TtR p95 = p95 ('credited _ at - created_at') by method.
Refund Error Rate = `Failed / Attempted` (<0. 3%).
Refund-to-Source% ≥ 95% (where available).
Double Refund Incidents = 0.

Alerts:
  • 'TtR p95'is higher than SLO by the P2 → method.
  • Spikes by 'Refund Rate' in one provider/BIN → P1 (check grabs/doubles).
  • Any'Double Refund> 0 '→ P0 (immediate freezing of auto-refands).

10) SQL slices

10. 1 Refand profile

sql
SELECT
DATE_TRUNC('day', r. created_at) AS d,
method_code, provider,
COUNT() FILTER (WHERE r. status='SUCCESS')  AS refunds_ok,
COUNT() FILTER (WHERE r. status='FAILED')  AS refunds_fail,
SUM(r. amount) AS refunded_amount,
PERCENTILE_CONT(0. 95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (r. credited_at - r. created_at))) AS ttr_p95_sec
FROM refunds r
JOIN payments p ON p. payment_id = r. payment_id
GROUP BY 1,2,3;

10. 2 Balance control for partial

sql
SELECT p. payment_id,
p. captured_amount,
SUM(r. amount) AS refunded_sum,
(p. captured_amount - SUM(r. amount)) AS refundable_left
FROM payments p
LEFT JOIN refunds r ON r. payment_id = p. payment_id AND r. status IN ('SUCCESS','PENDING')
GROUP BY 1,2
HAVING (p. captured_amount - SUM(r. amount)) < 0;

11) UX and support

Message templates by methods: we explain the possible delay on discharge to cards, A2A - almost instantly.
Statuses in the office: 'Issued → In process → Returned'; Show the expected enlistment date.
Reasons (reason_code) - human readable: 'Duplicate write-off', 'Service cancellation', 'Partial compensation'.
Self-service partial - safe only with limits and clear rules.

12) Risk and compliance

Anti-laundering: refand should not turn into output to an alternative channel; commit exceptions with MLRO approval.
Sanctions/REP: for returns initiated to "red" accounts/details - mandatory verification.
DSAR/Retention - Store traces of refands within a retention policy.
Local rules: terms and procedure for returns (for example, consumer regulations) - reflected in policy.

13) Frequent mistakes and how to avoid them

Double refand due to lack of idempotency and repeated webhooks → store idem key/status, check the balance.
Partial> balance → row-lock/optimistic version and strict checks.
Cross-method refund without compliance permission → violates refund-to-source.
Mixing void and refund in reports → distortion of KPIs.
There are no auto-checks → black holes between the PSP and your ledger.

14) Playbooks

A surge in provider returns → check authorization failures/capture duplicates, turn on the failover, contact with the PSP.
Mass partial-compensation (campaign) → raise the partial limit, enable group operations, and strengthen reconciliations.
Webhooks error → switch to polling, increase TTL idempotency, postpone auto-refands.
Refund-to-source exception (rare) → MLRO escalation, documented payout, and 'comp _ approved = true'.

15) Test Cases (UAT/Prod)

1. Full refund after one capture → correctly resets the balance.
2. Batch partial (3 ×) → sum ≤ capture; then full for the remainder.
3. Idempotency - Repeat the same query → 1 result.
4. Webhook-bounce: 3 identical notifications → one write-off/credit.
5. Reconciliations: artificial mismatch → alert and auto-correction.
6. Restriction of rights: the agent cannot exceed the partial limit.
7. Cut-off: late refand attempt → correct failure and logging.

16) Implementation checklist

  • full/partial + refund-to-source policies by jurisdiction/method.
  • Idempotency, retreats, webhooks and polling, DLQ.
  • Data model with residual to return and reason_code.
  • Ledger and daily auto-reconciliations.
  • KPI/dashboard: Refund Rate, TtR, Error, Double Refund = 0.
  • Rights and application matrix, support templates.
  • UAT test cases and production-level alerts.

Summary

Refand management is a strict discipline of processes: refund-to-source, idempotency, transparent data model, auto-reconciliation and understandable partial/full policies. With such fundamentals, you keep TtR low, errors near zero, duplicates impossible, and compliance and finance synchronized with business goals.

Contact

Get in Touch

Reach out with any questions or support needs.We are always ready to help!

Telegram
@Gamble_GC
Start Integration

Email is required. Telegram or WhatsApp — optional.

Your Name optional
Email optional
Subject optional
Message optional
Telegram optional
@
If you include Telegram — we will reply there as well, in addition to Email.
WhatsApp optional
Format: +country code and number (e.g., +380XXXXXXXXX).

By clicking this button, you agree to data processing.