14

From the Stripe docs:

When you cancel the subscription, the customer's card will not be charged again, but no money will be refunded either. If you'd like to issue a refund, you can do so through Stripe's dashboard or via the API.

I created a monthly subscription, and I only want to refund the amount of money for the number of days that have not yet passed within the month of the subscription. How can I refund only the amount of money from the subscription for the days that are not complete yet with the Stripe API?

Hope4You
  • 1,927
  • 4
  • 21
  • 45
  • 4
    It's mid-2016 and it's astonishing Stripe still hasn't implemented the entire subscription life-cycle (specifically end-of-life) – DeepSpace101 May 12 '16 at 03:35

4 Answers4

11

Stripe will handle this now. It can immediately cancel the subscription and create an invoice refunding them the cost of the unused part of the month.

https://stripe.com/docs/api/subscriptions/cancel

Add the "prorate" tag to refund the remaining monthly cost.

In PHP:

$subscription = \Stripe\Subscription::retrieve(
  'SUBSCRIPTION_ID'
);

$subscription->delete([
    'prorate' => true
]);

EDIT:

@Dobes Vandermeer commented below and pointed out my mistake. The above method will not actually refund the customer the prorated amount but only create an account credit which will be applied to the next invoice.

The code below will actually refund the prorated amount to the card. It generates a sample prorated invoice as if the customer switched their subscription quantity to zero. It then refunds that prorated amount from the last invoice on their active subscription.

The "subscription_proration_date" tag is optional but useful for if you need to prorate the subscription from a certain point in time.

// get active subscription
$subscription = \Stripe\Subscription::retrieve(' SUBSCRIPTION_ID ');

// sample prorated invoice for a subscription with quantity of 0
$sample_subscription_item = array(
    "id"       => $subscription->items->data[0]->id,
    "plan"     => $subscription->items->data[0]->plan->id,
    "quantity" => 0,
);

$upcoming_prorated_invoice = \Stripe\Invoice::upcoming([
    "customer"                    => $subscription->customer,
    "subscription"                => $subscription->id,
    "subscription_items"          => array($sample_subscription_item),
    "subscription_proration_date" => ' PRORATED_DATE ', // optional
]);

// find prorated amount
$prorated_amount = 0;
foreach($upcoming_prorated_invoice->lines->data as $invoice) {
    if ($invoice->type == "invoiceitem") {

        $prorated_amount = ($invoice->amount < 0) ? abs($invoice->amount) : 0;

        break;

    }
}

// find charge id on the active subscription's last invoice
$latest_invoice = \Stripe\Invoice::retrieve($subscription->latest_invoice);
$latest_charge_id = $latest_invoice->charge;

// refund amount from last invoice charge
if ($prorated_amount > 0) {

    $refund = \Stripe\Refund::create([
        'charge' => $latest_charge_id,
        'amount' => $prorated_amount,
    ]);
  
}

// delete subscription
$subscription->delete();
Philip Stevens
  • 184
  • 1
  • 9
  • It seems that when you do this Stripe only creates a refund and issues a "credit". It does not actually issue the refund. – Dobes Vandermeer Dec 09 '19 at 23:32
  • Thanks @Dobes Vandermeer. Good point. I updated the answer with a flow that works for my use case. – Philip Stevens Jan 08 '20 at 23:13
  • I got this error Stripe\Exception\InvalidRequestException You cannot preview the upcoming invoice for a canceled subscription. – Shankar S Bavan Jun 16 '20 at 02:23
  • 1
    @ShankarSBavan It sounds like the subscription was already cancelled. In my post I was assuming that the subscription was still active. It cancels the subscription after all calculations have been made and the charge has been refunded to the customer. – Philip Stevens Jun 17 '20 at 16:17
  • @PhilipStevens yes I want to give refund, who cancelled their subscription in middle of the month. could you please verify is this correct answer. https://stackoverflow.com/questions/62403075/stripe-api-get-upcoming-invoice-for-cancelled-subscription?noredirect=1#comment110403008_62403075 – Shankar S Bavan Jun 18 '20 at 02:39
  • To avoid the `Invoice::retrieve` call, you can send `expand` option with `latest_invoice` when calling `Subscription::retrieve`. Then `$subscription->latest_invoice->charge`. – Emre Mar 23 '22 at 00:12
  • 1
    This worked for me. You have to obviously replace SUBSCRIPTION ID with an actual subscription id, and the PRORATED DATE needs to be a Unix Timestamp date. Also note that `$subscription->cancel();` appears to work exactly the same as `$subscription->delete();` – jsherk Apr 02 '22 at 20:11
  • I understand this answer represents a certain use case, where the assumption is made that the charge doesn't have any amount already refunded. In the event the charge already has some amount already refunded, the process will need to be modified a bit depending how one wants to account the already refunded amount. The charge of the latest invoice will need to be retrieved to see if the amount refunded is positive. From there, further processing is needed. There is also the limitation where the refund can't exceed the amount remaining. – Viet Sep 25 '22 at 06:35
5

You will need to calculate refund amount, and then make a refund API call to Stripe. After refund you will have to make another API call for Subscription cancellation

Chirag B
  • 2,106
  • 2
  • 20
  • 35
  • Is it possible to make Cancelation call and after that make refund call ? Or the api calls should be followed strictly as you suggested? – So_oP Oct 08 '21 at 17:20
4

After researching for a while, I came to this flow written in JavaScript for Node.js:

refundAndUnsubscribe = async function () {
    try {

        // Set proration date to this moment:
        const proration_date = Math.floor(Date.now() / 1000);

        let sub = await stripe.subscriptions.retrieve("sub_CILnalN9HpvADj");

        // See what the next invoice would look like with a plan switch
        // and proration set:
        let items = [{
            quantity: 0,
            id: sub.items.data[0].id,
            plan: "your_plan" // Switch to new plan
        }];


        let invoices = await stripe.invoices.retrieveUpcoming('cus_CIP9dtlq143gq7', 'sub_CILnalN9HpvADj', {
            subscription_items: items,
            subscription_proration_date: proration_date
        });

        //List all invoices
        let payedInvoicesList = await stripe.invoices.list({
            customer: 'cus_CIP9dtlq143gq7'
        });

        // Calculate the proration cost:
        let current_prorations = [];
        let cost = 0;
        for (var i = 0; i < invoices.lines.data.length; i++) {
            let invoice_item = invoices.lines.data[i];
            if (invoice_item.period.start == proration_date) {
                current_prorations.push(invoice_item);
                cost += invoice_item.amount;
            }
        }

        //create a refund
        if (cost !== 0) {
            cost = (cost < 0) ? cost * -1 : cost //A positive integer in cents

            let refund = await stripe.refunds.create({
                charge: payedInvoicesList.data[0].charge,
                amount: cost
            });
        }

        // delete subscription
        return stripe.subscriptions.del('sub_CILnalN9HpvADj');

    } catch (e) {
        console.log(e);
    }
}
TOPKAT
  • 6,667
  • 2
  • 44
  • 72
3

If you want to let Stripe handle the refund calculations you can change the subscription qty to 0 then cancel the plan after.

Elliot
  • 1,893
  • 4
  • 17
  • 35
  • 1
    To elaborate on this trick, you need to set the quantity to zero and then retrieve the upcoming invoice to get the refunded amount (upcomingInvoice.total * -1). To issue a refund, grab the latest paid invoice (not the upcoming) and use the charge id to issue the refund. – claviska Dec 12 '17 at 17:18