Billing
Refunds
Return money to a customer for a captured payment, in full or in part. Refunds work the same way whatever payment rail took the original charge, and the amount is always re-derived on our side so you can never refund more than what is left.
Issue a refund
Refund a payment by its id. Omit the amount to return the full remaining
balance, or pass an amount for a partial refund. Send an Idempotency-Key
so a retried request returns the original refund instead of issuing a second
one.
curl -X POST https://api.meridapay.com/payments/<payment-id>/refund \
-H "Authorization: Bearer $MERIDA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-2026-09-09-001" \
-d '{ "amount": "49.99" }'
With the TypeScript SDK:
await merida.POST("/payments/{id}/refund", {
params: { path: { id: paymentId } },
body: { amount: "49.99" },
headers: { "idempotency-key": "refund-2026-09-09-001" },
})
The full request and response, including the reason field and the error
cases, live in the API reference. You never send a currency: we
refund in the currency of the original charge and reject an amount larger
than the captured total minus what was already refunded.
Refund status
A refund comes back with one of two states:
succeededstatusThe money is on its way back to the customer. Nothing more to do.
pendingstatusThe refund was accepted but the funds have not landed back yet, which happens with some payment methods that take a day or two to settle. It completes on its own. Listen for the webhook below, or re-fetch the payment to read its final state.
While a refund is pending, its amount is already held against the payment's refundable balance, so you cannot refund the same money twice.
Refunds under a partner group key
Partners refund a practice's payment with their group key, without the
practice signing in. The identifier is always the payment id, and it covers
every payment type. For a checkout (a one-off or a payment request) you read
payment_id back from GET /partner/practices/{id}/checkouts/{checkoutId}
once it is paid, or from the public GET /checkouts/{id}. For an off-session
charge (an installment with no checkout) the same payment_id comes back on
the charge response itself. Store it at payment time and pass it straight to
the refund.
curl -X POST \
https://api.meridapay.com/partner/practices/<practice-id>/payments/<payment-id>/refund \
-H "Authorization: Bearer $MERIDA_GROUP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-2026-09-14-001" \
-d '{ "amount": "50.00" }'
Full and partial work the same as the org-key refund above, and the response
carries the same id, payment_id, amount, currency, and status. A
payment that belongs to another practice or group is a 404, never a
cross-tenant refund.
To validate a partial before you send it, or to reconcile afterwards, read the payment's refund state:
curl https://api.meridapay.com/partner/practices/<practice-id>/payments/<payment-id> \
-H "Authorization: Bearer $MERIDA_GROUP_KEY"
It returns amount_captured, amount_refunded, refundable_amount, and a
refund_status of none, pending, partially_refunded, or refunded.
To receive refund webhooks for every practice at one URL instead of registering one per practice, register a group-level endpoint. See Group endpoints under Webhooks.
Webhook events
payment.refundedwebhookA refund completed and the funds are on their way back. Fires for full and partial refunds, and once a pending refund finishes settling. The payload carries the paymentId, the refund amount, and the round-trip fields from the original checkout.
payment.refund_failedwebhookA refund that was accepted could not be completed. The payment stays in its prior settled state and the funds were not returned.
See Webhooks for delivery, signing, and retry behavior.
From the dashboard
Merchants can refund without writing code. Open an order under Revenue, click Refund, and enter an amount up to the refundable balance. The same status and events above apply, so a pending refund shows as processing until it settles.