Paiements

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.

Ouvrir en Markdown
Sur cette page
  1. Rembourser un paiement par carte
  2. La vie d’un remboursement
  3. Annuler un remboursement
  4. Événements et soldes
  5. Depuis le tableau de bord
  6. Paiements 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 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ètreCe qu’il contient
payment_intentLe paiement à rembourser (pi_…). Ou envoyez charge, sa tentative réussie (ch_…).
amountFacultatif. En centimes. Sans lui, tout ce qui n’a pas encore été remboursé.
reasonFacultatif. requested_by_customer, duplicate ou fraudulent.
metadataFacultatif. Vos propres références, comme un numéro de retour : voir Métadonnées.

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

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
refund.mjsrefund.tsrefund.pyrefund.phprefund.rbrefund.goRefund.javaRefund.csmain.rs
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(())
}
Réponse200 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
}

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.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
cancel-refund.mjscancel-refund.tscancel_refund.pycancel_refund.phpcancel_refund.rbcancel_refund.goCancelRefund.javaCancelRefund.csmain.rs
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) // canceled
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
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
$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
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
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
}
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(())
}

Événements et soldes

ÉvénementQuand
refund.createdUn remboursement a été créé.
refund.updatedUn remboursement a changé de statut, par exemple vers succeeded.
refund.failedUn remboursement a échoué.
charge.refundedUne 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. 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.