Skip to content
jovvie.com

Refunds for custom Jovvie Checkout integrations

This page explains how refunds work for payments your integration creates, and how your integration can keep its records up to date when a merchant refunds a payment.

The Jovvie SDKs cannot create refunds yet. SDK support for refunds is planned. Until then, the merchant refunds the payment with their payment processor. Your integration can update its records when a refund event arrives, and the Stripe Dashboard or the NMI portal is the record to check against.

Processor Where to refund
Stripe The merchant’s Stripe Dashboard
NMI The NMI portal

Do not build a refund button in your app that looks like it refunds the card. If your app has a refund screen, tell staff to make the refund in the Stripe Dashboard or the NMI portal.

Before making or retrying a refund, or returning money another way, check the payment and its refunds in the Stripe Dashboard or the NMI portal. Confirm that the amount being returned has not already been refunded and no refund for that amount is still processing. If you are unsure, contact Jovvie support.

When you create a payment, save the IDs the merchant will need to find it with their processor:

  • Stripe: $payment->generatedData->paymentIntentId. This is the payment’s ID in Stripe.
  • NMI: the NMI transaction ID. Get it with getTransactionId() from the verified jovvie-gateway.payment.completed webhook event. See Handle webhooks.

Show these IDs on the order in your app. With Stripe, the description you pass when you create the payment also appears on the payment in the Stripe Dashboard.

Jovvie can send a refund event to your application’s Webhook URL. When one arrives, verify it with your webhook key before you use it, then update your records. See Handle webhooks.

Do not count on a refund event for every refund. The Stripe Dashboard or the NMI portal is the record of what was refunded, so reconcile your records against it. See Keep your records in step.

Stripe NMI
Event type jovvie.payment.refunded jovvie-gateway.payment.refunded
Event class JovviePayments\WebhookEvents\Events\PaymentRefunded JovviePaymentsGateway\WebhookEvents\Events\PaymentRefunded
Jovvie payment ID getPaymentId() getPaymentId()
Processor ID getPaymentIntentId() getTransactionId()
Amount refunded getInvoiceRefund(): the total refunded so far getTransactionRefund(): the amount of this refund
Other amount getInvoiceAmount(): the amount left after refunds getTransactionAmount(): the original payment amount
Currency getInvoiceCurrency() getTransactionCurrency()
Mode getPaymentMode(): live or test Not included

All amounts are in the smallest currency unit, such as cents.

A refunded status can mean a full or a partial refund. To tell them apart, compare the amounts in the event. Do not rely on the payment’s status alone to know whether, or how much, a payment was refunded. Check the Stripe Dashboard or the NMI portal.

This example runs after you have verified the webhook and created $event with WebhookEvent::constructEvent(). The same event can arrive more than once, so it saves the event ID and the refund in one database transaction. Your events table has a unique constraint on the event ID, so a second copy of the event cannot be saved. See Process each event once.

use JovviePayments\WebhookEvents\Events\PaymentRefunded;
if ($event instanceof PaymentRefunded) {
// Ignore events from the other mode, so a test refund never changes a live order.
$appMode = 'YOUR_APP_MODE'; // 'live' or 'test'. Load this from your server configuration.
if ($event->getPaymentMode() !== $appMode) {
http_response_code(200);
exit;
}
yourApp_beginTransaction();
try {
// Fails if this event ID was already saved.
if (!yourApp_insertEventId($event->getId())) {
yourApp_rollBack();
http_response_code(200);
exit;
}
$order = yourApp_findOrderByJovviePaymentId($event->getPaymentId());
if (!$order) {
// Keep the event so you can match it to an order later.
yourApp_storeEventForReconciliation($event);
} else {
// Lock the order row so two refund events cannot update it at once.
yourApp_lockOrder($order);
$refunded = $event->getInvoiceRefund(); // total refunded so far
$remaining = $event->getInvoiceAmount(); // amount left
// Events can arrive out of order. Only move forward: never replace
// a larger refunded total with a smaller one.
if ($refunded > yourApp_getRefundedTotal($order)) {
if ($remaining === 0) {
yourApp_markFullyRefunded($order, $refunded);
} else {
yourApp_markPartlyRefunded($order, $refunded);
}
}
}
yourApp_commit();
} catch (\Throwable $e) {
// Nothing was saved. Answer with an error so Jovvie sends the event again.
yourApp_rollBack();
http_response_code(500);
exit;
}
http_response_code(200);
exit;
}

The yourApp_ functions stand for your own code.

With NMI, each refund event carries the amount of that refund only. If the merchant makes more than one partial refund, add the amounts from each event to get the total.

  • The refund is made by the processor, not by your app. Treat the Stripe Dashboard or the NMI portal as the record of what was refunded.
  • If your app does not show a refund the merchant made, check the payment in the Stripe Dashboard or the NMI portal and update your records to match.
  • Update your records after the refund is made, not before. If your app shows a refund that was never made, staff and customers can be misled.

Contact Jovvie support.