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.
Where merchants refund
Section titled “Where merchants refund”| 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.
Help the merchant find the payment
Section titled “Help the merchant find the payment”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 verifiedjovvie-gateway.payment.completedwebhook 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.
How your integration hears about a refund
Section titled “How your integration hears about a refund”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.
Example: handle a Stripe refund event
Section titled “Example: handle a Stripe refund event”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.
Keep your records in step
Section titled “Keep your records in step”- 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.
Related pages
Section titled “Related pages”Need help?
Section titled “Need help?”Contact Jovvie support.