Issue a refund
A refund returns captured funds to the customer who paid them. You can refund a charge by its payment intent or by the invoice it paid, refund the full amount or only part of it, and record refunds that were already processed outside PaymentKit.
Create a refund
A refund always targets a single succeeded charge. You identify that charge in one of two ways:
payment_intent_id— refund the latest succeeded charge on a payment intent.invoice_id— refund the charge that paid an invoice. PaymentKit resolves the payment allocation for you. (If the invoice was paid by more than one payment intent, refund bypayment_intent_idinstead.)
Provide at least one of these. If you supply both, payment_intent_id takes precedence.
Dashboard
API
- Open the Payment or Invoice you want to refund
- Click Refund
- Enter a full or partial amount and choose a reason
- Click Refund to send it to the processor
The refund appears immediately with a Pending status and updates to Succeeded once the processor confirms it.
Request fields
Send an Idempotency-Key header on every create request. If the call is retried with the same
key, PaymentKit returns the original refund instead of issuing a duplicate.
The refund object
Refund statuses
Look up refunds
Rules and limits
- The charge must have succeeded. You can only refund a charge in the
succeededstate. - You cannot refund more than the refundable amount — the captured amount minus any refunds
that have already succeeded. Fully-refunded charges have a refundable amount of
0. - Partial refunds are allowed as long as the running total stays within the refundable amount. Issue several partial refunds against the same charge until it is fully refunded.
- Refunding an invoice payment reduces the paid amount on the linked invoice(s) accordingly.
Webhook events
Subscribe to refund.succeeded to reconcile completed refunds. It is the positive signal for a
refund reaching a terminal successful state, and it fires on every path a refund can settle
through — including refunds that settle asynchronously, hours after they were created.
refund.succeeded does not replace refund.updated — nothing was removed, so an integration
watching refund.updated keeps behaving exactly as it does today. If you do receive both for the
same refund, treat them as the same fact rather than two refunds. Reconcile on refund.succeeded:
it is the event that fires on every settlement path.
refund.failed is different: it replaces refund.updated for the failure transition, and you will
not receive both.
Once refund.succeeded fires, the refund is terminal for every ordinary flow and the charge’s
refundable amount is reduced in the same transaction as the event. An administrative correction can
still move a settled refund back to failed afterwards, which emits refund.failed.
What the payload carries
The webhook body’s data.object carries the refund’s stored columns, including amount_atom, currency,
status, charge_id, processor_refund_id, reason, failure_code, failure_message,
processed_at, created_at, updated_at.
Two differences from the REST response shown above are worth knowing before you parse it:
- Your own metadata arrives as
meta, notmetadata. The REST API renames the column; the webhook body does not.metadatain a webhook body is never the refund’s metadata. - Keys are never omitted. A field with no value arrives as
null, so a refund recorded out of band — which has no processor reference — arrives as"processor_refund_id": null. Branch on the value, not on whether the key exists.
The same event is also retrievable from GET /api/{account_id}/events/, where it carries a separate
metadata object describing the transition — including amount_atom, currency, from_state,
to_state, and processor_refund_id when there is one. That object is not part of the webhook
body, and it is unrelated to the refund’s own meta.