Pay
Voids & Refunds

Voids and Refunds

Learn how to cancel payments with voids and return funds with refunds


Voids and refunds allow you to reverse payments that have been processed. Voids cancel payments before they settle, preventing funds from being transferred. Refunds return funds to customers after a payment has settled. Understanding when to use each ensures proper fund management and provides a smooth customer experience.

Prerequisites

Before implementing voids and refunds, it's helpful to learn about the following topics.


Understanding Voids vs Refunds


Voids and refunds serve different purposes in the payment lifecycle and have different eligibility requirements.

Voids

A void cancels a payment before it settles with the bank or card network. When you void a payment:

  • The transaction is canceled before funds are transferred
  • No money moves from the customer
  • The authorization hold on the customer's card is released

Refunds

A refund reverses funds after a transaction has settled. When you refund a transaction:

  • A new transaction is created to reverse the original transaction
  • For payments: Funds are returned to the customer from your processing account
  • For payouts: Funds are reversed from the recipient back to your processing account
  • Takes 3-5 business days for funds to complete the transfer
  • Available after transaction has settled

Voiding Payments

Cancel a payment before it settles by updating its status to voided.

import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Retrieve the payment transaction
payment = pl.Transaction.get('txn_payment123')
 
# Void the payment by updating status to voided
payment.update(status={'value': 'voided'})
 
print(f"Payment {payment.id} has been voided")
print(f"Status: {payment.status}")

This example demonstrates voiding a payment:

  1. Retrieve the payment transaction by ID
  2. Update the transaction status to voided
  3. The authorization hold is released immediately
  4. Customer is not charged

A payment can only be voided while its finalized property is false. Once a payment is finalized (finalized is true), it has settled and must be refunded instead.


Refunding Payments

Return funds to customers by creating a refund transaction.

Refund the entire payment amount

Create a refund transaction for the full amount of the original payment.

import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Retrieve the original payment
payment = pl.Transaction.get('txn_payment123')
 
# Create a refund transaction with transfer linking to original payment
refund = pl.Transaction.create(
    type='refund',
    amount=payment.amount,
    description=f'Refund for order {payment.order_number}',
    transfers=[{
        'assoc_transaction_id': payment.id
    }]
)
 
print(f"Refund created: {refund.id}")
print(f"Amount: ${refund.amount}")
print(f"Status: {refund.status}")

This example demonstrates a full refund:

  1. Retrieve the original payment transaction
  2. Create a new transaction with type='refund'
  3. Set the refund amount to the original payment amount
  4. Link to the original payment using transfers with assoc_transaction_id
  5. Funds are returned to the customer's original payment method

The refund processes through the same payment network as the original payment. Funds typically appear in the customer's account within 3-5 business days.

Refund part of the payment amount

Create a refund transaction for less than the original payment amount.

import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Retrieve the original payment
payment = pl.Transaction.get('txn_payment123')
 
# Create a partial refund for $25 of a $100 payment with transfer linking
refund = pl.Transaction.create(
    type='refund',
    amount=25.00,
    description='Partial refund for one returned item',
    transfers=[{
        'assoc_transaction_id': payment.id
    }]
)
 
print(f"Partial refund created: {refund.id}")
print(f"Refund amount: ${refund.amount}")
print(f"Original payment: ${payment.amount}")
print(f"Status: {refund.status}")

This example demonstrates a partial refund:

  1. Retrieve the original payment transaction
  2. Create a new transaction with type='refund'
  3. Set the refund amount to less than the original payment
  4. Link to the original payment using transfers with assoc_transaction_id
  5. Include a description explaining the partial refund reason
  6. Only the specified amount is returned to the customer

You can process multiple partial refunds against the same payment, as long as the total refunded amount doesn't exceed the original payment amount.


Checking Refund Eligibility

Verify whether a payment can be voided or must be refunded based on its finalization status.

import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Retrieve the payment transaction
payment = pl.Transaction.get('txn_payment123')
 
# Check if payment can be voided or must be refunded using finalized property
if payment.finalized is False:
    print("Payment not finalized, can be voided")
    # Void the payment
    payment.update(status={'value': 'voided'})
    print(f"Payment {payment.id} has been voided")
 
elif payment.finalized is True:
    print("Payment is finalized, must be refunded")
    # Create a refund with transfer linking
    refund = pl.Transaction.create(
        type='refund',
        amount=payment.amount,
        transfers=[{
            'assoc_transaction_id': payment.id
        }]
    )
    print(f"Refund created: {refund.id}")
 
else:
    print(f"Payment finalized status is unknown, cannot void or refund")

This example demonstrates eligibility checks:

  1. Retrieve the payment transaction
  2. Check the finalized property to determine current state
  3. If finalized is false, the payment can be voided
  4. If finalized is true, the payment has settled and must be refunded

Checking eligibility before attempting voids or refunds prevents errors and ensures proper fund handling.


Handling Refund Failures

Properly handle scenarios where refunds cannot be processed.

import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Retrieve the original payment
payment = pl.Transaction.get('txn_payment123')
 
try:
    # Attempt to create a refund with transfer linking
    refund = pl.Transaction.create(
        type='refund',
        amount=150.00,
        transfers=[{
            'assoc_transaction_id': payment.id
        }]
    )
    print(f"Refund successful: {refund.id}")
 
except Exception as error:
    # Handle refund errors
    print(f"Refund failed: {error.error_description}")
    print(f"Error type: {error.error_type}")
    print("Details:", error.details)
 
    # Handle specific error types
    if error.error_type == 'InvalidAttributes':
        print("Action: Verify request parameters")
    else:
        print("Action: Contact support or check error details")

Common refund failure scenarios:

  • Invalid Payment Method: Original payment method is closed or invalid
  • Exceeded Amount: Refund amount exceeds original payment
  • Already Refunded: Payment has already been fully refunded
  • Network Issues: Temporary problems with payment network

Testing Voids and Refunds

Test void and refund functionality in test mode before production deployment.

Test voiding payments before settlement

Use test card numbers to create payments, then immediately void them:

import payload
 
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Create a test payment
payment = pl.Transaction.create(
    type='payment',
    amount=50.00,
    sender={
        'method': {
            'type': 'card',
            'card': {
                'card_number': '4111111111111111',  # Test card
                'expiry': '12/25',
                'card_code': '123',
            },
            'billing_address': {'postal_code': '12345'},
        }
    },
    receiver={'account_id': 'acct_test123'},
)
 
print(f"Test payment created: {payment.id}")
print(f"Status: {payment.status}")
 
# Immediately void the payment
payment.update(status={'value': 'voided'})
 
print(f"\nPayment voided: {payment.id}")
print(f"New status: {payment.status}")
 
# Verify void succeeded
assert payment.status.value == 'voided', "Void failed"
print("\n✓ Void test passed")

Test refunding finalized payments

Create test payments and wait for them to finalize before processing refunds:

import payload
 
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Create and settle a test payment
payment = pl.Transaction.create(
    type='payment',
    amount=75.00,
    sender={
        'method': {
            'type': 'card',
            'card': {
                'card_number': '4111111111111111',
                'expiry': '12/25',
                'card_code': '123',
            },
            'billing_address': {'postal_code': '12345'},
        }
    },
    receiver={'account_id': 'acct_test123'},
)
 
print(f"Test payment created: {payment.id}")
print(f"Amount: ${payment.amount}")
 
# Create a refund with transfer linking
refund = pl.Transaction.create(
    type='refund', amount=payment.amount, transfers=[{'assoc_transaction_id': payment.id}]
)
 
print(f"\nRefund created: {refund.id}")
print(f"Refund amount: ${refund.amount}")
print(f"Status: {refund.status}")
 
# Verify refund
assert refund.type == 'refund', "Type should be refund"
assert refund.amount == payment.amount, "Amount should match payment"
print("\n✓ Refund test passed")

Monitoring Voids and Refunds

Track void and refund operations through webhooks and transaction queries.

Webhook Events

Monitor these webhook events for void and refund operations:

  • void - Payment was successfully voided
  • refund - Refund transaction was created
  • processed - Refund completed and funds transferred
  • decline - Payment declined before processing

For detailed webhook setup and implementation, see Transaction Webhooks.

Querying Transactions

List voided and refunded transactions:

import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Query all refund transactions
refunds = pl.Transaction.filter_by(
    type='refund'
).all()
print(f"Found {len(refunds)} refund transactions\n")
 
# Display refund summary
total_refunded = 0
for refund in refunds:
    print(f"Refund: {refund.id}")
    print(f"  Amount: ${refund.amount}")
    print(f"  Status: {refund.status}")
    print(f"  Date: {refund.created_at}")
    print(f"  Description: {refund.description}")
    print()
    total_refunded += refund.amount
 
print(f"Total refunded: ${total_refunded}")
 
# Query voided transactions
voided = pl.Transaction.filter_by(
    status__value='voided'
).all()
print(f"\nFound {len(voided)} voided transactions")

Use queries to:

  • Generate refund reports for accounting
  • Monitor refund rates by product or service
  • Identify high-refund accounts or customers
  • Track refund processing times
  • Analyze refund reasons

Schema Reference


The following fields are relevant for void and refund operations:

Transaction Fields

type
enum[string]Immutable
The type of transaction being processed. This determines the direction and nature of funds movement, such as payment collection, refund issuance, credit disbursement, or account funding operations.
Values: payment, deposit, withdraw, refund, payout
amount
number (double)
The monetary amount for this transaction in the currency of the processing account. This value is always positive and represents the total value being transferred, collected, or refunded. The amount is rounded to two decimal places for display.
description
string
A human-readable description of what this transaction is for. This text provides context about the purchase, service, or payment purpose and may be displayed to customers on receipts and in transaction histories.
Max length: 128
status
object
The status information for this transaction, including the current state, a detailed status code, and a human-readable message explaining the result.
code
enum[string]
A machine-readable code detailing the specific reason or result for the transaction status. This provides granular information about success, failure reasons, or other status conditions.
Values: approved, card_expired, duplicate_attempt, exceeded_limit, general_decline, insufficient_bal, invalid_card_code, invalid_card_number, invalid_zip, invalid_address, invalid_account_number, suspicious_activity, too_many_attempts, processing_issue, issue_reading_card, not_supported, general_reject, general_adjustment, payment_stopped
Read-only permissions: "Undocumented"
message
stringRead-only
A human-readable message describing the transaction status and code. This text is suitable for display to users and provides context about the transaction outcome.
value
enum[string]
The current processing status of the transaction, indicating its lifecycle state such as processing, processed, authorized, declined, voided, or rejected.
Values: processing, authorized, processed, declined, rejected, voided, adjusted
notes
string
Internal notes or comments about this transaction. This field is for record-keeping purposes and can contain any additional information that may be useful for reference, customer service, or accounting purposes.
Max length: 2048

For complete transaction field documentation, see the Transaction API Reference.


Next Steps

Enhance your payment operations with voids and refunds


Handle Payment Lifecycle

Process payments with the Payment API, implement Two-Step Processing to authorize and capture payments separately, and receive real-time updates via Webhook Events for void and refund operations.

Manage Failed Payments

Handle Payment Declines for real-time payment failures, manage Bank Rejects for asynchronous ACH rejections, and resolve Disputes to handle customer disputes and chargebacks.

Track and Reconcile Payments

Monitor payment metrics with Reporting Overview, query and analyze transaction data using Build Report Queries, and match bank statements with Bank Reconciliation.


Related articles