Create a Reversal (Refund)
Overview
Create a Reversal to refund or reverse a previously successful Transfer. Reversals return funds to the customer and are themselves represented as Transfers with type "REVERSAL".
Resource Access
- User Permissions: Admin users only
- Endpoint:
POST /transfers/\{transfer_id}/reversals
Arguments
| Parameter | Type | Required | Description |
|---|---|---|---|
| transfer_id | string | Yes | ID of original Transfer to reverse (in URL path) |
| refund_amount | integer | No | Amount to refund in cents (defaults to full amount) |
| tags | object | No | Custom key-value metadata for the reversal |
Example Request (Full Refund)
curl -X POST \
'https://api.ahrvo.network/payments/na/transfers/TRtransfer789/reversals' \
-u username:password \
-H 'Content-Type: application/json' \
-d '{
"tags": {
"reason": "customer_request",
"requested_by": "customer_service",
"ticket_id": "TICKET-5678"
}
}'
Example Request (Partial Refund)
curl -X POST \
'https://api.ahrvo.network/payments/na/transfers/TRtransfer789/reversals' \
-u username:password \
-H 'Content-Type: application/json' \
-d '{
"refund_amount": 5000,
"tags": {
"reason": "partial_return",
"items_returned": "2_of_5",
"restocking_fee": "applied"
}
}'
Example Response
{
"id": "TRreversal123",
"created_at": "2023-12-11T15:30:00Z",
"updated_at": "2023-12-11T15:30:00Z",
"amount": 10000,
"currency": "USD",
"state": "PENDING",
"type": "REVERSAL",
"merchant": "MUmerchant123",
"source": null,
"destination": "PIcreditCard456",
"fee": 0,
"ready_to_settle_at": null,
"trace_id": "TRC_98765435",
"parent_transfer": "TRtransfer789",
"tags": {
"reason": "customer_request",
"requested_by": "customer_service",
"ticket_id": "TICKET-5678"
},
"_links": {
"self": {
"href": "https://api.ahrvo.network/payments/na/transfers/TRreversal123"
},
"parent": {
"href": "https://api.ahrvo.network/payments/na/transfers/TRtransfer789"
},
"merchant": {
"href": "https://api.ahrvo.network/payments/na/merchants/MUmerchant123"
},
"payment_instrument": {
"href": "https://api.ahrvo.network/payments/na/payment_instruments/PIcreditCard456"
}
}
}
Additional Information
- Reversal as Transfer: Reversals ARE Transfers
- Have their own Transfer ID (starts with "TR")
- Type = "REVERSAL"
- Can be fetched via GET /transfers/{reversal_id}
- Appear in GET /transfers list
- Subject to same states (PENDING, SUCCEEDED, FAILED)
- Full vs Partial Refunds:
- Full refund: Omit
refund_amountor set to original amount- Returns entire payment
- Most common case
- Simpler workflow
- Partial refund: Set
refund_amountto portion- In cents
- Must be ≤ original amount
- Can create multiple partial refunds
- Example: $100 charge → $30 refund + $20 refund = $50 total refunded
- Full refund: Omit
- Refund Amount Limits:
- Cannot exceed original transfer amount
- Cannot exceed un-refunded amount
- Multiple partial refunds allowed
- Example: $100 transfer → Can refund $60 + $40, but not $60 + $60
- Timing:
- Card refunds: 5-10 business days to appear
- Processor processes quickly
- Bank may hold 5-10 days
- Customer sees "pending" during hold
- ACH refunds: 3-5 business days
- Same as original ACH timing
- Instant: Not available for refunds
- Original instant payouts cannot be reversed instantly
- Card refunds: 5-10 business days to appear
- Fees: Not refunded automatically
- Reversal has fee = 0
- Original transfer fee NOT returned to merchant
- Platform keeps processing fee from original transfer
- Merchant loses fee even on full refund
- Example: $100 charge, $3 fee → Full refund → Merchant lost $3
- State Flow:
- Created: PENDING
- Processor accepts: SUCCEEDED
- Processor rejects: FAILED (rare)
- Check state or use webhooks
- Parent Transfer: Link to original
parent_transferfield contains original Transfer ID- Use to track refund relationships
- Query via GET /transfers/{original_id}/reversals
- Source/Destination: Reversed
- Original DEBIT: source = customer card
- Reversal: destination = same customer card
- Funds flow reversed
- Same payment instrument
- Multiple Reversals: Allowed
- Can refund same transfer multiple times
- Each reversal is separate Transfer
- Total refunded cannot exceed original amount
- Track via GET /transfers/{id}/reversals
- Failed Reversals: Rare but possible
- Card closed or expired
- Bank rejects refund
- Check messages for details
- May need alternative refund method (check, etc.)
- No Undo: Reversals cannot be reversed
- Cannot "un-refund"
- Permanent action
- Customer would need to make new payment
Use Cases
Customer Return (Full Refund)
# Customer returns product, full refund
curl -X POST \
'https://api.ahrvo.network/payments/na/transfers/TRtransfer789/reversals' \
-u username:password \
-H 'Content-Type: application/json' \
-d '{
"tags": {
"reason": "product_return",
"condition": "unopened",
"rma": "RMA-123456",
"requested_date": "2023-12-11"
}
}'
- Full refund (no refund_amount specified)
- Tag with return details
- Track RMA number
Partial Return
# Customer keeps 3 items, returns 2
# Original order: $100 (5 items @ $20 each)
# Refund: $40 (2 items)
curl -X POST \
'https://api.ahrvo.network/payments/na/transfers/TRtransfer790/reversals' \
-u username:password \
-H 'Content-Type: application/json' \
-d '{
"refund_amount": 4000,
"tags": {
"reason": "partial_return",
"items_returned": "2",
"items_kept": "3",
"return_shipping": "customer_paid"
}
}'
- Partial refund: $40 of $100
- Customer keeps $60 worth of items
- Track item counts
Service Cancellation (Prorated Refund)
# Customer cancels annual subscription after 3 months
# Paid: $120/year = $10/month
# Used: 3 months = $30
# Refund: 9 months = $90
curl -X POST \
'https://api.ahrvo.network/payments/na/transfers/TRtransfer791/reversals' \
-u username:password \
-H 'Content-Type: application/json' \
-d '{
"refund_amount": 9000,
"tags": {
"reason": "subscription_cancellation",
"subscription_id": "SUB-456",
"months_used": "3",
"months_refunded": "9",
"cancellation_date": "2023-12-11"
}
}'
- Prorated refund based on usage
- Track subscription details
- Fair refund calculation
Duplicate Charge Correction
# Accidentally charged customer twice
# Refund second charge
curl -X POST \
'https://api.ahrvo.network/payments/na/transfers/TRtransfer792/reversals' \
-u username:password \
-H 'Content-Type: application/json' \
-d '{
"tags": {
"reason": "duplicate_charge",
"original_transfer": "TRtransfer791",
"error": "system_glitch",
"resolved_by": "support_team"
}
}'
- Full refund of duplicate
- Tag with error details
- Link to correct transfer
Damaged Goods (Partial Refund)
# Product arrived damaged, customer keeps with discount
# Original: $100
# Refund: $30 (30% discount for damage)
curl -X POST \
'https://api.ahrvo.network/payments/na/transfers/TRtransfer793/reversals' \
-u username:password \
-H 'Content-Type: application/json' \
-d '{
"refund_amount": 3000,
"tags": {
"reason": "damaged_product",
"damage_type": "cosmetic",
"customer_keeps_item": "true",
"discount_percent": "30",
"approved_by": "manager_john"
}
}'
- Customer keeps damaged item with discount
- Partial refund as compensation
- Track approval chain
Best Practices
- Verify Original Transfer: Check before refunding
// Fetch original transfer first
const transfer = await fetchTransfer(transferId);
// Verify it succeeded
if (transfer.state !== 'SUCCEEDED') {
throw new Error('Cannot refund: Transfer not successful');
}
// Verify amount
if (refundAmount > transfer.amount) {
throw new Error('Refund amount exceeds original');
}
// Check previous refunds
const reversals = await listReversals(transferId);
const totalRefunded = reversals.reduce((sum, r) => sum + r.amount, 0);
if (totalRefunded + refundAmount > transfer.amount) {
throw new Error('Total refunds would exceed original amount');
}
// Create reversal
await createReversal(transferId, refundAmount); - Tag Thoroughly: Always include reason
reason: Why refundedrequested_by: Who requestedticket_id: Support ticketapproval: Who approved- Useful for accounting and analytics
- Customer Communication: Set expectations
- Explain 5-10 day timeline
- Provide reversal ID for tracking
- Send confirmation email
- Follow up if delayed
- Webhooks: Listen for completion
transfer.succeededwith type=REVERSALtransfer.failedwith type=REVERSAL- Update customer status
- Send completion notification
- Partial Refund Tracking: Calculate totals
- Track all reversals for a transfer
- Sum refunded amounts
- Display remaining refundable amount
- Prevent over-refunding
- Idempotency: Don't retry blindly
- Store reversal ID after creation
- Check for existing reversal first
- Could result in double refund
- Use unique tags to detect duplicates
- Failed Reversals: Handle gracefully
- Rare but possible
- Check state after creation
- If FAILED: Alternative refund method
- Contact customer about delay
- Accounting: Track refunds separately
- Reversals affect revenue
- Track refund rate
- Monitor refund reasons
- Identify product/service issues
Common Workflows
Customer Service Refund Flow
- Customer requests refund
- Agent verifies order and payment
- Check refund policy (timeframe, conditions)
- Approve or deny
- If approved: Create reversal
- Tag with reason and ticket ID
- Send confirmation email to customer
- Follow up in 7 days if not processed
Partial Refund Calculation
// Order: 5 items @ $20 each = $100
// Customer returns 2 items
const originalAmount = 10000; // $100
const itemPrice = 2000; // $20
const itemsReturned = 2;
const refundAmount = itemPrice * itemsReturned; // $40
await createReversal(transferId, {
refund_amount: refundAmount,
tags: {
reason: 'partial_return',
items_returned: itemsReturned.toString(),
item_price: (itemPrice / 100).toString()
}
});
Subscription Proration
// Annual subscription: $120 = $10/month
// Customer cancels after 3 months
// Refund 9 months
const annualPrice = 12000; // $120
const monthlyPrice = annualPrice / 12; // $10
const monthsUsed = 3;
const monthsRemaining = 9;
const refundAmount = monthlyPrice * monthsRemaining; // $90
await createReversal(transferId, {
refund_amount: refundAmount,
tags: {
reason: 'subscription_cancellation',
months_used: monthsUsed.toString(),
months_refunded: monthsRemaining.toString(),
prorated: 'true'
}
});
Track All Refunds
// Get all refunds for a transfer
const reversals = await listReversals(transferId);
const totalRefunded = reversals
.filter(r => r.state === 'SUCCEEDED')
.reduce((sum, r) => sum + r.amount, 0);
const transfer = await fetchTransfer(transferId);
const remainingRefundable = transfer.amount - totalRefunded;
console.log(`Original: $${transfer.amount / 100}`);
console.log(`Refunded: $${totalRefunded / 100}`);
console.log(`Remaining: $${remainingRefundable / 100}`);
Related Endpoints
- POST /transfers: Create original transfer
- GET /transfers: List all transfers (including reversals)
- GET /transfers/{id}: Fetch transfer/reversal details
- PUT /transfers/{id}: Update reversal tags
- GET /transfers/{id}/reversals: List all refunds for a transfer