> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.crisscross.money/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.crisscross.money/_mcp/server.

# Refund a Payment

POST https://api.crisscross.money/v1/payment/{transactionId}/refund
Content-Type: application/json

Initiates a refund against a completed payment, identified by its `transactionId` in the path. Omit `refundValue` for a full refund; supply it (amount in minor units) for a partial refund. When provided, the currency must match the original payment.

CrissCross executes refunds in one of two ways depending on what the originating provider supports: a **provider refund** through the provider's native refund API, or a **payout refund** that returns the funds via a payout to the original destination. The latter is used when the originating provider does not expose a refund API — common for mobile money, and applicable to bank methods when full account details are held. Both paths are tracked against the original payment so the refundable balance is enforced uniformly. See the [Refunds guide](/full-and-partial-refunds) for details.

Eligibility (ownership, refundable state, refund window, and cumulative amount ≤ original) is validated when the refund is initiated. Call [Get Payment Refundability](#operation/getRefundability) beforehand to check whether a payment can be refunded and for how much.

Refunds are processed asynchronously. The response is a **refund transaction** in a non-terminal state; track completion by polling [Retrieve Payment Status](#operation/getTransaction) with the returned refund `transactionId`, or by subscribing to the `refund.completed`, `refund.failed`, `refund.errored`, and `refund.cancelled` webhook events.


Reference: https://docs.crisscross.money/api-reference/collect/payments/manage-payments/refund-payment

## Request

### Path parameters

- `transactionId` (string, required) — Identifier of the original payment to refund. UUID v7.

### Body (application/json)

This endpoint expects a RefundPaymentRequest.

- `refundValue` (RefundValue, optional) — Refund amount in minor units. Omit for a full refund. When provided, the currency must match the original payment.
- `reason` (string, optional) — Optional free-form reason for the refund, recorded for reporting and dispute defense.

## Response

### 201

Refund accepted and created as an asynchronous refund transaction. Poll the returned `transactionId` for completion.

- `status` (string, required) — Current transaction status.
- `transactionId` (string, required) — Unique identifier for the transaction.
- `message` (string, required) — Human-readable status message.
- `authState` (AuthState, optional) — Detail for the transaction's current state. When the transaction status is `AUTH_REQUIRED`, `authMethodType` says how the payer completes authentication, and a matching detail object (`redirect`, `fields`, `ussd`, `bankTransfer`) carries the specifics. Error states instead carry `code`, `message`, and the raw `connectorFailureCode`/`connectorFailureMessage`.
- `sessionId` (string, optional) — Session the transaction belongs to.
- `merchantReference` (string, optional) — Merchant reference from the original session.
- `processorName` (string, optional) — Name of the payment processor (if assigned).
- `processorReference` (string, optional) — Processor reference (if available).
- `paymentMethodId` (string, optional) — The payment method used, e.g. `card` or `mobilemoney`.
- `identifiers` (map from string to string, optional) — Additional identifiers, usually `sessionId` and `payerId`.
- `paymentAttributes` (map from string to string, optional) — Attributes associated with the payment.
- `batchPayoutId` (string, optional) — Batch payout identifier, present when the transaction is part of a batch payout.
- `collection` (FxMoneyAmount, optional) — Amount the customer paid in the collection currency. Present only on transactions converted by [Adaptive Currency Conversion](/adaptive-currency-conversion) — per-session rate or [rate lock](/rate-locks).
- `fxMidRate` (string, optional) — Spot mid-market rate at the time the FX quote was committed for this attempt (decimal string, 8 decimal places). Present only on FX-converted transactions.
- `fxAllInRate` (string, optional) — Mid rate + markup — the rate this attempt was actually converted at (decimal string, 8 decimal places). Present only on FX-converted transactions.
- `fxMarkupBps` (integer, optional) — Markup applied to this attempt in basis points (100 bps = 1%). Present only on FX-converted transactions.
- `fxFee` (FxMoneyAmount, optional) — Conversion fee, denominated in the collection currency. Present only on FX-converted transactions.

## Errors

### 400 Bad Request Error

Invalid request parameters.

- `message` (string, required) — Human-readable description of what went wrong.
- `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on.
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

### 401 Unauthorized Error

Unauthorized.

- `message` (string, required) — Human-readable description of what went wrong.
- `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on.
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

### 404 Not Found Error

Payment not found.

- `message` (string, required) — Human-readable description of what went wrong.
- `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on.
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

### 422 Unprocessable Entity Error

Unprocessable Entity — for example, the payment is not in a refundable state, the refund amount exceeds the remaining refundable balance, or the currency does not match the original payment.

- `message` (string, required) — Human-readable description of what went wrong.
- `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on.
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

## Types

### RefundValue

- `minorAmount` (integer, required) — Amount in minor units (e.g., cents). For NGN, 10000 = 100.00 NGN.
- `currency` (string, required) — ISO 4217 currency code. Must match the currency of the original transaction.

### AuthState

Detail for the transaction's current state. When the transaction status is `AUTH_REQUIRED`, `authMethodType` says how the payer completes authentication, and a matching detail object (`redirect`, `fields`, `ussd`, `bankTransfer`) carries the specifics. Error states instead carry `code`, `message`, and the raw `connectorFailureCode`/`connectorFailureMessage`.

- `state` (string, optional) — Transaction state name. In-flight states are uppercase (`PENDING`, `SCREENING_PENDING`, `SCREENING_COMPLETED`, `AUTH_REQUIRED`); terminal states are lowercase (`completed`, `failed`, `error`, `cancelled`, `expired`, `settled`). Match exact values, casing included.
- `transitionedAt` (string, optional) — ISO 8601 timestamp when the transaction entered this state.
- `authMethodType` (enum, optional) — How the payer completes authentication. Present on `AUTH_REQUIRED` states.
  - Allowed values: `redirect`, `fields`, `ussd`, `pendingApproval`, `bankTransfer`, `none`
- `redirect` (AuthStateRedirect, optional) — Present when `authMethodType` is `redirect` — send the payer to `url`.

### FxMoneyAmount

Monetary amount as a decimal-string major unit + ISO 4217 currency code.

- `amount` (string, required) — Decimal string in major units.
- `currency` (string, required) — ISO 4217 currency code.

### AuthStateRedirect

Present when `authMethodType` is `redirect` — send the payer to `url`.

- `url` (string, optional) — URL to redirect the payer to for authentication.
- `requiresTopLevel` (boolean, optional) — When true, the browser must navigate top-level to `url` rather than embedding it in an iframe.

## Examples

**Request**

```json
{
  "refundValue": {
    "minorAmount": 2000,
    "currency": "NGN"
  },
  "reason": "Customer returned the item"
}
```

**Response**

```json
{
  "status": "string",
  "transactionId": "string",
  "message": "string",
  "authState": {
    "state": "string",
    "transitionedAt": "string",
    "authMethodType": "redirect",
    "redirect": {
      "url": "string",
      "requiresTopLevel": true
    }
  },
  "sessionId": "string",
  "merchantReference": "string",
  "processorName": "string",
  "processorReference": "string",
  "paymentMethodId": "string",
  "identifiers": {},
  "paymentAttributes": {},
  "batchPayoutId": "string",
  "collection": {
    "amount": "string",
    "currency": "string"
  },
  "fxMidRate": "string",
  "fxAllInRate": "string",
  "fxMarkupBps": 1,
  "fxFee": {
    "amount": "string",
    "currency": "string"
  }
}
```

**SDK Code**

```python
import requests

url = "https://api.crisscross.money/v1/payment/transactionId/refund"

payload = {
    "refundValue": {
        "minorAmount": 2000,
        "currency": "NGN"
    },
    "reason": "Customer returned the item"
}
headers = {"Content-Type": "application/json"}

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

print(response.json())
```

```javascript
const url = 'https://api.crisscross.money/v1/payment/transactionId/refund';
const options = {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: '{"refundValue":{"minorAmount":2000,"currency":"NGN"},"reason":"Customer returned the item"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

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

func main() {

	url := "https://api.crisscross.money/v1/payment/transactionId/refund"

	payload := strings.NewReader("{\n  \"refundValue\": {\n    \"minorAmount\": 2000,\n    \"currency\": \"NGN\"\n  },\n  \"reason\": \"Customer returned the item\"\n}")

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

	req.Header.Add("Content-Type", "application/json")

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

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

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

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.crisscross.money/v1/payment/transactionId/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.body = "{\n  \"refundValue\": {\n    \"minorAmount\": 2000,\n    \"currency\": \"NGN\"\n  },\n  \"reason\": \"Customer returned the item\"\n}"

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

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.crisscross.money/v1/payment/transactionId/refund")
  .header("Content-Type", "application/json")
  .body("{\n  \"refundValue\": {\n    \"minorAmount\": 2000,\n    \"currency\": \"NGN\"\n  },\n  \"reason\": \"Customer returned the item\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.crisscross.money/v1/payment/transactionId/refund', [
  'body' => '{
  "refundValue": {
    "minorAmount": 2000,
    "currency": "NGN"
  },
  "reason": "Customer returned the item"
}',
  'headers' => [
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.crisscross.money/v1/payment/transactionId/refund");
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"refundValue\": {\n    \"minorAmount\": 2000,\n    \"currency\": \"NGN\"\n  },\n  \"reason\": \"Customer returned the item\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Content-Type": "application/json"]
let parameters = [
  "refundValue": [
    "minorAmount": 2000,
    "currency": "NGN"
  ],
  "reason": "Customer returned the item"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.crisscross.money/v1/payment/transactionId/refund")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```