Refunds
Process full or partial refunds for completed payments.
This guide covers processing refunds for completed payments via the Paysera Checkout API.
Overview​
Refunds allow you to return funds to customers for completed payments. You can process:
- Full refunds - Return the entire payment amount
- Partial refunds - Return a portion of the payment amount
Amount Format
All amounts use minor currency units (e.g., cents for EUR):
- Request: String format (e.g.,
"1000"for €10.00) - Response: Long/integer format (e.g.,
1000for €10.00)
Prerequisites​
Before processing a refund:
- The original order must be in
paidstatus - Sufficient time has passed for the payment to settle (typically 1-2 business days)
- The refund amount doesn't exceed the original payment amount minus any previous refunds
Endpoint​
| Method | Endpoint | Description |
|---|---|---|
| POST | /merchant-order/integration/v1/orders/{order_id}/refunds | Create a refund |
Create Refund​
Request​
curl -X POST https://api.paysera.com/merchant-order/integration/v1/orders/ORDER_ID/refunds \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": "1000",
"currency": "EUR",
"reason": "Customer requested refund"
}'
Request Parameters​
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Refund amount in minor units as string (e.g., "1000" for €10.00) |
currency | string | Yes | ISO 4217 currency code (must match original) |
reason | string | No | Reason for the refund |
Response​
{
"id": "b7c8d9e0-1f2a-3b4c-5d6e-7f8a9b0c1d2e",
"orderId": "a6f2b8e3-5e5f-47d9-b13f-87ed2db2938a",
"status": "pending",
"amount": 1000,
"currency": "EUR",
"reason": "Customer requested refund",
"createdAt": 1736437200
}
Refund Statuses​
| Status | Description |
|---|---|
pending | Refund initiated, processing |
completed | Refund successfully processed |
failed | Refund failed |
Code Examples​
- PHP
- JavaScript
- Python
- Kotlin
- Go
- C#
<?php
function createRefund(string $accessToken, string $orderId, int $amountCents, string $currency, ?string $reason = null): array
{
$url = "https://api.paysera.com/merchant-order/integration/v1/orders/$orderId/refunds";
$payload = [
'amount' => (string) $amountCents, // Already in minor units (cents)
'currency' => $currency,
];
if ($reason) {
$payload['reason'] = $reason;
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 201) {
throw new Exception('Failed to create refund: ' . $response);
}
return json_decode($response, true);
}
// Full refund - amount in minor units (cents)
$refund = createRefund(
$accessToken,
'order-uuid',
2500, // €25.00 in cents
'EUR',
'Customer requested refund'
);
echo "Refund ID: " . $refund['id'];
echo "Status: " . $refund['status'];
async function createRefund(accessToken, orderId, amountCents, currency, reason) {
const response = await fetch(
`https://api.paysera.com/merchant-order/integration/v1/orders/${orderId}/refunds`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: String(amountCents), // Already in minor units (cents)
currency: currency,
reason: reason,
}),
}
);
if (!response.ok) {
throw new Error(`Failed to create refund: ${await response.text()}`);
}
return response.json();
}
// Usage - amount in minor units (cents)
const refund = await createRefund(
accessToken,
'order-uuid',
2500, // €25.00 in cents
'EUR',
'Customer requested refund'
);
console.log('Refund ID:', refund.id);
console.log('Status:', refund.status);
import requests
def create_refund(access_token: str, order_id: str, amount_cents: int, currency: str, reason: str = None) -> dict:
payload = {
'amount': str(amount_cents), # Already in minor units (cents)
'currency': currency,
}
if reason:
payload['reason'] = reason
response = requests.post(
f'https://api.paysera.com/merchant-order/integration/v1/orders/{order_id}/refunds',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
},
json=payload,
)
response.raise_for_status()
return response.json()
# Usage - amount in minor units (cents)
refund = create_refund(
access_token,
'order-uuid',
2500, # €25.00 in cents
'EUR',
'Customer requested refund'
)
print(f"Refund ID: {refund['id']}")
print(f"Status: {refund['status']}")
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import kotlinx.serialization.json.*
suspend fun createRefund(
accessToken: String,
orderId: String,
amountCents: Long,
currency: String,
reason: String? = null
): JsonObject {
val client = HttpClient.newHttpClient()
val requestBody = buildJsonObject {
put("amount", amountCents.toString())
put("currency", currency)
reason?.let { put("reason", it) }
}
val request = HttpRequest.newBuilder()
.uri(URI.create("https://api.paysera.com/merchant-order/integration/v1/orders/$orderId/refunds"))
.header("Authorization", "Bearer $accessToken")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody.toString()))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() != 201) {
throw Exception("Failed to create refund: ${response.body()}")
}
return Json.parseToJsonElement(response.body()).jsonObject
}
// Usage - amount in minor units (cents)
val refund = createRefund(
accessToken,
"order-uuid",
2500, // €25.00 in cents
"EUR",
"Customer requested refund"
)
println("Refund ID: ${refund["id"]}")
println("Status: ${refund["status"]}")
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
type RefundRequest struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
Reason string `json:"reason,omitempty"`
}
func createRefund(accessToken, orderId string, amountCents int64, currency, reason string) (map[string]interface{}, error) {
reqBody := RefundRequest{
Amount: fmt.Sprintf("%d", amountCents),
Currency: currency,
Reason: reason,
}
jsonData, _ := json.Marshal(reqBody)
url := fmt.Sprintf("https://api.paysera.com/merchant-order/integration/v1/orders/%s/refunds", orderId)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 201 {
return nil, fmt.Errorf("failed to create refund: %s", string(body))
}
var result map[string]interface{}
json.Unmarshal(body, &result)
return result, nil
}
// Usage - amount in minor units (cents)
func main() {
refund, _ := createRefund(
accessToken,
"order-uuid",
2500, // €25.00 in cents
"EUR",
"Customer requested refund",
)
fmt.Printf("Refund ID: %s\n", refund["id"])
fmt.Printf("Status: %s\n", refund["status"])
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
public class PayseraRefundClient
{
private readonly HttpClient _httpClient;
public PayseraRefundClient(string accessToken)
{
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
}
public async Task<JsonDocument> CreateRefundAsync(string orderId, long amountCents, string currency, string? reason = null)
{
var request = new Dictionary<string, object>
{
["amount"] = amountCents.ToString(),
["currency"] = currency
};
if (!string.IsNullOrEmpty(reason))
{
request["reason"] = reason;
}
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(
$"https://api.paysera.com/merchant-order/integration/v1/orders/{orderId}/refunds",
content);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Failed to create refund: {responseBody}");
}
return JsonDocument.Parse(responseBody);
}
}
// Usage - amount in minor units (cents)
var client = new PayseraRefundClient(accessToken);
var refund = await client.CreateRefundAsync(
"order-uuid",
2500, // €25.00 in cents
"EUR",
"Customer requested refund"
);
Console.WriteLine($"Refund ID: {refund.RootElement.GetProperty("id")}");
Console.WriteLine($"Status: {refund.RootElement.GetProperty("status")}");
PHP with SDK​
<?php
use Paysera\CheckoutSdk\SdkFacade;
/** @var SdkFacade $sdkFacade */
$refundsFacade = $sdkFacade->getRefundsFacade();
// Build refund request
$refundRequest = $refundsFacade->buildRefundOrderRequest([
'order_id' => 'order-uuid',
'amount' => 2500, // 25.00 EUR in cents
'currency' => 'EUR',
'reason' => 'Customer requested refund',
]);
// Process refund
$refundResponse = $refundsFacade->initiateRefundOrder($refundRequest);
echo "Refund ID: " . $refundResponse->getId();
echo "Status: " . $refundResponse->getStatus();
Partial Refunds​
You can process multiple partial refunds as long as the total doesn't exceed the original payment:
# Original payment: €100.00 (10000 cents)
# First partial refund: €30.00 (3000 cents)
curl -X POST https://api.paysera.com/merchant-order/integration/v1/orders/ORDER_ID/refunds \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": "3000",
"currency": "EUR",
"reason": "Partial refund for returned item"
}'
# Second partial refund: €20.00 (2000 cents)
curl -X POST https://api.paysera.com/merchant-order/integration/v1/orders/ORDER_ID/refunds \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": "2000",
"currency": "EUR",
"reason": "Partial refund for second returned item"
}'
# Remaining refundable: €50.00 (5000 cents)
Refund Webhooks​
When a refund status changes, you'll receive a webhook. Note that refund webhooks follow the same payload structure as other webhooks:
{
"event": {
"name": "order.paid",
"type": "order",
"timestamp": 1736437500
},
"order": {
"id": "a6f2b8e3-5e5f-47d9-b13f-87ed2db2938a",
"projectId": "01990618-2c90-7103-9169-752bdaac7a52",
"status": "paid",
"amount": 2500,
"currency": "EUR",
"reference": "ORDER-12345",
"amountPaid": 2500,
"balanceDue": 0,
"createdAt": 1736433270,
"updatedAt": 1736437500
}
}
Handle order status changes in your webhook handler:
<?php
$orderStatus = $data['order']['status'];
$reference = $data['order']['reference'];
switch ($orderStatus) {
case 'paid':
// Order fully paid - fulfill the order
updateOrderStatus($reference, 'paid');
break;
case 'pending_payment':
// Order awaiting payment
updateOrderStatus($reference, 'awaiting_payment');
break;
}
Error Responses​
Order Not Found​
{
"error": "not_found",
"message": "Order not found"
}
Order Not Completed​
{
"error": "invalid_state",
"message": "Cannot refund order in pending status"
}
Amount Exceeds Refundable​
{
"error": "validation_error",
"message": "Refund amount exceeds refundable amount",
"details": [
{
"field": "amount",
"message": "Maximum refundable amount is 15.00 EUR"
}
]
}
Currency Mismatch​
{
"error": "validation_error",
"message": "Currency must match original payment",
"details": [
{
"field": "amount.currency",
"message": "Original payment was in EUR"
}
]
}
Refund Timeline​
| Stage | Typical Duration |
|---|---|
| Refund initiated | Immediate |
| Processing | 1-3 business days |
| Funds returned to customer | 3-10 business days (varies by payment method) |
note
Refund processing times vary by payment method and banking institutions. Bank transfers typically take longer than card refunds.