Payments
Refunds
Refunds send MUNY from your settlement wallet back to the wallet that paid, verified on chain like every other Muny transfer. Full and partial refunds are supported.
Two flows#
Muny never takes custody of keys it doesn't already manage. Which flow applies depends on your settlement wallet.
| Settlement wallet | Flow | Statuses |
|---|---|---|
| Managed (Muny custody provider) | Muny signs and sends the refund for you. | pending → submitted → confirmed |
| External (your own wallet) | Muny prepares the transfer; you sign it with your wallet and submit it. | requires_signature → submitted → confirmed |
Managed wallet
ts
// Full refund (defaults to the remaining refundable amount)
const refund = await muny.payments.refund(payment.id, { reason: "Customer request" });
// Partial refund
await muny.payments.refund(payment.id, { amount: "50", reason: "Duplicate seat", idempotency_key: "refund_order_48291_1" });
refund.status; // "pending" → "submitted" → "confirmed"External (non-custodial) wallet
ts
const refund = await muny.payments.refund(payment.id, { amount: "249" });
// refund.status === "requires_signature"
// refund.transaction_to_sign → base64 transaction (network fee sponsored by Muny)
// Sign it with the settlement wallet (wallet adapter, hardware wallet, your signer)…
const tx = VersionedTransaction.deserialize(Buffer.from(refund.transaction_to_sign!, "base64"));
const signed = await wallet.signTransaction(tx);
// …and hand it back. Muny co-signs as fee payer, checks it matches what it prepared, and broadcasts.
await muny.refunds.submit(refund.id, Buffer.from(signed.serialize()).toString("base64"));Muny can't alter what you sign
The submitted transaction must match the prepared one byte for byte; Muny only adds the fee-payer signature. If the prepared transaction expires before you sign, submit fails and the refund staysrequires_signature, so you can try again.Rules#
- Only
confirmed,finalizedorpartially_refundedpayments can be refunded. - The destination is always the verified payer address from the original payment. It can't be changed.
- The sum of all non-failed refunds can never exceed the payment amount. Concurrent refund requests are serialised, so two can't both pass.
- When refunds confirm, the payment moves to
partially_refundedorrefundedandpayment.refundedfires. - Every refund is recorded in the audit log (refund.requested, refund.confirmed).
Refund object#
| Field | Description |
|---|---|
id, payment_id | Refund and payment identifiers. |
amount, token | Decimal string in whole tokens. |
status | pending · requires_signature · submitted · confirmed · failed |
destination_address, wallet_id | Payer wallet and source settlement wallet. |
transaction_to_sign | Only while requires_signature. |
transaction_signature, explorer_url | Once submitted. |
reason, failure_message | Your note, and the error if it failed. |
Endpoints#
POST /v1/payments/:id/refunds—{ amount?, reason?, idempotency_key? }(scoperefund_payments)GET /v1/payments/:id/refunds·GET /v1/refunds/:idPOST /v1/refunds/:id/submit—{ signed_transaction }