# Refund a Payment

This guide covers returning money to a customer after a successful payment: full refunds, partial refunds and how to track what has been returned so far.

You need the gateway transaction id of the payment you are refunding (the `data.id` returned when the payment was created, `2` in the examples below).

## Step 1: Refund the full amount

Call `POST /api/v1/transactions/{transaction_id}/refund` and omit `amount`.

The `Idempotency-Key` header makes the refund safe to retry. If you are unsure whether a refund went through, resend the exact same request with the same key and you get the original response back instead of refunding twice. The [conventions page](/api-basics/conventions) has the full contract.

{/* snippet:refunds:refund-full */}
<CodeTabs syncKey="request-lang">

```shell title="cURL"
curl https://api.sandbox.softlemons.com/api/v1/transactions/2/refund \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer {YOUR_API_KEY}' \
  --header 'Idempotency-Key: 4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f' \
  --data '{}'
```

```js title="JavaScript"
fetch('https://api.sandbox.softlemons.com/api/v1/transactions/2/refund', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer {YOUR_API_KEY}',
    'Idempotency-Key': '4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f'
  },
  body: JSON.stringify({})
})
```

```python title="Python"
requests.post(
    "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund",
    headers={
      "Content-Type": "application/json",
      "Authorization": "Bearer {YOUR_API_KEY}",
      "Idempotency-Key": "4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f"
    },
    json={}
)
```

```java title="Java"
OkHttpClient client = new OkHttpClient();

MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{}");
Request request = new Request.Builder()
  .url("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")
  .post(body)
  .addHeader("Content-Type", "application/json")
  .addHeader("Authorization", "Bearer {YOUR_API_KEY}")
  .addHeader("Idempotency-Key", "4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f")
  .build();

Response response = client.newCall(request).execute();
```

```go title="Go"
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	requestUrl := "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund"

	payload := strings.NewReader(`{}`)

	req, _ := http.NewRequest("POST", requestUrl, payload)

	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("Authorization", "Bearer {YOUR_API_KEY}")
	req.Header.Add("Idempotency-Key", "4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```csharp title="C#"
using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
request.Headers.TryAddWithoutValidation("Idempotency-Key", "4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f");
request.Content = new StringContent(
"""
{}
""",
System.Text.Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
```

```kotlin title="Kotlin"
val client = OkHttpClient()

val mediaType = MediaType.parse("application/json")
val body = RequestBody.create(mediaType, "{}")
val request = Request.Builder()
  .url("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")
  .post(body)
  .addHeader("Content-Type", "application/json")
  .addHeader("Authorization", "Bearer {YOUR_API_KEY}")
  .addHeader("Idempotency-Key", "4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f")
  .build()

val response = client.newCall(request).execute()
```

```objc title="Objective-C"
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"Content-Type": @"application/json",
                           @"Authorization": @"Bearer {YOUR_API_KEY}",
                           @"Idempotency-Key": @"4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f" };

NSDictionary *parameters = @{  };

NSData *postData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://api.sandbox.softlemons.com/api/v1/transactions/2/refund"]
                                                      cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                  timeoutInterval:10.0];
[request setHTTPMethod:@"POST"];
[request setAllHTTPHeaderFields:headers];
[request setHTTPBody:postData];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  if (error) {
    NSLog(@"%@", error);
  } else {
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
    NSLog(@"%@", httpResponse);
  }
}];
[dataTask resume];
```

```php title="PHP"
$ch = curl_init("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund");

curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Authorization: Bearer {YOUR_API_KEY}', 'Idempotency-Key: 4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([]));

curl_exec($ch);

curl_close($ch);
```

```ruby title="Ruby"
require 'uri'
require 'net/http'

url = URI("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request["Authorization"] = 'Bearer {YOUR_API_KEY}'
request["Idempotency-Key"] = '4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f'
request.body = <<~JSON
{}
JSON

response = http.request(request)
puts response.read_body
```

```swift title="Swift"
import Foundation

var request = URLRequest(url: URL(string: "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer {YOUR_API_KEY}", forHTTPHeaderField: "Authorization")
request.setValue("4c9d2e8f-6b1a-4f3c-8d7e-5a2b9c6d1e4f", forHTTPHeaderField: "Idempotency-Key")
let jsonBody = #"""
{}
"""#
request.httpBody = jsonBody.data(using: .utf8)

let (data, response) = try await URLSession.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse,
      200..<300 ~= httpResponse.statusCode else {
  throw URLError(.badServerResponse)
}

print(String(data: data, encoding: .utf8) ?? "")
```

</CodeTabs>
{/* /snippet */}

```json
{
  "success": true,
  "message": "Transaction refunded",
  "code": "",
  "data": {
    "id": 4,
    "related_trans_id": 2,
    "merchant_id": 1,
    "amount": 1250,
    "currency": "EUR",
    "status": "refunded",
    "transaction_type": "refund",
    "merchant_trans_id": "ORDER-912346",
    "created_at": "2025-05-20T09:18:47.000000Z"
  }
}
```

Like captures, each refund is its own transaction row linked to the parent by `related_trans_id` and sharing its `merchant_trans_id`. Your webhook endpoint receives `transaction.refunded` because the cumulative refunds now cover the captured amount.

## Step 2: Partial refunds

Pass an `amount` in major units to return part of the payment:

{/* snippet:refunds:refund-partial */}
<CodeTabs syncKey="request-lang">

```shell title="cURL"
curl https://api.sandbox.softlemons.com/api/v1/transactions/2/refund \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer {YOUR_API_KEY}' \
  --header 'Idempotency-Key: e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a' \
  --data '{
  "amount": 5
}'
```

```js title="JavaScript"
fetch('https://api.sandbox.softlemons.com/api/v1/transactions/2/refund', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer {YOUR_API_KEY}',
    'Idempotency-Key': 'e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a'
  },
  body: JSON.stringify({
    amount: 5
  })
})
```

```python title="Python"
requests.post(
    "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund",
    headers={
      "Content-Type": "application/json",
      "Authorization": "Bearer {YOUR_API_KEY}",
      "Idempotency-Key": "e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a"
    },
    json={
      "amount": 5
    }
)
```

```java title="Java"
OkHttpClient client = new OkHttpClient();

MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\n  \"amount\": 5\n}");
Request request = new Request.Builder()
  .url("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")
  .post(body)
  .addHeader("Content-Type", "application/json")
  .addHeader("Authorization", "Bearer {YOUR_API_KEY}")
  .addHeader("Idempotency-Key", "e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a")
  .build();

Response response = client.newCall(request).execute();
```

```go title="Go"
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	requestUrl := "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund"

	payload := strings.NewReader(`{
  "amount": 5
}`)

	req, _ := http.NewRequest("POST", requestUrl, payload)

	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("Authorization", "Bearer {YOUR_API_KEY}")
	req.Header.Add("Idempotency-Key", "e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```csharp title="C#"
using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");
request.Headers.TryAddWithoutValidation("Idempotency-Key", "e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a");
request.Content = new StringContent(
"""
{
  "amount": 5
}
""",
System.Text.Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
```

```kotlin title="Kotlin"
val client = OkHttpClient()

val mediaType = MediaType.parse("application/json")
val body = RequestBody.create(mediaType, "{\n  \"amount\": 5\n}")
val request = Request.Builder()
  .url("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")
  .post(body)
  .addHeader("Content-Type", "application/json")
  .addHeader("Authorization", "Bearer {YOUR_API_KEY}")
  .addHeader("Idempotency-Key", "e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a")
  .build()

val response = client.newCall(request).execute()
```

```objc title="Objective-C"
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"Content-Type": @"application/json",
                           @"Authorization": @"Bearer {YOUR_API_KEY}",
                           @"Idempotency-Key": @"e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a" };

NSDictionary *parameters = @{ @"amount": @5 };

NSData *postData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://api.sandbox.softlemons.com/api/v1/transactions/2/refund"]
                                                      cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                  timeoutInterval:10.0];
[request setHTTPMethod:@"POST"];
[request setAllHTTPHeaderFields:headers];
[request setHTTPBody:postData];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  if (error) {
    NSLog(@"%@", error);
  } else {
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
    NSLog(@"%@", httpResponse);
  }
}];
[dataTask resume];
```

```php title="PHP"
$ch = curl_init("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund");

curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Authorization: Bearer {YOUR_API_KEY}', 'Idempotency-Key: e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
  'amount' => 5
]));

curl_exec($ch);

curl_close($ch);
```

```ruby title="Ruby"
require 'uri'
require 'net/http'

url = URI("https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request["Authorization"] = 'Bearer {YOUR_API_KEY}'
request["Idempotency-Key"] = 'e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a'
request.body = <<~JSON
{
  "amount": 5
}
JSON

response = http.request(request)
puts response.read_body
```

```swift title="Swift"
import Foundation

var request = URLRequest(url: URL(string: "https://api.sandbox.softlemons.com/api/v1/transactions/2/refund")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer {YOUR_API_KEY}", forHTTPHeaderField: "Authorization")
request.setValue("e5a2c8f4-9d1b-4c6e-b7a3-2f8d5c1e9b4a", forHTTPHeaderField: "Idempotency-Key")
let jsonBody = #"""
{
  "amount": 5
}
"""#
request.httpBody = jsonBody.data(using: .utf8)

let (data, response) = try await URLSession.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse,
      200..<300 ~= httpResponse.statusCode else {
  throw URLError(.badServerResponse)
}

print(String(data: data, encoding: .utf8) ?? "")
```

</CodeTabs>
{/* /snippet */}

Each partial refund creates a new child row and emits its own event. The event type depends on the running total:

- While part of the captured amount remains unrefunded, each refund emits `transaction.partially_refunded`.
- The refund that brings cumulative refunds up to the captured amount emits `transaction.refunded`.

A refund may not exceed the amount still unrefunded on the parent.

## Step 3: Track the totals

Poll the parent transaction when you need the authoritative running totals:

{/* snippet:refunds:poll-totals */}
<CodeTabs syncKey="request-lang">

```shell title="cURL"
curl https://api.sandbox.softlemons.com/api/v1/transactions/2/status \
  --header 'Authorization: Bearer {YOUR_API_KEY}'
```

```js title="JavaScript"
fetch('https://api.sandbox.softlemons.com/api/v1/transactions/2/status', {
  headers: {
    Authorization: 'Bearer {YOUR_API_KEY}'
  }
})
```

```python title="Python"
requests.get(
    "https://api.sandbox.softlemons.com/api/v1/transactions/2/status",
    headers={
      "Authorization": "Bearer {YOUR_API_KEY}"
    }
)
```

```java title="Java"
OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
  .url("https://api.sandbox.softlemons.com/api/v1/transactions/2/status")
  .get()
  .addHeader("Authorization", "Bearer {YOUR_API_KEY}")
  .build();

Response response = client.newCall(request).execute();
```

```go title="Go"
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	requestUrl := "https://api.sandbox.softlemons.com/api/v1/transactions/2/status"

	req, _ := http.NewRequest("GET", requestUrl, nil)

	req.Header.Add("Authorization", "Bearer {YOUR_API_KEY}")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```csharp title="C#"
using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://api.sandbox.softlemons.com/api/v1/transactions/2/status");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "{YOUR_API_KEY}");

using var response = await client.SendAsync(request);
```

```kotlin title="Kotlin"
val client = OkHttpClient()

val request = Request.Builder()
  .url("https://api.sandbox.softlemons.com/api/v1/transactions/2/status")
  .get()
  .addHeader("Authorization", "Bearer {YOUR_API_KEY}")
  .build()

val response = client.newCall(request).execute()
```

```objc title="Objective-C"
#import <Foundation/Foundation.h>

NSDictionary *headers = @{ @"Authorization": @"Bearer {YOUR_API_KEY}" };

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://api.sandbox.softlemons.com/api/v1/transactions/2/status"]
                                                      cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                  timeoutInterval:10.0];
[request setHTTPMethod:@"GET"];
[request setAllHTTPHeaderFields:headers];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  if (error) {
    NSLog(@"%@", error);
  } else {
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
    NSLog(@"%@", httpResponse);
  }
}];
[dataTask resume];
```

```php title="PHP"
$ch = curl_init("https://api.sandbox.softlemons.com/api/v1/transactions/2/status");

curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer {YOUR_API_KEY}']);

curl_exec($ch);

curl_close($ch);
```

```ruby title="Ruby"
require 'uri'
require 'net/http'

url = URI("https://api.sandbox.softlemons.com/api/v1/transactions/2/status")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer {YOUR_API_KEY}'

response = http.request(request)
puts response.read_body
```

```swift title="Swift"
import Foundation

var request = URLRequest(url: URL(string: "https://api.sandbox.softlemons.com/api/v1/transactions/2/status")!)
request.httpMethod = "GET"
request.setValue("Bearer {YOUR_API_KEY}", forHTTPHeaderField: "Authorization")

let (data, response) = try await URLSession.shared.data(for: request)

guard let httpResponse = response as? HTTPURLResponse,
      200..<300 ~= httpResponse.statusCode else {
  throw URLError(.badServerResponse)
}

print(String(data: data, encoding: .utf8) ?? "")
```

</CodeTabs>
{/* /snippet */}

The parent's `refunded_amount` field reports the cumulative refunds in minor units. Webhook payloads for refund events carry the same information in `data.parent_transaction`, so most systems never need to poll. See the [webhooks guide](/guides/webhooks) for the payload shape.

## Good to know

- Amounts in refund requests are major units (`5.00`), while responses and webhooks report minor units (`500`).
- Refund rows sharing the parent's reference is expected and correct. It is not a duplicate. See the [duplicate protection guide](/guides/duplicate-protection).
- A fully refunded payment is terminal. Refunding a different payment for the same customer is a new operation on that payment's own transaction id.
- A replayed request answers with the response stored at first execution and an `Idempotency-Replayed: true` header. Reusing a key with a different body returns HTTP 409 with `ERR_IDEMPOTENCY_CONFLICT`, so generate a fresh key per refund.
