Référence
Erreurs
Toutes les erreurs de l'API ont la même forme et portent un code stable, conçu pour être testé par programme. Le statut HTTP dit la catégorie ; le code dit exactement quoi.
Forme d'une erreur
{
"success": false,
"error": {
"code": "invalid_request",
"message": "Les données envoyées sont invalides.",
"details": {
"amount": ["Le champ amount est obligatoire."],
"phone_number": ["Le numéro doit être au format international sans espaces (ex. 22997000000)."]
}
}
}
| Champ | Description |
|---|---|
error.code | Identifiant stable de l'erreur. C'est sur lui que votre code doit brancher. |
error.message | Phrase en français destinée à un humain. Peut être reformulée sans préavis : ne la testez jamais. |
error.details | Présent uniquement sur les erreurs de validation. Un objet champ → [messages]. |
Codes d'erreur
| Code | HTTP | Signification | Que faire |
|---|---|---|---|
unauthorized |
401 | Clé API absente, invalide, ou en-tête mal formé. | Vérifiez l'en-tête Authorization et que vous envoyez la clé secrète. Ne réessayez pas en boucle. |
key_expired |
401 | La clé a dépassé sa date d'expiration. | Régénérez-la depuis le tableau de bord et redéployez. |
merchant_inactive |
403 | Le compte marchand n'est pas actif (KYC en cours, compte suspendu). Ne concerne que le live. | Terminez votre KYC, ou contactez le support. Vos clés sk_test_… restent utilisables entre-temps. |
invalid_request |
422 | Corps mal formé, champ manquant, valeur hors bornes, opérateur inconnu. | Lisez details, corrigez la requête. Réessayer à l'identique ne servira à rien. |
payment_failed |
402 | L'opérateur a refusé le paiement (solde insuffisant, numéro inconnu, service indisponible). | Le paiement est enregistré en failed. Proposez au client un autre moyen ou un nouvel essai. |
not_found |
404 | La ressource n'existe pas, ou n'appartient pas à votre compte. | Vérifiez la référence. Une ressource d'un autre marchand est indistinguable d'une inexistante. |
conflict |
409 | L'état actuel interdit l'opération : remboursement d'un paiement non validé, remboursement en double. | Relisez l'état de la ressource avant de réessayer. |
method_not_allowed |
405 | Le verbe HTTP n'est pas accepté sur cette route. | Erreur d'intégration : comparez avec la référence. |
rate_limited |
429 | Plus de 120 requêtes en une minute depuis votre adresse IP. | Attendez le délai indiqué par Retry-After, puis réessayez avec un intervalle croissant. |
server_error |
500 | Incident de notre côté. | Réessayez avec un intervalle croissant. Si cela persiste, contactez le support avec la référence concernée. |
Erreurs de validation
Une requête mal formée renvoie un 422 avec le code invalid_request et un
objet details. Les clés y sont les noms des champs, en notation pointée pour les
objets imbriqués :
{
"success": false,
"error": {
"code": "invalid_request",
"message": "Les données envoyées sont invalides.",
"details": {
"amount": ["Le champ amount est obligatoire."],
"customer.email": ["Le champ customer.email doit être une adresse e-mail valide."]
}
}
}
Certaines erreurs métier utilisent le même code sans venir de la validation de formulaire :
montant hors des bornes de l'opérateur, OTP manquant, identité du porteur absente pour une carte.
Elles portent alors un message explicite et, quand c'est utile, un
details ciblé.
Que réessayer
| Situation | Réessayer ? |
|---|---|
429, 500, timeout réseau | Oui, avec un délai croissant (1 s, 2 s, 4 s, 8 s…). |
401, 403, 404, 405, 422 | Non. La requête ne passera jamais telle quelle. |
402 payment_failed | Non automatiquement : c'est une décision du client, pas un incident technique. |
409 conflict | Non sans avoir relu l'état de la ressource. |
Un timeout ne veut pas dire que le paiement n'a pas été créé : la requête a pu aboutir et
seule la réponse s'est perdue. Ne rejouez un POST /payments que si vous aviez
fourni un external_id : MivaaPay renverra alors le paiement existant au lieu
d'en créer un second. Sans external_id, vous débitez le client deux fois.
Traiter les erreurs proprement
$response = Http::withToken(env('MIVAAPAY_SECRET_KEY'))
->post('https://api.mivaapay.com/api/v1/payments', $payload);
$body = $response->json();
if (!($body['success'] ?? false)) {
$code = $body['error']['code'] ?? 'server_error';
return match ($code) {
'invalid_request' => $this->afficherErreursChamps($body['error']['details'] ?? []),
'payment_failed' => $this->proposerUnAutreMoyen($body['error']['message']),
'rate_limited' => $this->reprogrammer($response->header('Retry-After')),
default => $this->incidentTechnique($code),
};
}
const res = await fetch('https://api.mivaapay.com/api/v1/payments', options);
const body = await res.json();
if (!body.success) {
switch (body.error.code) {
case 'invalid_request':
return afficherErreursChamps(body.error.details ?? {});
case 'payment_failed':
return proposerUnAutreMoyen(body.error.message);
case 'rate_limited':
return reprogrammer(res.headers.get('Retry-After'));
default:
return incidentTechnique(body.error.code);
}
}
res = requests.post('https://api.mivaapay.com/api/v1/payments', json=payload,
headers=headers, timeout=30)
body = res.json()
if not body.get('success'):
code = body['error']['code']
if code == 'invalid_request':
return afficher_erreurs_champs(body['error'].get('details', {}))
if code == 'payment_failed':
return proposer_un_autre_moyen(body['error']['message'])
if code == 'rate_limited':
return reprogrammer(res.headers.get('Retry-After'))
return incident_technique(code)
De nouveaux codes peuvent apparaître dans une version mineure — c'est un ajout, pas une rupture. Traitez tout code inconnu comme un incident technique récupérable : n'échouez pas de façon brutale sur une valeur que vous ne connaissez pas encore.