Payments
Refunds
Refund a card payment in full or in part, how long a refund takes, and why a cash payment is not refunded in cash.
On this page
A refund gives money back to your customer for a payment that succeeded. Card payments can be refunded, in full or in part. Cash payments cannot: see Cash payments below.
- Only a
succeededpayment can be refunded. - You can make several partial refunds, as long as their total stays within the payment's
amount_received. A refund over what remains is refused withamount_too_large. - A cash payment is refused with
refund_unsupported_payment_method, in test mode as in live mode.
Refund a card payment
Create a refund with POST /v1/refunds. The Idempotency-Key header is required, as on every
request that moves money: a refund sent twice with the same key is made once.
| Parameter | What it holds |
|---|---|
payment_intent | The payment to refund (pi_…). Or send charge, its succeeded attempt (ch_…). |
amount | Optional. In centimes. Without it, everything not yet refunded. |
reason | Optional. requested_by_customer, duplicate or fraudulent. |
metadata | Optional. Your own references, such as a return number: see Metadata. |
The answer is the refund, pending at first.
curl https://pay-api.adpromax.com/v1/refunds \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-order-1042-1" \
-d '{
"payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
"amount": 10000,
"reason": "requested_by_customer",
"metadata": { "return_id": "R-311" }
}'const response = await fetch('https://pay-api.adpromax.com/v1/refunds', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'refund-order-1042-1',
},
body: JSON.stringify({
payment_intent: 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
amount: 10000,
reason: 'requested_by_customer',
metadata: { return_id: 'R-311' },
}),
})
const refund = await response.json()
if (!response.ok)
throw new Error(refund.error.message)
console.log(refund.id, refund.status)interface Refund {
id: string
object: 'refund'
amount: number
currency: string
payment_intent: string
reason: string | null
status: string
metadata: Record<string, string>
}
interface ApiError {
error: { type: string, code: string, message: string, request_id: string }
}
const response = await fetch('https://pay-api.adpromax.com/v1/refunds', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'refund-order-1042-1',
},
body: JSON.stringify({
payment_intent: 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
amount: 10000,
reason: 'requested_by_customer',
metadata: { return_id: 'R-311' },
}),
})
if (!response.ok) {
const { error } = (await response.json()) as ApiError
throw new Error(`${error.code}: ${error.message}`)
}
const refund = (await response.json()) as Refund
console.log(refund.id, refund.status)import os
import requests
response = requests.post(
"https://pay-api.adpromax.com/v1/refunds",
auth=(os.environ["APX_SECRET_KEY"], ""),
headers={"Idempotency-Key": "refund-order-1042-1"},
json={
"payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
"amount": 10000,
"reason": "requested_by_customer",
"metadata": {"return_id": "R-311"},
},
)
refund = response.json()
response.raise_for_status()
print(refund["id"], refund["status"])<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/refunds');
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Idempotency-Key: refund-order-1042-1',
],
CURLOPT_POSTFIELDS => json_encode([
'payment_intent' => 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
'amount' => 10000,
'reason' => 'requested_by_customer',
'metadata' => ['return_id' => 'R-311'],
]),
CURLOPT_RETURNTRANSFER => true,
]);
$refund = json_decode(curl_exec($ch), true);
echo $refund['id'], ' ', $refund['status'], PHP_EOL;require 'json'
require 'net/http'
uri = URI('https://pay-api.adpromax.com/v1/refunds')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'refund-order-1042-1'
request.body = JSON.generate(
payment_intent: 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
amount: 10000,
reason: 'requested_by_customer',
metadata: { return_id: 'R-311' }
)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
refund = JSON.parse(response.body)
raise refund.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)
puts "#{refund['id']} #{refund['status']}"package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type Refund struct {
ID string `json:"id"`
Amount int64 `json:"amount"`
PaymentIntent string `json:"payment_intent"`
Status string `json:"status"`
}
type APIError struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func main() {
body, err := json.Marshal(map[string]any{
"payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
"amount": 10000,
"reason": "requested_by_customer",
"metadata": map[string]string{"return_id": "R-311"},
})
if err != nil {
log.Fatal(err)
}
req, err := http.NewRequest(http.MethodPost,
"https://pay-api.adpromax.com/v1/refunds", bytes.NewReader(body))
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "refund-order-1042-1")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode >= 400 {
var apiErr APIError
json.NewDecoder(res.Body).Decode(&apiErr)
log.Fatal(apiErr.Error.Message)
}
var refund Refund
if err := json.NewDecoder(res.Body).Decode(&refund); err != nil {
log.Fatal(err)
}
fmt.Println(refund.ID, refund.Status)
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Refund {
public static void main(String[] args) throws Exception {
String body = """
{
"payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
"amount": 10000,
"reason": "requested_by_customer",
"metadata": { "return_id": "R-311" }
}
""";
HttpRequest request = HttpRequest.newBuilder(
URI.create("https://pay-api.adpromax.com/v1/refunds"))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", "refund-order-1042-1")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IllegalStateException(response.body());
}
// The refund as JSON: read its id and status with a JSON library.
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
using var request = new HttpRequestMessage(
HttpMethod.Post, "https://pay-api.adpromax.com/v1/refunds");
request.Headers.Add("Idempotency-Key", "refund-order-1042-1");
request.Content = JsonContent.Create(new
{
payment_intent = "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
amount = 10000,
reason = "requested_by_customer",
metadata = new { return_id = "R-311" },
});
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
throw new InvalidOperationException(failure?.Error.Message);
}
var refund = await response.Content.ReadFromJsonAsync<Refund>(json);
Console.WriteLine($"{refund!.Id} {refund.Status}");
record Refund(string Id, long Amount, string PaymentIntent, string Status);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct Refund {
id: String,
status: String,
}
#[derive(Deserialize)]
struct ApiError {
error: ErrorBody,
}
#[derive(Deserialize)]
struct ErrorBody {
message: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let response = reqwest::blocking::Client::new()
.post("https://pay-api.adpromax.com/v1/refunds")
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.header("Idempotency-Key", "refund-order-1042-1")
.json(&json!({
"payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
"amount": 10000,
"reason": "requested_by_customer",
"metadata": { "return_id": "R-311" }
}))
.send()?;
if !response.status().is_success() {
let failure: ApiError = response.json()?;
return Err(failure.error.message.into());
}
let refund: Refund = response.json()?;
println!("{} {}", refund.id, refund.status);
Ok(())
}{
"id": "re_01k7e7w3y6a9c2e5g8j1m4p7rt",
"object": "refund",
"amount": 10000,
"currency": "mad",
"payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
"charge": "ch_01k7e7v3c6e9g2j5m8p1r4t7wb",
"status": "pending",
"reason": "requested_by_customer",
"metadata": { "return_id": "R-311" },
"created": 1790863800,
"livemode": false
}The life of a refund
- pending → succeeded: carried out
- pending → failed: refused
- pending → canceled: you cancel
A refund is pending until it is carried out, then succeeded. It ends failed if the card network
refuses it, and canceled if you cancel it first.
- In test mode, a refund succeeds within seconds.
- In live mode, a card refund may be processed manually: our team carries it out with the
card network. It stays
pendinguntil that is done, which can take a few business days. Once it issucceeded, your customer's bank still takes its own time to show the money on their card.
The statuses are the same either way, so your code does not change between modes.
Cancel a refund
A pending refund that has not been carried out yet can be canceled with
POST /v1/refunds/{id}/cancel. It becomes canceled, and its amount can be refunded again later.
curl -X POST https://pay-api.adpromax.com/v1/refunds/re_01k7e7w3y6a9c2e5g8j1m4p7rt/cancel \
-u "$APX_SECRET_KEY:"const id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/refunds/${id}/cancel`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const refund = await response.json()
console.log(refund.status) // canceledinterface Refund {
id: string
object: 'refund'
status: string
}
const id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/refunds/${id}/cancel`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const refund = (await response.json()) as Refund
console.log(refund.status) // canceledimport os
import requests
refund_id = "re_01k7e7w3y6a9c2e5g8j1m4p7rt"
response = requests.post(
f"https://pay-api.adpromax.com/v1/refunds/{refund_id}/cancel",
auth=(os.environ["APX_SECRET_KEY"], ""),
)
print(response.json()["status"]) # canceled<?php
$id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt';
$ch = curl_init("https://pay-api.adpromax.com/v1/refunds/{$id}/cancel");
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
]);
$refund = json_decode(curl_exec($ch), true);
echo $refund['status'], PHP_EOL; // canceledrequire 'json'
require 'net/http'
id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt'
uri = URI("https://pay-api.adpromax.com/v1/refunds/#{id}/cancel")
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
puts JSON.parse(response.body)['status'] # canceledpackage main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type Refund struct {
ID string `json:"id"`
Status string `json:"status"`
}
func main() {
id := "re_01k7e7w3y6a9c2e5g8j1m4p7rt"
req, err := http.NewRequest(http.MethodPost,
"https://pay-api.adpromax.com/v1/refunds/"+id+"/cancel", nil)
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var refund Refund
if err := json.NewDecoder(res.Body).Decode(&refund); err != nil {
log.Fatal(err)
}
fmt.Println(refund.Status) // canceled
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class CancelRefund {
public static void main(String[] args) throws Exception {
String id = "re_01k7e7w3y6a9c2e5g8j1m4p7rt";
HttpRequest request = HttpRequest.newBuilder(
URI.create("https://pay-api.adpromax.com/v1/refunds/" + id + "/cancel"))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// The refund as JSON: its status is now "canceled".
System.out.println(response.body());
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var id = "re_01k7e7w3y6a9c2e5g8j1m4p7rt";
using var response = await http.PostAsync(
$"https://pay-api.adpromax.com/v1/refunds/{id}/cancel", null);
var refund = await response.Content.ReadFromJsonAsync<Refund>(json);
Console.WriteLine(refund!.Status); // canceled
record Refund(string Id, string Status);// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde = { version = "1", features = ["derive"] }
use serde::Deserialize;
#[derive(Deserialize)]
struct Refund {
status: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let id = "re_01k7e7w3y6a9c2e5g8j1m4p7rt";
let refund: Refund = reqwest::blocking::Client::new()
.post(format!("https://pay-api.adpromax.com/v1/refunds/{id}/cancel"))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.send()?
.json()?;
println!("{}", refund.status); // canceled
Ok(())
}Events and balances
| Event | When |
|---|---|
refund.created | A refund was created. |
refund.updated | A refund changed status, for example to succeeded. |
refund.failed | A refund failed. |
charge.refunded | An attempt was refunded, in full or in part. |
- The payment intent's
amount_refundedgrows when a refund succeeds. The payment intent stayssucceeded, even when it is refunded in full. - A failed or canceled refund frees its amount: you can refund it again.
- A succeeded refund is taken from your balance; it shows as a refund line under Balances. Our fee on the payment is not given back: a payment refunded in full leaves your balance lower by that fee.
From the dashboard
On a payment's page, Refund opens a form:
- the amount, prefilled with what remains to refund, with Full and Partial;
- a reason: duplicate, fraudulent, requested by customer, or other with a note;
- in live mode, a last confirmation before the refund is sent.
The Refunds page lists every refund with its status, its payment and who made it.
Who can refund on the dashboard depends on the role: Owner, Administrator, Finance and Support can. Support can refund up to a ceiling per refund, 1,000.00 MAD by default, which the form shows before you type. For the other roles, the button is disabled and says why. See Team and roles. An API key is not limited by roles: it acts for the whole organization.
Cash payments
A succeeded cash payment cannot be refunded through AdProMaxPay: neither in cash, nor to a card.
Why. Your customer handed the money over in cash at an agent. There is no card and no account to send it back to.
What happens. The payment stays succeeded and its amount stays in your balance. On the
dashboard, Refund is disabled on a cash payment and says why; the API answers
refund_unsupported_payment_method, in test mode as in live mode.
What you do. Settle it with your customer directly: a bank transfer, a store credit, or cash at your shop. Record it in your own books. Tell your customers your policy before they choose cash.
A cash code that has not been paid yet is not refunded but canceled: see Cancel a code you no longer want.