Learn how to programmatically issue full or partial refunds with the Stripe API, including setup, code examples, error handling, webhooks, and best practices.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
How to Programmatically Issue Refunds with Stripe API
Step 1: Set Up Your Stripe Account and Get API Keys
Before you can start issuing refunds programmatically, you need to set up your Stripe account and obtain your API keys:
Step 2: Install the Stripe Library
Install the Stripe library for your programming language. Below are examples for popular languages:
For Node.js:
npm install stripe
For Python:
pip install stripe
For PHP:
composer require stripe/stripe-php
For Ruby:
gem install stripe
Step 3: Initialize the Stripe Client
Once you've installed the library, you need to initialize the Stripe client with your secret key.
Node.js:
const stripe = require('stripe')('sk_test_your_secret_key');
// Or with ES modules
// import Stripe from 'stripe';
// const stripe = new Stripe('sk_test_your_secret_key');
Python:
import stripe
stripe.api_key = "sk_test_your_secret\_key"
PHP:
\Stripe\Stripe::setApiKey('sk_test_your_secret_key');
Ruby:
require 'stripe'
Stripe.api_key = 'sk_test_your_secret\_key'
Step 4: Issue a Full Refund
To issue a full refund, you need the payment intent or charge ID that you want to refund.
Node.js:
async function issueFullRefund(paymentIntentId) {
try {
const refund = await stripe.refunds.create({
payment\_intent: paymentIntentId,
});
console.log('Refund processed successfully:', refund.id);
return refund;
} catch (error) {
console.error('Error processing refund:', error);
throw error;
}
}
Python:
def issue_full_refund(payment_intent_id):
try:
refund = stripe.Refund.create(
payment_intent=payment_intent\_id
)
print(f"Refund processed successfully: {refund.id}")
return refund
except Exception as e:
print(f"Error processing refund: {str(e)}")
raise
PHP:
function issueFullRefund($paymentIntentId) {
try {
$refund = \Stripe\Refund::create([
'payment\_intent' => $paymentIntentId,
]);
echo "Refund processed successfully: " . $refund->id;
return $refund;
} catch (\Exception $e) {
echo "Error processing refund: " . $e->getMessage();
throw $e;
}
}
Ruby:
def issue_full_refund(payment_intent_id)
begin
refund = Stripe::Refund.create({
payment_intent: payment_intent\_id
})
puts "Refund processed successfully: #{refund.id}"
return refund
rescue Stripe::StripeError => e
puts "Error processing refund: #{e.message}"
raise e
end
end
Step 5: Issue a Partial Refund
For partial refunds, you need to specify the amount to refund in addition to the payment intent or charge ID.
Node.js:
async function issuePartialRefund(paymentIntentId, amountToRefund) {
try {
// Amount should be in cents (e.g., $10.00 = 1000)
const refund = await stripe.refunds.create({
payment\_intent: paymentIntentId,
amount: amountToRefund,
});
console.log('Partial refund processed successfully:', refund.id);
return refund;
} catch (error) {
console.error('Error processing partial refund:', error);
throw error;
}
}
Python:
def issue_partial_refund(payment_intent_id, amount_to_refund):
try:
# Amount should be in cents (e.g., $10.00 = 1000)
refund = stripe.Refund.create(
payment_intent=payment_intent\_id,
amount=amount_to_refund
)
print(f"Partial refund processed successfully: {refund.id}")
return refund
except Exception as e:
print(f"Error processing partial refund: {str(e)}")
raise
PHP:
function issuePartialRefund($paymentIntentId, $amountToRefund) {
try {
// Amount should be in cents (e.g., $10.00 = 1000)
$refund = \Stripe\Refund::create([
'payment\_intent' => $paymentIntentId,
'amount' => $amountToRefund,
]);
echo "Partial refund processed successfully: " . $refund->id;
return $refund;
} catch (\Exception $e) {
echo "Error processing partial refund: " . $e->getMessage();
throw $e;
}
}
Ruby:
def issue_partial_refund(payment_intent_id, amount_to_refund)
begin
# Amount should be in cents (e.g., $10.00 = 1000)
refund = Stripe::Refund.create({
payment_intent: payment_intent\_id,
amount: amount_to_refund
})
puts "Partial refund processed successfully: #{refund.id}"
return refund
rescue Stripe::StripeError => e
puts "Error processing partial refund: #{e.message}"
raise e
end
end
Step 6: Add a Reason for the Refund
Adding a reason for the refund can be helpful for bookkeeping and auditing purposes. Stripe allows you to specify one of the following reasons: 'duplicate', 'fraudulent', or 'requested_by_customer'.
Node.js:
async function issueRefundWithReason(paymentIntentId, reason) {
try {
const refund = await stripe.refunds.create({
payment\_intent: paymentIntentId,
reason: reason, // 'duplicate', 'fraudulent', or 'requested_by_customer'
});
console.log('Refund processed with reason:', refund.id);
return refund;
} catch (error) {
console.error('Error processing refund:', error);
throw error;
}
}
Python:
def issue_refund_with_reason(payment_intent\_id, reason):
try:
refund = stripe.Refund.create(
payment_intent=payment_intent\_id,
reason=reason # 'duplicate', 'fraudulent', or 'requested_by_customer'
)
print(f"Refund processed with reason: {refund.id}")
return refund
except Exception as e:
print(f"Error processing refund: {str(e)}")
raise
PHP:
function issueRefundWithReason($paymentIntentId, $reason) {
try {
$refund = \Stripe\Refund::create([
'payment\_intent' => $paymentIntentId,
'reason' => $reason, // 'duplicate', 'fraudulent', or 'requested_by_customer'
]);
echo "Refund processed with reason: " . $refund->id;
return $refund;
} catch (\Exception $e) {
echo "Error processing refund: " . $e->getMessage();
throw $e;
}
}
Ruby:
def issue_refund_with_reason(payment_intent\_id, reason)
begin
refund = Stripe::Refund.create({
payment_intent: payment_intent\_id,
reason: reason # 'duplicate', 'fraudulent', or 'requested_by_customer'
})
puts "Refund processed with reason: #{refund.id}"
return refund
rescue Stripe::StripeError => e
puts "Error processing refund: #{e.message}"
raise e
end
end
Step 7: Refund Specific Payment Intent or Charge
You can refund either using a payment intent ID or a charge ID. Here's how to refund using a charge ID:
Node.js:
async function refundByChargeId(chargeId) {
try {
const refund = await stripe.refunds.create({
charge: chargeId,
});
console.log('Refund processed using charge ID:', refund.id);
return refund;
} catch (error) {
console.error('Error processing refund:', error);
throw error;
}
}
Python:
def refund_by_charge_id(charge_id):
try:
refund = stripe.Refund.create(
charge=charge\_id
)
print(f"Refund processed using charge ID: {refund.id}")
return refund
except Exception as e:
print(f"Error processing refund: {str(e)}")
raise
PHP:
function refundByChargeId($chargeId) {
try {
$refund = \Stripe\Refund::create([
'charge' => $chargeId,
]);
echo "Refund processed using charge ID: " . $refund->id;
return $refund;
} catch (\Exception $e) {
echo "Error processing refund: " . $e->getMessage();
throw $e;
}
}
Ruby:
def refund_by_charge_id(charge_id)
begin
refund = Stripe::Refund.create({
charge: charge\_id
})
puts "Refund processed using charge ID: #{refund.id}"
return refund
rescue Stripe::StripeError => e
puts "Error processing refund: #{e.message}"
raise e
end
end
Step 8: Add Metadata to a Refund
Metadata can be used to store additional information about the refund for your records.
Node.js:
async function refundWithMetadata(paymentIntentId, metadata) {
try {
const refund = await stripe.refunds.create({
payment\_intent: paymentIntentId,
metadata: metadata, // e.g., { order_id: '12345', customer_email: '[email protected]' }
});
console.log('Refund processed with metadata:', refund.id);
return refund;
} catch (error) {
console.error('Error processing refund:', error);
throw error;
}
}
Python:
def refund_with_metadata(payment_intent_id, metadata):
try:
refund = stripe.Refund.create(
payment_intent=payment_intent\_id,
metadata=metadata # e.g., { 'order_id': '12345', 'customer_email': '[email protected]' }
)
print(f"Refund processed with metadata: {refund.id}")
return refund
except Exception as e:
print(f"Error processing refund: {str(e)}")
raise
PHP:
function refundWithMetadata($paymentIntentId, $metadata) {
try {
$refund = \Stripe\Refund::create([
'payment\_intent' => $paymentIntentId,
'metadata' => $metadata, // e.g., ['order_id' => '12345', 'customer_email' => '[email protected]']
]);
echo "Refund processed with metadata: " . $refund->id;
return $refund;
} catch (\Exception $e) {
echo "Error processing refund: " . $e->getMessage();
throw $e;
}
}
Ruby:
def refund_with_metadata(payment_intent_id, metadata)
begin
refund = Stripe::Refund.create({
payment_intent: payment_intent\_id,
metadata: metadata # e.g., { order_id: '12345', customer_email: '[email protected]' }
})
puts "Refund processed with metadata: #{refund.id}"
return refund
rescue Stripe::StripeError => e
puts "Error processing refund: #{e.message}"
raise e
end
end
Step 9: Retrieve a Refund
After processing a refund, you might want to retrieve it to check its status or get additional information.
Node.js:
async function retrieveRefund(refundId) {
try {
const refund = await stripe.refunds.retrieve(refundId);
console.log('Refund retrieved:', refund);
return refund;
} catch (error) {
console.error('Error retrieving refund:', error);
throw error;
}
}
Python:
def retrieve_refund(refund_id):
try:
refund = stripe.Refund.retrieve(refund\_id)
print(f"Refund retrieved: {refund}")
return refund
except Exception as e:
print(f"Error retrieving refund: {str(e)}")
raise
PHP:
function retrieveRefund($refundId) {
try {
$refund = \Stripe\Refund::retrieve($refundId);
echo "Refund retrieved: " . json\_encode($refund);
return $refund;
} catch (\Exception $e) {
echo "Error retrieving refund: " . $e->getMessage();
throw $e;
}
}
Ruby:
def retrieve_refund(refund_id)
begin
refund = Stripe::Refund.retrieve(refund\_id)
puts "Refund retrieved: #{refund}"
return refund
rescue Stripe::StripeError => e
puts "Error retrieving refund: #{e.message}"
raise e
end
end
Step 10: List All Refunds
You might also want to list all refunds or filter them based on certain criteria.
Node.js:
async function listRefunds(limit = 10) {
try {
const refunds = await stripe.refunds.list({
limit: limit,
});
console.log('Refunds listed:', refunds.data.length);
return refunds;
} catch (error) {
console.error('Error listing refunds:', error);
throw error;
}
}
Python:
def list\_refunds(limit=10):
try:
refunds = stripe.Refund.list(limit=limit)
print(f"Refunds listed: {len(refunds.data)}")
return refunds
except Exception as e:
print(f"Error listing refunds: {str(e)}")
raise
PHP:
function listRefunds($limit = 10) {
try {
$refunds = \Stripe\Refund::all(['limit' => $limit]);
echo "Refunds listed: " . count($refunds->data);
return $refunds;
} catch (\Exception $e) {
echo "Error listing refunds: " . $e->getMessage();
throw $e;
}
}
Ruby:
def list\_refunds(limit=10)
begin
refunds = Stripe::Refund.list(limit: limit)
puts "Refunds listed: #{refunds.data.length}"
return refunds
rescue Stripe::StripeError => e
puts "Error listing refunds: #{e.message}"
raise e
end
end
Step 11: Update a Refund
You can update some information associated with a refund, such as metadata.
Node.js:
async function updateRefundMetadata(refundId, metadata) {
try {
const refund = await stripe.refunds.update(refundId, {
metadata: metadata,
});
console.log('Refund metadata updated:', refund.id);
return refund;
} catch (error) {
console.error('Error updating refund metadata:', error);
throw error;
}
}
Python:
def update_refund_metadata(refund\_id, metadata):
try:
refund = stripe.Refund.modify(
refund\_id,
metadata=metadata
)
print(f"Refund metadata updated: {refund.id}")
return refund
except Exception as e:
print(f"Error updating refund metadata: {str(e)}")
raise
PHP:
function updateRefundMetadata($refundId, $metadata) {
try {
$refund = \Stripe\Refund::update($refundId, [
'metadata' => $metadata,
]);
echo "Refund metadata updated: " . $refund->id;
return $refund;
} catch (\Exception $e) {
echo "Error updating refund metadata: " . $e->getMessage();
throw $e;
}
}
Ruby:
def update_refund_metadata(refund\_id, metadata)
begin
refund = Stripe::Refund.update(
refund\_id,
{ metadata: metadata }
)
puts "Refund metadata updated: #{refund.id}"
return refund
rescue Stripe::StripeError => e
puts "Error updating refund metadata: #{e.message}"
raise e
end
end
Step 12: Handle Webhooks for Refund Events
To keep your system in sync with Stripe, you should set up webhook handling for refund events.
Node.js with Express:
const express = require('express');
const stripe = require('stripe')('sk_test_your_secret_key');
const bodyParser = require('body-parser');
const app = express();
// Use JSON parser for webhook endpoint
app.post('/webhook', bodyParser.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec_your_webhook_signing_secret'
);
} catch (err) {
console.error(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'charge.refunded':
const chargeRefunded = event.data.object;
console.log('Charge refunded:', chargeRefunded.id);
// Update your database with refund information
break;
case 'refund.created':
const refundCreated = event.data.object;
console.log('Refund created:', refundCreated.id);
// Process the refund creation
break;
case 'refund.updated':
const refundUpdated = event.data.object;
console.log('Refund updated:', refundUpdated.id);
// Process the refund update
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
// Return a response to acknowledge receipt of the event
res.json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
Python with Flask:
import stripe
from flask import Flask, request, jsonify
app = Flask(**name**)
stripe.api_key = "sk_test_your_secret\_key"
webhook_secret = "whsec_your_webhook_signing\_secret"
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.data
sig\_header = request.headers.get('Stripe-Signature')
try:
event = stripe.Webhook.construct\_event(
payload, sig_header, webhook_secret
)
except ValueError as e:
# Invalid payload
return jsonify(error=str(e)), 400
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return jsonify(error=str(e)), 400
# Handle the event
if event['type'] == 'charge.refunded':
charge\_refunded = event\['data']\['object']
print(f"Charge refunded: {charge\_refunded['id']}")
# Update your database with refund information
elif event['type'] == 'refund.created':
refund\_created = event\['data']\['object']
print(f"Refund created: {refund\_created['id']}")
# Process the refund creation
elif event['type'] == 'refund.updated':
refund\_updated = event\['data']\['object']
print(f"Refund updated: {refund\_updated['id']}")
# Process the refund update
else:
print(f"Unhandled event type {event['type']}")
return jsonify(success=True), 200
if **name** == '**main**':
app.run(port=3000)
Step 13: Error Handling and Best Practices
Proper error handling is essential when working with the Stripe API. Here's a more comprehensive error handling approach:
Node.js:
async function safeRefund(paymentIntentId, amount = null) {
try {
// Verify the payment intent exists and is in a refundable state
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
if (paymentIntent.status !== 'succeeded') {
throw new Error(`Payment intent ${paymentIntentId} is not in a refundable state. Status: ${paymentIntent.status}`);
}
// Check if it's already refunded
if (paymentIntent.charges.data[0].refunded) {
console.log(`Payment intent ${paymentIntentId} is already refunded.`);
return { already\_refunded: true };
}
// Create refund parameters
const refundParams = {
payment\_intent: paymentIntentId,
};
// Add amount if it's a partial refund
if (amount) {
refundParams.amount = amount;
}
// Process refund
const refund = await stripe.refunds.create(refundParams);
console.log(`Refund processed successfully: ${refund.id}`);
return refund;
} catch (error) {
if (error.type === 'StripeInvalidRequestError') {
console.error(`Invalid request error: ${error.message}`);
} else if (error.type === 'StripeAPIError') {
console.error(`API error: ${error.message}`);
} else if (error.type === 'StripeAuthenticationError') {
console.error(`Authentication error: ${error.message}`);
} else {
console.error(`Unexpected error: ${error.message}`);
}
throw error;
}
}
Python:
def safe_refund(payment_intent\_id, amount=None):
try:
# Verify the payment intent exists and is in a refundable state
payment_intent = stripe.PaymentIntent.retrieve(payment_intent\_id)
if payment\_intent.status != 'succeeded':
raise ValueError(f"Payment intent {payment_intent_id} is not in a refundable state. Status: {payment\_intent.status}")
# Check if it's already refunded
if payment\_intent.charges.data[0].refunded:
print(f"Payment intent {payment_intent_id} is already refunded.")
return {"already\_refunded": True}
# Create refund parameters
refund\_params = {
"payment_intent": payment_intent\_id
}
# Add amount if it's a partial refund
if amount:
refund\_params["amount"] = amount
# Process refund
refund = stripe.Refund.create(\*\*refund\_params)
print(f"Refund processed successfully: {refund.id}")
return refund
except stripe.error.InvalidRequestError as e:
print(f"Invalid request error: {str(e)}")
raise
except stripe.error.APIError as e:
print(f"API error: {str(e)}")
raise
except stripe.error.AuthenticationError as e:
print(f"Authentication error: {str(e)}")
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
Step 14: Implementing Idempotency
Idempotency ensures that you don't accidentally process the same refund twice if you retry a failed request.
Node.js:
async function idempotentRefund(paymentIntentId, idempotencyKey) {
try {
const refund = await stripe.refunds.create(
{
payment\_intent: paymentIntentId,
},
{
idempotencyKey: idempotencyKey, // Use a unique key for each refund request
}
);
console.log('Idempotent refund processed:', refund.id);
return refund;
} catch (error) {
console.error('Error processing refund:', error);
throw error;
}
}
Python:
def idempotent_refund(payment_intent_id, idempotency_key):
try:
refund = stripe.Refund.create(
payment_intent=payment_intent\_id,
idempotency_key=idempotency_key # Use a unique key for each refund request
)
print(f"Idempotent refund processed: {refund.id}")
return refund
except Exception as e:
print(f"Error processing refund: {str(e)}")
raise
PHP:
function idempotentRefund($paymentIntentId, $idempotencyKey) {
try {
$refund = \Stripe\Refund::create(
[
'payment\_intent' => $paymentIntentId,
],
[
'idempotency\_key' => $idempotencyKey, // Use a unique key for each refund request
]
);
echo "Idempotent refund processed: " . $refund->id;
return $refund;
} catch (\Exception $e) {
echo "Error processing refund: " . $e->getMessage();
throw $e;
}
}
Ruby:
def idempotent_refund(payment_intent_id, idempotency_key)
begin
refund = Stripe::Refund.create(
{ payment_intent: payment_intent\_id },
{ idempotency_key: idempotency_key } # Use a unique key for each refund request
)
puts "Idempotent refund processed: #{refund.id}"
return refund
rescue Stripe::StripeError => e
puts "Error processing refund: #{e.message}"
raise e
end
end
Step 15: Integration with Your Backend System
Here's an example of how to integrate refunds with a REST API in a web application:
Node.js with Express:
const express = require('express');
const stripe = require('stripe')('sk_test_your_secret_key');
const bodyParser = require('body-parser');
const { v4: uuidv4 } = require('uuid');
const app = express();
app.use(bodyParser.json());
// Endpoint to process a refund
app.post('/api/refunds', async (req, res) => {
const { payment_intent_id, amount, reason, metadata } = req.body;
if (!payment_intent_id) {
return res.status(400).json({ error: 'payment_intent_id is required' });
}
try {
// Generate a unique idempotency key
const idempotencyKey = uuidv4();
// Prepare refund parameters
const refundParams = {
payment_intent: payment_intent\_id,
};
if (amount) refundParams.amount = amount;
if (reason) refundParams.reason = reason;
if (metadata) refundParams.metadata = metadata;
// Process the refund with idempotency key
const refund = await stripe.refunds.create(
refundParams,
{ idempotencyKey }
);
// Record the refund in your database
// await saveRefundToDatabase(refund);
return res.status(200).json({
success: true,
refund\_id: refund.id,
status: refund.status,
amount: refund.amount,
});
} catch (error) {
console.error('Refund error:', error);
return res.status(error.statusCode || 500).json({
success: false,
error: error.message,
});
}
});
// Endpoint to get refund details
app.get('/api/refunds/:refund\_id', async (req, res) => {
const { refund\_id } = req.params;
try {
const refund = await stripe.refunds.retrieve(refund\_id);
return res.status(200).json({
success: true,
refund,
});
} catch (error) {
console.error('Error retrieving refund:', error);
return res.status(error.statusCode || 500).json({
success: false,
error: error.message,
});
}
});
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Python with Flask:
import uuid
from flask import Flask, request, jsonify
import stripe
app = Flask(**name**)
stripe.api_key = "sk_test_your_secret\_key"
Endpoint to process a refund
@app.route('/api/refunds', methods=['POST'])
def create\_refund():
data = request.get\_json()
payment_intent_id = data.get('payment_intent_id')
if not payment_intent_id:
return jsonify({"error": "payment_intent_id is required"}), 400
amount = data.get('amount')
reason = data.get('reason')
metadata = data.get('metadata')
try:
# Generate a unique idempotency key
idempotency\_key = str(uuid.uuid4())
# Prepare refund parameters
refund\_params = {
"payment_intent": payment_intent\_id
}
if amount:
refund\_params["amount"] = amount
if reason:
refund\_params["reason"] = reason
if metadata:
refund\_params["metadata"] = metadata
# Process the refund with idempotency key
refund = stripe.Refund.create(
\*\*refund\_params,
idempotency_key=idempotency_key
)
# Record the refund in your database
# save_refund_to\_database(refund)
return jsonify({
"success": True,
"refund\_id": refund.id,
"status": refund.status,
"amount": refund.amount
}), 200
except stripe.error.StripeError as e:
return jsonify({
"success": False,
"error": str(e)
}), e.http\_status or 500
except Exception as e:
return jsonify({
"success": False,
"error": str(e)
}), 500
Endpoint to get refund details
@app.route('/api/refunds/', methods=['GET'])
def get_refund(refund_id):
try:
refund = stripe.Refund.retrieve(refund\_id)
return jsonify({
"success": True,
"refund": refund
}), 200
except stripe.error.StripeError as e:
return jsonify({
"success": False,
"error": str(e)
}), e.http\_status or 500
except Exception as e:
return jsonify({
"success": False,
"error": str(e)
}), 500
if **name** == '**main**':
app.run(port=3000, debug=True)
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.