A refund is a separate resource whose progress can outlast the request that
created it. Use a configured client named payments, caller context
ctx, and the original financial payment ID saved against your order. Import
arkoren.dev/core/service/payment for the request and status types.
Choosing Amounts
Authorize the refund against the saved order/account and select its persisted merchant and test/live environment. Do not accept an arbitrary provider ID from the caller. Calculate the refund under your server-side policy and keep its currency equal to the original payment’s currency.
For EUR 5.00 of an existing EUR payment, construct an exact positive amount:
refundAmount, err := payment.NewMoney("EUR", 500)
A full refund also requires an explicit positive amount. Zero does not mean “the remainder.” The provider enforces the remaining refundable balance, including concurrent requests; a locally calculated balance is not a reservation.
Submitting Refunds
Give this logical refund its own durable operation key, distinct from checkout and completion keys. Save the immutable request, original payment ID, and merchant/environment before dispatch:
request := payment.NewRefundRequest(paymentID, refundAmount, "book-184-refund-1")
Reuse that key only for the identical request within the provider’s documented scope and retention. A second intended partial refund is a different operation; it must not become a way to repeat an unresolved first refund.
A failed refund reply can follow an accepted refund. Keep the original request/key and any returned refund references even with an error. A later rejection or read failure does not settle that earlier attempt. Follow Payment Recovery rather than issuing a fresh-key refund.
Submit the saved request:
refund, err := payments.RefundPayment(ctx, request)
Record the result before telling the buyer it completed. An accepted
RefundStatusPending is normal provider progress, not returned money and not
an outcome-unknown error. The facade does not perform a hidden balance lookup
or post-mutation read; a driver can document a required currency preflight.
Reading Progress
Save the entire refund.ID, not a token extracted from its value. To obtain a
later observation, read the saved refundID without resubmitting the refund:
observed, err := payments.GetRefund(ctx, refundID)
After a successful read, reconcile the returned identity, expected amount, and any observed payment relationship against your saved operation. Missing relationships remain absent rather than being guessed.
| Status | Interpretation |
|---|---|
RefundStatusSucceeded |
The provider confirms refund success. |
RefundStatusPending |
The refund is not finalized. |
RefundStatusRequiresAction |
Further action is required; funds are not confirmed returned. |
RefundStatusFailed |
A known unsuccessful refund outcome. |
RefundStatusCanceled |
Canceled before success. |
RefundStatusUnknown |
No mapped state; do not treat it as success. |
Your application chooses when to read and how to notify the buyer. Provider notifications can repeat or arrive out of order, so persist reconciliation idempotently. Refunding a payment does not cancel a recurring agreement; subscription cancellation is separate.
Provider Limits
Stripe checks the PaymentIntent’s currency before submitting an explicit refund.
PayPal refunds Payments v2 captures only; subscription sale lookup/refund returns
ErrUnsupported. A sale ID cannot be substituted for a capture ID.
Mollie’s opaque compound refund handle carries the parent payment needed for
lookup. Preserve it verbatim. Its queued/pending/processing states remain pending;
only refunded succeeds. Mollie caches POST replies for one hour per exact
credential, so credential rotation or replay after expiry can duplicate an
unresolved refund. Its classic callback carries a payment ID, not this compound
handle: read the refund IDs already saved by your application.
Use Testing to script pending, succeeded, and uncertain replies without financial effects. For request and result types, see the payment API.