Laravel PayMongo

Refunds

A Refund returns all or part of a paid payment to the customer's original payment method. Only a payment whose status is paid can be refunded. You can refund one payment several times, as long as the refunds together do not exceed what was paid.

Every method lives on Paymongo::refunds(), and a single refund comes back as a Refund. For every attribute PayMongo accepts, see its Refund reference and Refunds guide.

Refund a payment

create() takes the payment to refund, the amount, and why:

use Luigel\Paymongo\Enums\RefundReason;
use Luigel\Paymongo\Facades\Paymongo;

$refund = Paymongo::refunds()->create([
    'payment_id' => 'pay_i7tdqnmwdszWo5B4Xqk2ogX5',
    'amount' => 50050, // PHP 500.50 of a PHP 1,500.50 payment, in centavos
    'reason' => RefundReason::Others, // or Duplicate, Fraudulent
    'notes' => 'Order 1234: one item out of stock', // up to 255 characters
    'metadata' => ['order_id' => '1234'],
], idempotencyKey: 'order-1234-refund-1');

$refund->status; // ?RefundStatus: usually Pending at first
  • amount is integer centavos, at least 100 (PHP 1.00). The full payment amount refunds it in full, and anything less refunds part of it.
  • reason is required: a Luigel\Paymongo\Enums\RefundReason case (Duplicate, Fraudulent, or Others) or its string value. PayMongo's reference also lists requested_by_customer, which you can pass as a string. The enum does not have it yet, so $refund->reason reads null for it. Read the raw value with $refund->attribute('reason').
  • notes (up to 255 characters) and metadata are for you.
  • idempotencyKey: makes a retried request return the same refund instead of refunding twice. Use a key unique to this refund of this order.

How long a refund may be made after the payment, whether partial refunds are allowed, and how soon the customer sees the money all depend on the payment method. Card refunds are allowed for 60 days, and UnionBank online banking payments cannot be refunded at all. See PayMongo's Refunds guide for the table.

The money comes out of your upcoming payout. If that balance cannot cover the refund, the refund does not go through until it can. A refund of a payment already paid out to you is deducted from your next payout.

Retrieve a refund

retrieve() returns a refund with its status:

use Luigel\Paymongo\Enums\RefundStatus;
use Luigel\Paymongo\Facades\Paymongo;

$refund = Paymongo::refunds()->retrieve('ref_vPSqdAPD2pmtKj6Ac5SRfXjs');

$refund->status === RefundStatus::Succeeded;
$refund->reason;             // ?RefundReason
$refund->paymentId;          // "pay_..."
$refund->money()?->format(); // "₱500.50"
$refund->refundedAt();       // ?CarbonImmutable
$refund->attribute('payout_id'); // the payout it was deducted from, or null

$refund->status is a Luigel\Paymongo\Enums\RefundStatus:

Case Value Meaning
Pending pending Being processed. It rarely stays here for more than a few minutes.
Succeeded succeeded Refunded.
Failed failed Did not go through. Try the refund again.

PayMongo also documents a processing status, which reads as null here. Like pending, it rarely lasts. Contact PayMongo support about a refund that stays in either.

List refunds

list() returns a page of refunds. Pass payment_id for the refunds of one payment:

use Luigel\Paymongo\Facades\Paymongo;

$refunded = 0;

foreach (Paymongo::refunds()->list(['payment_id' => 'pay_i7tdqnmwdszWo5B4Xqk2ogX5']) as $refund) {
    $refunded += $refund->amount; // centavos
}

The page is a Luigel\Paymongo\Pagination\CursorPage. Iterate it for its refunds, check hasMore, and call nextPage() for the next one, or lazy() to walk every page.

When it fails

A refund PayMongo rejects throws a Luigel\Paymongo\Exceptions\PaymongoException, usually an InvalidRequestException, and $e->firstError()?->code says why. PayMongo's Refunds guide names refund_amount_exceeds_payment when the amount is more than is left to refund, payment_not_refundable when the payment is not paid, and payment_not_found. See PayMongo's refund errors for the rest.

Know when it went through

PayMongo sends payment.refunded and payment.refund.updated, which the package dispatches as Luigel\Paymongo\Events\PaymentRefunded and PaymentRefundUpdated. See Webhooks.