Skip to navigation

Issue a refund

Return funds to a customer for a charge, a payment intent, or a paid invoice — in full or in part.
View as Markdown

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 by payment_intent_id instead.)

Provide at least one of these. If you supply both, payment_intent_id takes precedence.

  1. Open the Payment or Invoice you want to refund
  2. Click Refund
  3. Enter a full or partial amount and choose a reason
  4. 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

FieldTypeDescription
payment_intent_idstringThe payment intent to refund. Required unless invoice_id is provided.
invoice_idstringAn invoice to refund. PaymentKit resolves the charge that paid it. Required unless payment_intent_id is provided.
amount_atomintegerAmount to refund, in the smallest currency unit (e.g. cents). Must be greater than 0. Defaults to the full refundable amount.
currencystringCurrency code (e.g. USD). Defaults to the charge’s currency; if provided, it must match.
reasonstringOne of manual, duplicate, fraudulent, requested_by_customer, expired_uncaptured_charge, manual_out_of_band. Defaults to manual.
refunded_out_of_bandbooleanSet true to record a refund you already processed elsewhere (e.g. in the processor’s dashboard). No processor call is made and the refund is marked succeeded immediately. Defaults to false.
metadataobjectFree-form JSON, up to 10 KB.

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

{
"id": "re_prod_0a1b2c3d4e5f6g7h",
"account_id": "acc_prod_...",
"charge_id": "pa_prod_...",
"amount_atom": 1500,
"currency": "USD",
"status": "succeeded",
"reason": "requested_by_customer",
"processor_refund_id": "re_3Nz...",
"failure_code": null,
"failure_message": null,
"processed_at": "2026-06-17T14:05:00Z",
"metadata": null,
"created_at": "2026-06-17T14:04:58Z",
"updated_at": "2026-06-17T14:05:00Z"
}
FieldDescription
idRefund identifier, prefixed re_.
charge_idThe charge that was refunded.
amount_atom / currencyThe refunded amount and its currency.
statusLifecycle status — see below.
reasonThe reason supplied at creation, if any.
processor_refund_idThe processor’s own refund ID. null for out-of-band refunds or while still pending.
failure_code / failure_messagePopulated when status is failed.
processed_atWhen the processor finished the refund. null while pending.

Refund statuses

StatusMeaning
pendingCreated and awaiting the processor’s response.
succeededFunds returned to the customer (or recorded out-of-band). Terminal.
failedThe processor rejected the refund. See failure_code / failure_message. Terminal.
requires_actionAdditional action is required before the refund can complete (rare). Non-terminal.
cancelledCancelled before completion — from pending or requires_action. Terminal.

Look up refunds

MethodPathReturns
GET/api/{account_id}/payments/refunds/{refund_id}A single refund
GET/api/{account_id}/payments/refunds/by_charge/{charge_id}All refunds for a charge (paginated)
GET/api/{account_id}/payments/refunds/by_intent/{payment_intent_id}All refunds for a payment intent (paginated)
curl https://app.paymentkit.com/api/{account_id}/payments/refunds/by_intent/pi_abc123 \
-H "Authorization: Bearer sk_live_..."

Rules and limits

  • The charge must have succeeded. You can only refund a charge in the succeeded state.
  • 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

EventTrigger
refund.createdA refund was created.
refund.updatedA refund field changed. Not sent for the transition to failed, and not reliably sent for the transition to succeeded — reconcile settlements on refund.succeeded.
refund.succeededThe refund settled.
refund.failedThe refund was rejected. Carries failure_code and failure_message.

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, not metadata. The REST API renames the column; the webhook body does not. metadata in 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.