---
title: Refund a credit card transaction
slug: refund-a-credit-card-transaction
docTags: 
createdAt: 2026-09-29T19:27:24.417Z
---

Return money to a customer for a credit card payment, in full or in part.

Use this flow for returns, cancelled orders, duplicate charges, or any case where you owe the customer money back. You can issue several refunds against the same debit, up to the amount that was paid.

***

## Prerequisites

- [Getting your API Key](docId:_zgmIm7QKrrAoX9JjaQrL)
- The ID of a paid credit card debit

If you haven't collected a payment yet, see [Directly charge a credit card](docId\:fQy5afJMMgJ3h5ioYttFd) or [Preauthorize and capture](docId:24zJOsKsikmhbluvDyMgq) first.

***

## Flow overview

```mermaid
sequenceDiagram
    autonumber
    participant Platform
    participant Jetpay
    participant Bank as Customer's Bank

    Platform->>Jetpay: GET /debit/{debit_id}/refunds
    Jetpay-->>Platform: Refundable amount and existing refunds

    Platform->>Jetpay: POST /debit/{debit_id}/refunds
    Jetpay-->>Platform: Refund created (status: processing)
    Jetpay-->>Platform: Webhook (payments.refunds.created)

    Jetpay->>Bank: Submit refund
    Jetpay-->>Platform: Webhook (payments.refunds.submitted / failed)
```

***

## Implementation details

### Check what can be refunded

Endpoint: [`GET /debit/{debit_id}/refunds`](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)/operation/get_debit_refunds_debit__transaction_id__refunds_get)

Look up how much of a debit is still refundable before you issue a refund.

:::CodeblockTabs
```curl
curl https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/refunds \
  -H "Authorization: Bearer <YOUR_API_TOKEN>"
```

```python
import requests

url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/refunds"

headers = {
    "Authorization": "Bearer <YOUR_API_TOKEN>"
}

response = requests.get(url, headers=headers)

print(response.status_code)
print(response.json())
```

```nodejs
const fetch = require("node-fetch");

const url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/refunds";

fetch(url, {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_TOKEN>"
  }
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```
:::

### Handle the response

```json
{
  "transaction_id": "debit_123",
  "captured_amount": 100.00,
  "submitted_refund_amount": 0.00,
  "processing_refund_amount": 0.00,
  "refundable_amount": 100.00,
  "blocked_refund_amount": 0.00,
  "blocked_refund_reason": null,
  "refunds": []
}
```

### Create a refund

Endpoint: [`POST /debit/{debit_id}/refunds`](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)/operation/create_debit_refund_debit__transaction_id__refunds_post)

Refund part or all of the refundable amount, with a reason.

:::CodeblockTabs
```curl
curl https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/refunds \
  -X POST \
  -H "Authorization: Bearer <YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 25.00,
    "reason": "customer_request",
    "reason_details": "Item returned in store"
  }'
```

```python
import requests

url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/refunds"

headers = {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json"
}

payload = {
    "amount": 25.00,
    "reason": "customer_request",
    "reason_details": "Item returned in store"
}

response = requests.post(url, json=payload, headers=headers)

print(response.status_code)
print(response.json())
```

```nodejs
const fetch = require("node-fetch");

const url = "https://extapi.demo.jetpay.baselinepayments.com/debit/debit_123/refunds";

const payload = {
  amount: 25.00,
  reason: "customer_request",
  reason_details: "Item returned in store"
};

fetch(url, {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_API_TOKEN>",
    "Content-Type": "application/json"
  },
  body: JSON.stringify(payload)
})
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```
:::

### Handle the response

If the refund is created, the endpoint returns HTTP `201`:

```json
{
  "id": "refund_001",
  "amount": 25.00,
  "reason": "customer_request",
  "reason_details": "Item returned in store",
  "status": "processing"
}
```

Store the returned `id` to match it against refund events later.

Checking the debit again now shows the refund in progress:

```json
{
  "transaction_id": "debit_123",
  "captured_amount": 100.00,
  "submitted_refund_amount": 0.00,
  "processing_refund_amount": 25.00,
  "refundable_amount": 75.00,
  "blocked_refund_amount": 0.00,
  "blocked_refund_reason": null,
  "refunds": [
    {
      "id": "refund_001",
      "amount": 25.00,
      "reason": "customer_request",
      "reason_details": "Item returned in store",
      "status": "processing"
    }
  ]
}
```

### Track status updates via events/webhooks

As a refund moves through its lifecycle, an **Event** is created for each update. The most efficient way to track these updates in your system is to register for webhooks. If you are not yet familiar with our webhooks system, please review the [Events and webhooks](docId:79a4CGVETAChOLkpJYd5C) guide.

```json
{
  "refundId": "refund_001",
  "transactionId": "debit_123",
  "status": "submitted",
  "amount": "25.00",
  "reason": "customer_request",
  "reasonDetails": "Item returned in store"
}
```

See the [Transactions reference](https://extapi.jetpay.baselinepayments.com/docs#tag/Transactions-\(v0\)) for request fields and error codes, and the [Events reference](https://extapi.jetpay.baselinepayments.com/docs#section/Events-Reference) for the refund events.

***

## Handling failures

**The refund can't be created.** Nothing was refunded, so fix the cause before trying again:

- `409`: the debit isn't refundable right now. Either it hasn't been paid yet, or it has been permanently closed to refunds.
- `422`: the amount is more than what's left to refund, or the debit wasn't paid by credit card.

**The request fails with a&#x20;**`5xx`**&#x20;or times out.** The refund may still have been created. Check [what can be refunded](#check-what-can-be-refunded) before you retry, and only create a new refund if it isn't in the list.

**The refund fails.** A refund can come back as `failed`, either right away in the create response or later through a `payments.refunds.failed` event. A failed refund releases its amount, so you can create a new one for the same amount. `failed` is final, and the same refund never changes status again.

**A submitted refund is rejected.** In rare cases the customer's bank rejects a refund after it was submitted. Jetpay reviews these, and you receive `payments.refunds.failed` if the refund can't be completed.

***

## Testing

Refunds in the demo environment follow the same lifecycle as production. To test end to end:

1. Charge a card in the demo environment using a [Directly charge a credit card](docId\:fQy5afJMMgJ3h5ioYttFd) or a preauthorization you capture.
2. Create a partial refund, then check that the refundable amount drops by that amount.
3. Try to refund more than what's left and confirm you handle the `422`.
4. Point a webhook at your test server and confirm you record `payments.refunds.created`.

***

## Best practices

- **Check before you refund.** Read the refundable amount first so you never ask for more than is available.
- **Never blindly retry a create.** Refund creation doesn't support an `Idempotency-Key`, so repeating a request creates a second refund. After any error or timeout, check the refunds list first.
- **Store the refund&#x20;**`id`**.** It's how you match webhook events to the refund in your system.
- **Drive your status from webhooks.** Update your records from refund events rather than polling, and be ready for `failed` at any point.
- **Record a reason.** Add `reason_details` so your team and ours can see why the money went back.

***

## FAQ

**Can I refund a bank transfer or an in-person terminal payment?**
No. Refunds are available for credit card debits only.

**Can I issue more than one refund on the same payment?**
Yes. Refund as many times as you need, up to the total amount paid.

**Can I refund a preauthorization?**
You can refund whatever has been captured. To give back an amount you haven't captured yet, release it instead (see [`Preauthorize and capture`](docId:24zJOsKsikmhbluvDyMgq)).

**Does the refund include the surcharge?**
The refundable amount is what the customer was charged, including any customer surcharge. Refund that full amount to give back everything they paid.

**Is my customer notified?**
Yes. Jetpay emails the customer when a refund is initiated.

**How long does a refund take?**
A refund stays `processing` while Jetpay prepares and sends it, then moves to `submitted` once it has been sent to the customer's bank. When the money shows up on the customer's statement depends on their bank.
