Skip to main content

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

ParameterTypeRequiredDescription
transfer_idstringYesID of original Transfer to reverse (in URL path)
refund_amountintegerNoAmount to refund in cents (defaults to full amount)
tagsobjectNoCustom 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_amount or set to original amount
      • Returns entire payment
      • Most common case
      • Simpler workflow
    • Partial refund: Set refund_amount to portion
      • In cents
      • Must be ≤ original amount
      • Can create multiple partial refunds
      • Example: $100 charge → $30 refund + $20 refund = $50 total refunded
  • 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
  • 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_transfer field 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 refunded
    • requested_by: Who requested
    • ticket_id: Support ticket
    • approval: 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.succeeded with type=REVERSAL
    • transfer.failed with 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

  1. Customer requests refund
  2. Agent verifies order and payment
  3. Check refund policy (timeframe, conditions)
  4. Approve or deny
  5. If approved: Create reversal
  6. Tag with reason and ticket ID
  7. Send confirmation email to customer
  8. 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}`);
  • 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