High-Level Design: Split Tender Refunds
| Field | Detail |
|---|---|
| Author(s) | ranga_chenna@optum.com |
| Reviewers | BabuSa@Optum.com,syamala01@optum.com |
| Status | Final |
| Last Updated | 2026-02-19 |
| Version | 1.0 |
This is a high-level design document intended for planning and alignment purposes. All numbers, statuses, thresholds, and timelines mentioned are illustrative and subject to change during implementation. This document is not a specification or implementation contract.
1. Overview
Split Tender allows a customer to pay for a single order using exactly two payment methods (e.g., two cards, a card and a bank account). This design assumes a maximum of two payment methods per transaction — support for more than two is out of scope. When a refund is needed on such a transaction, the system must determine how to distribute the refund across the original payment methods and handle scenarios where one or both refund legs fail.
This design covers how the CCG (Convenient Checkout Gateway) platform orchestrates refunds for split tender transactions via the v2/refunds API — including full refunds, partial refunds, multiple refunds, and failure handling.
- Product — Understand refund scenarios, outcomes, and customer impact. Focus on: clear refund outcomes; merchants can resolve partial failures; no money "stuck".
- Engineering — Understand the orchestration flow, refund eligibility checks, error handling, Stripe integration, and status mapping.
- QA — Understand testable scenarios, edge cases (expired cards, zero balance, time limits), and expected statuses.
- API Consumers (Merchants) — Understand the
v2/refundscontract: what to send, what to expect back, predictable statuses, and clear error codes. - Finance — Accurate refund accounting, no over-refunds, and an audit trail per payment method.
2. Problem Statement
- A split tender payment involves two separate charges on two separate payment methods. Refunding such a transaction is not a single operation — it requires coordinated refunds across both methods.
- If one refund succeeds and the other fails (e.g., card expired, account closed), the merchant needs a clear status and a path to resolve the remaining amount.
- Without this design, refund handling for split tender would be manual, error-prone, and inconsistent across merchants.
3. Scope & Out of Scope
In Scope
| # | Item |
|---|---|
| 1 | Support full refunds (return all money across both payment methods) |
| 2 | Support partial refunds (return a specific amount from one or both methods) |
| 3 | Support multiple refunds (refund in stages) |
| 4 | Return clear statuses: REFUNDED, PARTIAL_REFUND, REFUND_FAILED, or PENDING |
Out of Scope
| # | Item | Reason |
|---|---|---|
| 1 | Unlinked refunds / credits for split tender | Out of scope for initial release — merchants can use the existing standard v1/refunds unlinked refund flow to resolve failed legs; native support may be considered in a future iteration |
| 2 | Automatic retry of failed refund legs | Merchant decides next steps on partial failures; automatic retry logic may be evaluated in a future phase |
| 3 | UI widget changes for refund initiation | Refunds are API-only operations for merchants in this release |
| 4 | Chargeback / dispute resolution | Handled by a separate disputes process |
| 5 | End-customer notification | CCG notifies the merchant via webhooks; how the merchant communicates with their customer is outside CCG scope |
| 6 | Auto-skip zero-balance methods | Planned for a future phase once the Balance Tracker is implemented; in the initial release, Stripe is queried directly |
| 7 | Pre-processing eligibility validation (status, amounts, time limits) | Basic validation is performed, but comprehensive pre-flight checks (e.g., local balance verification) depend on the Balance Tracker |
4. Current State (Before Split Tender Refunds)
Split tender payments already use the v2/payments APIs. However, refunds for split tender transactions are not yet supported at the platform level. Today, standard (single-method) refunds work as follows:
Limitations with split tender:
- The
v1/refundsAPI handles one payment at a time — it has no concept of a parent transaction with two child payments. - There is no balance tracking across multiple payment methods within a single order.
- If a merchant manually refunds each leg separately, there is no coordinated status or rollback.
- The
v1refund API cannot determine the correct final status across both legs of a split tender payment.
5. Proposed Design (After)
The v2/refunds API introduces split-tender-aware refund orchestration:
Key Components
| Component | Responsibility |
|---|---|
| CCG Refund API (v2) | Receives refund request, validates, orchestrates refunds across payment methods |
| Refund Eligibility Check | In the initial release, CCG issues the refund request directly to Stripe, and Stripe performs its own validation — failing the request if eligibility checks (e.g., insufficient balance, expired refund window) are not met. A dedicated internal Balance Tracker is planned as a future enhancement to enable local pre-validation before reaching Stripe. |
| Payment Processor (Stripe) | Executes the actual refund on each payment method |
| Webhook Service | Sends refund status updates to the merchant at the parent transaction level |
6. Data Flow
The step-by-step flow for a split tender refund:
Split tender refunds are always triggered in parallel — both payment methods are refunded at the same time. CCG waits for both results before determining the final status.
Processing Order
| Payment Combination | Processing Model | Reason |
|---|---|---|
| Card + Card | Parallel | Both refunds triggered simultaneously |
| ACH + ACH | Parallel | Both refunds triggered simultaneously |
| Card + ACH | Parallel | Both refunds triggered simultaneously |
7. Refund Scenarios Key Benefits
The statuses and scenario outcomes below are proposed and subject to change during implementation.
| # | Scenario | What happens | Final Status |
|---|---|---|---|
| 1 | Full refund — both succeed | Both payment methods refunded in full | REFUNDED |
| 2 | Full refund — one fails | One succeeds, one fails (expired card, closed account, etc.) | PARTIAL_REFUND |
| 3 | Full refund — both fail | Both refund legs fail (e.g., both accounts closed) | REFUND_FAILED |
| 4 | Partial refund — amounts specified | Merchant specifies amount per payment method; both succeed | REFUNDED |
| 5 | Partial refund — one method only | Merchant specifies an amount for only one of the two methods; that single refund succeeds | REFUNDED |
| 6 | Multiple refunds (happy path) | Merchant issues refunds over time; both methods still have remaining balance on each request; all legs succeed | REFUNDED |
| 7 | Refund exceeds available balance | Requested amount is more than what remains; Stripe rejects the request | REFUND_FAILED |
| 8 | Refund below minimum amount | Requested amount is less than the minimum ($0.01); Stripe rejects the request | REFUND_FAILED |
| 9 | Refund on disputed payment | One payment is under dispute; refund not possible on it | PARTIAL_REFUND |
| 10 | Subsequent refund — zero balance on one method | One method already fully refunded. In the initial release, CCG does not track balances locally, so it forwards both legs to Stripe. Stripe rejects the zero-balance leg; the other leg is processed normally. | PARTIAL_REFUND |
| 11 | Refund after 180 days | Time limit exceeded; Stripe rejects the request | REFUND_FAILED |
| 12 | Concurrent refund with same merchantTransactionId | A refund is already in progress for this merchantTransactionId; the second request is rejected by CCG | REFUND_FAILED |
| 13 | Held refund (insufficient Stripe balance) | Stripe does not have enough available balance to process the refund immediately; refund is queued until balance is available. PENDING may eventually transition to REFUNDED (once balance is available) or remain held until resolved. Merchants should monitor for the final webhook. | PENDING (until resolved) |
| 14 | Stripe unavailable or timeout | Stripe is unreachable or does not respond within the expected time; CCG treats the leg as failed | PARTIAL_REFUND or REFUND_FAILED |
8. Validation Rules Summary
Specific values (e.g., 180-day window, $0.01 minimum) are indicative and subject to change based on business and processor requirements.
Before processing any split tender refund, CCG validates:
| Rule | Check |
|---|---|
| Unique Transaction ID | merchantTransactionId must not be a duplicate; rejected if a refund with the same ID already exists |
| Parent Transaction Exists | The parent transaction ID must exist in the system |
| Payment Status | Parent transaction must be in COMPLETED status before a refund can be initiated |
| Amount Constraints | Refund amount must not exceed the remaining balance; minimum refund is $0.01 per method |
| Time Limit | Refund must be requested within 180 days of the original transaction; requests beyond this window are rejected by Stripe |
| Already Fully Refunded | A payment method with zero remaining balance cannot be refunded again; in the initial release, this is enforced by Stripe (which rejects the request), not by CCG locally |
| Payment Method Valid | Child payment IDs provided in the request must belong to the specified parent transaction |
| Partial Refund Allocation | When per-method amounts are specified, each amount must reference a child payment that belongs to the parent transaction; amounts cannot be assigned to unrelated payments |
| No Concurrent Refund | If a refund is already in progress for the same parent transaction, the new request is rejected to prevent over-refunds |
9. Key Decisions
| # | Decision | Reasoning |
|---|---|---|
| 1 | Partial success IS accepted for refunds — one leg can succeed while the other fails | A partial refund is better than no refund; merchant can resolve the rest manually |
| 2 | Initial release uses Stripe as source of truth for refundable amounts | Reduces initial complexity; a dedicated internal Balance Tracker is planned as a future enhancement to improve performance and enable local validation |
| 3 | Webhooks fire at parent level only | Simplifies merchant integration; one event per refund, not two |
| 4 | Merchants must specify per-payment amounts for partial refunds | System does not auto-distribute — avoids ambiguity in refund allocation |
| 5 | Parallel processing for all refund combinations | Both payment method refunds are triggered simultaneously; CCG waits for both results before determining the final status |
| 6 | Concurrent refund requests with the same merchantTransactionId are rejected | If a refund request is already in progress for the same merchantTransactionId, subsequent requests with that ID are rejected |
| 7 | v1/refunds remains available for individual payment refunds | Merchants can still use v1/refunds for refunds |
10. Future Enhancement: Balance Tracker
🔮 Balance Tracker — Planned for a Future Release (click to expand)
Note: The Balance Tracker is planned as a future improvement and is not part of the initial release of split tender refunds. In the initial release, refund eligibility and remaining amounts are determined by the payment processor (Stripe) directly.
What is the Balance Tracker?
The Balance Tracker is a capability within CCG that will allow the platform to know the remaining refundable amount per child payment locally — without relying on Stripe for every request. The specific implementation approach (e.g., maintaining a running balance in the database, or computing it on the fly from completed refund records) will be determined during the implementation phase.
Once available, it will enable:
- Faster validation — check refund eligibility locally without relying on Stripe to fail the request; reject ineligible refunds upfront with clear error messages
- Accurate multi-refund tracking — know the remaining refundable amount across multiple partial refunds
- Over-refund prevention — reject requests that exceed the remaining balance at the platform level, before reaching Stripe
- Automatic zero-balance skipping — detect which payment methods are fully refunded and skip them without sending a request to Stripe
Worked Example: Partial Refund → Complete Refund
Setup: A customer pays $100.00 for an order using two payment methods:
- Card (Child Payment 1): $60.00
- ACH (Child Payment 2): $40.00
Step 1 — Initial State (after payment)
| Child Payment | Method | Charged | Refunded So Far | Remaining Balance |
|---|---|---|---|---|
| Payment 1 | Card | $60.00 | $0.00 | $60.00 |
| Payment 2 | ACH | $40.00 | $0.00 | $40.00 |
| Total | $100.00 | $0.00 | $100.00 |
Step 2 — Partial Refund ($25 from Card)
The merchant issues a partial refund of $25.00 from the Card only.
- CCG determines Card has $60.00 remaining → eligible
- Refund of $25.00 is sent to Stripe → succeeds
- Remaining balance for Card is now $35.00
| Child Payment | Method | Charged | Refunded So Far | Remaining Balance |
|---|---|---|---|---|
| Payment 1 | Card | $60.00 | $25.00 | $35.00 |
| Payment 2 | ACH | $40.00 | $0.00 | $40.00 |
| Total | $100.00 | $25.00 | $75.00 |
Result: REFUNDED — the $25.00 partial refund on the Card succeeded.
Step 3 — Complete Refund (refund all remaining)
The merchant now requests a full refund for the remaining balance. CCG determines $35.00 (Card) + $40.00 (ACH) = $75.00 remaining.
Both refunds are triggered in parallel:
- Card: Refund $35.00 → succeeds
- ACH: Refund $40.00 → succeeds
Both methods now have $0.00 remaining.
| Child Payment | Method | Charged | Refunded So Far | Remaining Balance |
|---|---|---|---|---|
| Payment 1 | Card | $60.00 | $60.00 | $0.00 |
| Payment 2 | ACH | $40.00 | $40.00 | $0.00 |
| Total | $100.00 | $100.00 | $0.00 |
Result: REFUNDED — the remaining $35.00 (Card) + $40.00 (ACH) = $75.00 refunded. Both methods now have zero remaining balance.
Step 4 — Subsequent Refund Attempt (rejected)
The merchant tries to refund again. CCG determines $0.00 remaining across both methods → request is rejected immediately with NOT_ACCEPTABLE, without querying Stripe.
Sequence Diagram (with Balance Tracker)
11. Dependencies
| Dependency | Type | Owner | Risk Level |
|---|---|---|---|
| Stripe Refund API | External | Stripe | Medium — subject to Stripe rate limits and available balance |
| CCG v2 Payment API | Internal | CCG Team | Low |
| Webhook Infrastructure | Internal | CCG Team | Low |
| Balance Tracking (CCG DB) | Internal (Future) | CCG Team | Low (initial release) — not in scope for first release; see Section 10 |
| Stripe Connect Account Balance | External | Stripe | Medium — refunds come from available balance; autodebit setting matters |
12. Success Metrics
The targets below are illustrative and subject to change during implementation. Final thresholds will be agreed upon by Product and Engineering before release.
| Metric | Indicative Target |
|---|---|
| Refund success rate (both legs succeed) | ≥ 95% |
| Partial refund resolution time (merchant acts) | Within 48 hours |
| Refund processing time (end-to-end) | Cards: 1–3 days; ACH: 3–5 days |
| Over-refund incidents | Zero |
13. Risks & Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| One refund leg fails permanently (closed account) | High | Return PARTIAL_REFUND; merchant can issue an unlinked refund to resolve the failed portion |
| Both refund legs fail | High | Return REFUND_FAILED; merchant retries or issues manual refunds |
| Stripe available balance insufficient for refund | Medium | If autodebit is off, the refund is held (queued) by Stripe until the balance is available. CCG reports the refund as PENDING and sends an updated webhook once Stripe resolves it. Merchants should monitor for the final webhook. If autodebit is on, Stripe debits the connected account's bank automatically. |
| Balance tracking miscalculation (future) | Medium | Applies only after Balance Tracker is implemented; mitigated by thorough unit/integration tests and a reconciliation job |
| ACH refund takes 3–5 days — split tender has mixed timing | Low | Clearly communicate per-method timelines in documentation |
| Refund requested after 180 days | Low | Validate time limit upfront; return clear error message |
14. Related Documentation
| Document | Link |
|---|---|
| Split Tender Payments (Business) | Split Tender Payments |
| Split Tender Refund Scenarios (Business) | Refund Scenarios |
| Split Tender Refund Business Rules | Business Rules |
| Split Tender Linked Refunds | Linked Refunds |
| Standard Refund API (Developers) | Refund API |
| Refund Business Overview | Refunds |
| Refund Error Codes | Error Codes |
15. Glossary
| Term | Definition |
|---|---|
| Split Tender | A payment method where a customer pays for a single order using exactly two payment methods |
| Parent Transaction | The top-level order or session that groups the two child payments together |
| Child Payment | One of the two individual charges within a split tender transaction |
| Linked Refund | A refund that references the original payment transaction — money goes back to the original method |
| Unlinked Refund | A refund issued to a payment method without referencing a prior charge — used as a fallback when linked refund fails |
| Balance Tracker | (Future enhancement) A capability within CCG that will allow the platform to determine remaining refundable amounts per child payment locally, without relying on Stripe for every request — see Section 10 |
| REFUNDED | Final status when all requested refund amounts have been successfully returned |
| PARTIAL_REFUND | Final status when one refund leg succeeded but the other failed |
| REFUND_FAILED | Final status when both refund legs failed or the request was invalid |
| PENDING | Interim status when a refund is queued (e.g., waiting for sufficient Stripe balance) |
| Autodebit | A Stripe Connect setting: when enabled, Stripe automatically debits the connected account's bank to cover refunds when the Stripe balance is insufficient |
| ACH | Automated Clearing House — a bank-to-bank transfer method; ACH refunds typically take 3–5 business days |
| Webhook | An automated notification sent by CCG to the merchant's server when a refund status changes |
merchantTransactionId | A unique reference number that the merchant includes with each refund request to prevent duplicate processing |
202 Accepted | An acknowledgment from the system meaning "your request has been received and will be processed shortly" — not the final result |
| Refund Leg | One half of a split tender refund — since there are two payment methods, each individual refund is called a "leg" |
| Orchestration | The coordination of multiple steps (in this case, two refunds) into a single managed process |
v1/refunds / v2/refunds | Version 1 and Version 2 of the CCG refund API; v1 handles single-method refunds, v2 adds support for split tender |