# Gérer les erreurs

L’objet d’erreur, les sept types d’erreur, les codes que vous rencontrerez d’abord, et comment traiter chacun.

Quand une requête échoue, l’API répond un statut HTTP de `400` ou plus et un **objet d’erreur**.
Votre code décide quoi faire d’après le `type` et le `code` de l’erreur ; le `message` est pour les
personnes.

## L’objet d’erreur

```json title="404 Not Found" response
{
  "error": {
    "type": "invalid_request_error",
    "code": "resource_missing",
    "message": "No such payment_intent: 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'",
    "doc_url": "https://pay-docs.adpromax.com/errors#resource_missing",
    "request_id": "req_01k7d9x3f6g9h2j5k8m1n4p7qs"
  }
}
```

| Champ          | Ce qu’il contient                                                                                                                                                                  |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`         | L’un des sept types ci-dessous : la grande famille de l’échec.                                                                                                                     |
| `code`         | La raison précise, stable dans le temps : c’est sur lui qu’on branche.                                                                                                             |
| `decline_code` | Pour une carte refusée : la raison de la banque, comme `insufficient_funds`.                                                                                                       |
| `param`        | Le paramètre en cause, sous forme de chemin (`metadata.order_id`), quand il y en a un.                                                                                             |
| `message`      | Une explication pour une personne, dans la langue de votre requête (anglais, français ou arabe, d’après l’en-tête `Accept-Language`). Il peut changer : ne branchez jamais dessus. |
| `doc_url`      | La page qui explique le code.                                                                                                                                                      |
| `request_id`   | L’identifiant de la requête (`req_…`), aussi dans l’en-tête `Request-Id` de chaque réponse.                                                                                        |

## Types d’erreur

| `type`                  | Statut              | Ce qui s’est passé                                                                                                                                            | Que faire                                                                                                  |
| ----------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `invalid_request_error` | `400`, `404`, `409` | La requête est fausse : un paramètre manquant, inconnu ou invalide, un objet ou une adresse qui n’existe pas, une action que l’état de l’objet ne permet pas. | Corrigez la requête. La renvoyer telle quelle échoue de la même façon.                                     |
| `authentication_error`  | `401`               | Pas de clé, ou une clé qui ne marche pas.                                                                                                                     | Vérifiez la clé et son mode.                                                                               |
| `permission_error`      | `403`               | La clé marche mais ne peut pas faire ceci.                                                                                                                    | Voyez le code : une clé de production avant l’activation, un assistant de test avec une clé de production… |
| `card_error`            | `402`               | Une carte a été refusée.                                                                                                                                      | Montrez un message à votre client et laissez-le essayer une autre carte.                                   |
| `idempotency_error`     | `400`, `409`        | Une `Idempotency-Key` a servi pour une autre requête, ou sa première requête s’exécute encore.                                                                | Voir [Idempotence](https://pay-docs.adpromax.com/fr/api-reference/idempotency).                                                            |
| `rate_limit_error`      | `429`               | Trop de requêtes en peu de temps.                                                                                                                             | Attendez le délai `Retry-After`, puis réessayez. Voir [Limites de débit](https://pay-docs.adpromax.com/fr/api-reference/rate-limits).      |
| `api_error`             | `500`, `503`        | Quelque chose a échoué de notre côté, ou est indisponible un instant.                                                                                         | Réessayez avec la même `Idempotency-Key` ; si cela persiste, écrivez au support avec le `request_id`.      |

## Les codes que vous rencontrerez d’abord

### Requêtes

| `code`                              | Ce qui s’est passé                                                                                                                                                                                                                                                                        |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parameter_missing`                 | Un paramètre obligatoire est absent ; `param` le nomme.                                                                                                                                                                                                                                   |
| `parameter_unknown`                 | Un paramètre que la requête n’accepte pas ; `param` le nomme. Vérifiez son orthographe.                                                                                                                                                                                                   |
| `parameter_invalid`                 | Un paramètre a un mauvais type ou une mauvaise valeur ; `param` le nomme. Un champ texte qui ressemble à un numéro de carte est refusé ainsi aussi : les numéros de carte ne passent jamais par l’API. Une liste demandée avec `starting_after` et `ending_before` à la fois l’est aussi. |
| `resource_missing`                  | Aucun objet avec cet identifiant, dans ce mode. S’il existe dans l’autre mode, le message le dit : vous avez sans doute utilisé la mauvaise clé.                                                                                                                                          |
| `unrecognized_request_url`          | Aucune route de l’API n’a cette méthode et cette adresse ; le message nomme les deux. Vérifiez le chemin et la méthode HTTP.                                                                                                                                                              |
| `api_version_invalid`               | L’en-tête `X-AdProMaxPay-Version`, ou l’`api_version` d’un endpoint de webhook, nomme une version qui n’existe pas. Le message liste celles qui existent.                                                                                                                                 |
| `amount_too_small`                  | Le montant est sous le minimum du moyen de paiement : 100,00 MAD pour les espèces, 1,00 MAD pour une carte.                                                                                                                                                                               |
| `amount_too_large`                  | Un remboursement dépasse ce qui reste remboursable sur le paiement.                                                                                                                                                                                                                       |
| `payment_method_unavailable`        | Le moyen de paiement ne peut pas servir pour votre compte dans ce mode.                                                                                                                                                                                                                   |
| `payment_intent_unexpected_state`   | Le statut de l’intention de paiement ne permet pas cette action, comme confirmer un paiement déjà réussi.                                                                                                                                                                                 |
| `payment_intent_busy`               | Une autre requête confirme ou modifie ce paiement en ce moment. Rien n’a été modifié : renvoyez la requête dans un instant.                                                                                                                                                               |
| `charge_unexpected_state`           | Le statut de la tentative ne permet pas cet assistant de test, comme terminer une tentative déjà réussie.                                                                                                                                                                                 |
| `refund_unsupported_payment_method` | Un paiement en espèces ne peut pas être remboursé.                                                                                                                                                                                                                                        |
| `url_not_allowed`                   | Une adresse de webhook qui n’est pas en `https`, porte un nom d’utilisateur ou un mot de passe, n’est pas publique, ou dont l’hôte ne se résout pas.                                                                                                                                      |
| `webhook_endpoint_limit_reached`    | L’organisation a déjà 16 endpoints de webhook dans ce mode.                                                                                                                                                                                                                               |
| `webhook_endpoints_busy`            | Deux modifications de vos endpoints se sont croisées (`409`). Renvoyez la requête.                                                                                                                                                                                                        |
| `idempotency_key_missing`           | Une requête qui fait circuler de l’argent a été envoyée sans `Idempotency-Key`.                                                                                                                                                                                                           |
| `invalid_request`                   | Une requête refusée sans code plus précis ; le message dit pourquoi.                                                                                                                                                                                                                      |

### Clés et permissions

| `code`                    | Ce qui s’est passé                                                                  |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `api_key_missing`         | La requête ne portait aucune clé.                                                   |
| `api_key_invalid`         | La clé n’existe pas. Vérifiez qu’il ne manque pas un caractère.                     |
| `api_key_revoked`         | La clé a été révoquée. Utilisez-en une autre.                                       |
| `api_key_expired`         | La clé a été renouvelée et son chevauchement est terminé. Utilisez la nouvelle clé. |
| `secret_key_required`     | Une clé publiable a servi là où il faut une clé secrète.                            |
| `live_mode_not_activated` | Une clé de production a servi avant l’activation du compte.                         |
| `account_inactive`        | Le compte est suspendu ou fermé.                                                    |
| `test_mode_only`          | Un assistant de test a été appelé avec une clé de production.                       |
| `authentication_required` | Un `401` sans code plus précis ; le message dit pourquoi.                           |
| `permission_denied`       | Un `403` sans code plus précis ; le message dit pourquoi.                           |

### Paiements, idempotence et limites

| `code`                    | Ce qui s’est passé                                                                  |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `card_declined`           | La carte a été refusée ; `decline_code` peut dire pourquoi.                         |
| `idempotency_key_reused`  | Cette `Idempotency-Key` a servi pour une autre requête.                             |
| `idempotency_in_progress` | La première requête avec cette `Idempotency-Key` s’exécute encore.                  |
| `idempotency_error`       | Un refus lié à l’`Idempotency-Key` sans code plus précis ; le message dit pourquoi. |
| `rate_limit_exceeded`     | Trop de requêtes ; le message nomme la limite atteinte et l’attente.                |
| `test_daily_cap_reached`  | Le plafond quotidien du mode test est atteint, jusqu’à minuit, heure du Maroc.      |
| `api_error`               | Une erreur de notre côté.                                                           |

Un paiement que votre client fait sur la page hébergée ne fait pas échouer votre appel d’API quand
la carte est refusée : votre client le voit et peut réessayer, et vous voyez la raison dans le
`last_payment_error` de l’intention de paiement et dans l’événement `payment_intent.payment_failed`.
Les raisons sont listées dans [Le fonctionnement des paiements](https://pay-docs.adpromax.com/fr/payments/how-payments-work).

## Lire une erreur dans votre code

```bash
curl -i https://pay-api.adpromax.com/v1/payment_intents/pi_01k7d9x2c5v8b1n4m7q0w3e6rt \
  -u "$APX_SECRET_KEY:"
```

```js title="errors.mjs"
const id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}`, {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
if (!response.ok) {
  const { error } = await response.json()
  if (error.code === 'resource_missing')
    console.log('No such payment here:', error.message)
  else
    console.log(error.type, error.code, error.request_id)
}
```

```ts title="errors.ts"
interface ApiError {
  error: {
    type: string
    code: string
    message: string
    request_id: string
    doc_url: string
    param?: string
  }
}

const id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}`, {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  if (error.code === 'resource_missing')
    console.log('No such payment here:', error.message)
  else
    console.log(error.type, error.code, error.request_id)
}
```

```python title="errors.py"
import os

import requests

payment_intent_id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt"
response = requests.get(
    f"https://pay-api.adpromax.com/v1/payment_intents/{payment_intent_id}",
    auth=(os.environ["APX_SECRET_KEY"], ""),
)
if not response.ok:
    error = response.json()["error"]
    if error["code"] == "resource_missing":
        print("No such payment here:", error["message"])
    else:
        print(error["type"], error["code"], error["request_id"])
```

```php title="errors.php"
<?php
$id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt';
$ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents/' . $id);
curl_setopt($ch, CURLOPT_USERPWD, getenv('APX_SECRET_KEY') . ':');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
    $error = $body['error'];
    if ($error['code'] === 'resource_missing') {
        echo 'No such payment here: ', $error['message'], PHP_EOL;
    } else {
        echo $error['type'], ' ', $error['code'], ' ', $error['request_id'], PHP_EOL;
    }
}
```

```ruby title="errors.rb"
require 'json'
require 'net/http'

id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
uri = URI("https://pay-api.adpromax.com/v1/payment_intents/#{id}")
request = Net::HTTP::Get.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
unless response.is_a?(Net::HTTPSuccess)
  error = JSON.parse(response.body)['error']
  if error['code'] == 'resource_missing'
    puts "No such payment here: #{error['message']}"
  else
    puts "#{error['type']} #{error['code']} #{error['request_id']}"
  end
end
```

```go title="errors.go"
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

type APIError struct {
	Error struct {
		Type      string `json:"type"`
		Code      string `json:"code"`
		Message   string `json:"message"`
		RequestID string `json:"request_id"`
	} `json:"error"`
}

func main() {
	id := "pi_01k7d9x2c5v8b1n4m7q0w3e6rt"
	req, err := http.NewRequest(http.MethodGet,
		"https://pay-api.adpromax.com/v1/payment_intents/"+id, 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()

	if res.StatusCode >= 400 {
		var failure APIError
		if err := json.NewDecoder(res.Body).Decode(&failure); err != nil {
			log.Fatal(err)
		}
		if failure.Error.Code == "resource_missing" {
			fmt.Println("No such payment here:", failure.Error.Message)
		} else {
			fmt.Println(failure.Error.Type, failure.Error.Code,
				failure.Error.RequestID)
		}
	}
}
```

```java title="Errors.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Errors {
    // The JDK has no JSON parser: a small regex reads one field of the error.
    static String field(String json, String name) {
        Matcher match = Pattern
            .compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"")
            .matcher(json);
        return match.find() ? match.group(1) : "";
    }

    public static void main(String[] args) throws Exception {
        String id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/payment_intents/" + id))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .GET()
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            String error = response.body();
            if (field(error, "code").equals("resource_missing")) {
                System.out.println(
                    "No such payment here: " + field(error, "message"));
            } else {
                System.out.println(field(error, "type") + " "
                    + field(error, "code") + " " + field(error, "request_id"));
            }
        }
    }
}
```

```csharp title="Errors.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 = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
using var response = await http.GetAsync($"https://pay-api.adpromax.com/v1/payment_intents/{id}");
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    var error = failure!.Error;
    if (error.Code == "resource_missing")
        Console.WriteLine($"No such payment here: {error.Message}");
    else
        Console.WriteLine($"{error.Type} {error.Code} {error.RequestId}");
}

record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Type, string Code, string Message, string RequestId);
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
use serde::Deserialize;

#[derive(Deserialize)]
struct ApiError {
    error: ErrorBody,
}

#[derive(Deserialize)]
struct ErrorBody {
    r#type: String,
    code: String,
    message: String,
    request_id: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
    let response = reqwest::blocking::Client::new()
        .get(format!("https://pay-api.adpromax.com/v1/payment_intents/{id}"))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .send()?;
    if !response.status().is_success() {
        let ApiError { error } = response.json()?;
        if error.code == "resource_missing" {
            println!("No such payment here: {}", error.message);
        } else {
            println!("{} {} {}", error.r#type, error.code, error.request_id);
        }
    }
    Ok(())
}
```

## Identifiants de requête

Chaque réponse, succès ou échec, porte un en-tête `Request-Id` (`req_…`). Les journaux de requêtes
du tableau de bord retrouvent une requête par lui, avec ce que vous avez envoyé et ce que l’API a
répondu. Gardez-le dans vos propres journaux, et donnez-le quand vous écrivez au support : c’est le
chemin le plus court vers la réponse.
