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 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.
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.
- 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.
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.
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.
- '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.