# Remboursements

Rembourser un paiement par carte en totalité ou en partie, le temps que prend un remboursement, et pourquoi un paiement en espèces n’est pas remboursé en espèces.

Un **remboursement** rend de l’argent à votre client pour un paiement réussi. Les paiements par carte
peuvent être remboursés, en totalité ou en partie. Les paiements en espèces ne le peuvent pas : voir
[Paiements en espèces](#paiements-en-espèces) plus bas.

* Seul un paiement `succeeded` peut être remboursé.
* Vous pouvez faire plusieurs remboursements partiels, tant que leur total ne dépasse pas
  l’`amount_received` du paiement. Un remboursement supérieur à ce qui reste est refusé avec
  `amount_too_large`.
* Un paiement en espèces est refusé avec `refund_unsupported_payment_method`, en mode test comme en
  production.

## Rembourser un paiement par carte

Créez un remboursement avec `POST /v1/refunds`. L’en-tête `Idempotency-Key` est obligatoire, comme
sur chaque requête qui déplace de l’argent : un remboursement envoyé deux fois avec la même clé n’est
fait qu’une fois.

| Paramètre        | Ce qu’il contient                                                                                       |
| ---------------- | ------------------------------------------------------------------------------------------------------- |
| `payment_intent` | Le paiement à rembourser (`pi_…`). Ou envoyez `charge`, sa tentative réussie (`ch_…`).                  |
| `amount`         | Facultatif. En centimes. Sans lui, tout ce qui n’a pas encore été remboursé.                            |
| `reason`         | Facultatif. `requested_by_customer`, `duplicate` ou `fraudulent`.                                       |
| `metadata`       | Facultatif. Vos propres références, comme un numéro de retour : voir [Métadonnées](https://pay-docs.adpromax.com/fr/payments/metadata). |

La réponse est le remboursement, d’abord `pending`.

```bash
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" }
  }'
```

```js title="refund.mjs"
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)
```

```ts title="refund.ts"
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)
```

```python title="refund.py"
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 title="refund.php"
<?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;
```

```ruby title="refund.rb"
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']}"
```

```go title="refund.go"
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)
}
```

```java title="Refund.java"
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());
    }
}
```

```csharp title="Refund.cs"
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);
```

```rust title="main.rs"
// 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(())
}
```

```json title="200 OK" response
{
  "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
}
```

## La vie d’un remboursement

Les statuts d’un remboursement:

- `pending` → `succeeded`: exécuté
- `pending` → `failed`: refusé
- `pending` → `canceled`: vous annulez

Un remboursement est `pending` jusqu’à son exécution, puis `succeeded`. Il finit `failed` si le
réseau carte le refuse, et `canceled` si vous l’annulez avant.

* **En mode test**, un remboursement réussit en quelques secondes.
* **En production**, un remboursement par carte peut être **traité manuellement** : notre équipe
  l’exécute auprès du réseau carte. Il reste `pending` jusqu’à ce que ce soit fait, ce qui peut
  prendre quelques jours ouvrés. Une fois `succeeded`, la banque de votre client prend encore son
  propre temps pour faire apparaître l’argent sur sa carte.

Les statuts sont les mêmes dans les deux cas : votre code ne change pas d’un mode à l’autre.

## Annuler un remboursement

Un remboursement `pending` qui n’a pas encore été exécuté peut être annulé avec
`POST /v1/refunds/{id}/cancel`. Il devient `canceled`, et son montant peut être remboursé de nouveau
plus tard.

```bash
curl -X POST https://pay-api.adpromax.com/v1/refunds/re_01k7e7w3y6a9c2e5g8j1m4p7rt/cancel \
  -u "$APX_SECRET_KEY:"
```

```js title="cancel-refund.mjs"
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) // canceled
```

```ts title="cancel-refund.ts"
interface 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) // canceled
```

```python title="cancel_refund.py"
import 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 title="cancel_refund.php"
<?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; // canceled
```

```ruby title="cancel_refund.rb"
require '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'] # canceled
```

```go title="cancel_refund.go"
package 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
}
```

```java title="CancelRefund.java"
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());
    }
}
```

```csharp title="CancelRefund.cs"
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);
```

```rust title="main.rs"
// 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(())
}
```

## Événements et soldes

| Événement         | Quand                                                              |
| ----------------- | ------------------------------------------------------------------ |
| `refund.created`  | Un remboursement a été créé.                                       |
| `refund.updated`  | Un remboursement a changé de statut, par exemple vers `succeeded`. |
| `refund.failed`   | Un remboursement a échoué.                                         |
| `charge.refunded` | Une tentative a été remboursée, en totalité ou en partie.          |

* L’`amount_refunded` de l’intention de paiement augmente quand un remboursement **réussit**.
  L’intention de paiement reste `succeeded`, même remboursée en totalité.
* Un remboursement échoué ou annulé libère son montant : vous pouvez le rembourser de nouveau.
* Un remboursement réussi est pris sur votre solde ; il apparaît comme une ligne de remboursement
  sous **Soldes**. Nos frais sur le paiement ne sont pas rendus : un paiement remboursé en totalité
  laisse votre solde plus bas de ces frais.

## Depuis le tableau de bord

Sur la page d’un paiement, **Rembourser** ouvre un formulaire :

* le montant, prérempli avec ce qui reste à rembourser, avec *Total* et *Partiel* ;
* une raison : doublon, fraude, demande du client, ou autre avec une note ;
* en production, une dernière confirmation avant l’envoi du remboursement.

La page **Remboursements** liste chaque remboursement avec son statut, son paiement et son auteur.

Qui peut rembourser sur le tableau de bord dépend du rôle : Propriétaire, Administrateur, Finance et
Support le peuvent. Le Support peut rembourser jusqu’à un plafond par remboursement, 1 000,00 MAD par
défaut, que le formulaire affiche avant la saisie. Pour les autres rôles, le bouton est désactivé et
dit pourquoi. Voir [Équipe et rôles](https://pay-docs.adpromax.com/fr/account/team-and-roles). Une clé d’API n’est pas limitée par
les rôles : elle agit pour toute l’organisation.

## Paiements en espèces

Un paiement en espèces réussi **ne peut pas être remboursé par AdProMaxPay** : ni en espèces, ni sur
une carte.

**Pourquoi.** Votre client a remis l’argent en espèces chez un agent. Il n’y a ni carte ni compte sur
lesquels le rendre.

**Ce qui se passe.** Le paiement reste `succeeded` et son montant reste dans votre solde. Sur le
tableau de bord, **Rembourser** est désactivé sur un paiement en espèces et dit pourquoi ; l’API
répond `refund_unsupported_payment_method`, en mode test comme en production.

**Ce que vous faites.** Réglez-le directement avec votre client : un virement bancaire, un avoir, ou
des espèces dans votre magasin. Enregistrez-le dans vos propres comptes. Annoncez votre politique à
vos clients avant qu’ils choisissent les espèces.

Un code espèces qui n’a pas encore été payé n’est pas remboursé mais **annulé** : voir
[Annuler un code dont vous ne voulez plus](https://pay-docs.adpromax.com/fr/payments/cash-payments#annuler-un-code-dont-vous-ne-voulez-plus).
