Docs API

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

Enveloppe d'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)."]
    }
  }
}
ChampDescription
error.codeIdentifiant stable de l'erreur. C'est sur lui que votre code doit brancher.
error.messagePhrase en français destinée à un humain. Peut être reformulée sans préavis : ne la testez jamais.
error.detailsPrésent uniquement sur les erreurs de validation. Un objet champ → [messages].

Codes d'erreur

CodeHTTPSignificationQue 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 :

422 — plusieurs champs invalides
{
  "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

SituationRéessayer ?
429, 500, timeout réseauOui, avec un délai croissant (1 s, 2 s, 4 s, 8 s…).
401, 403, 404, 405, 422Non. La requête ne passera jamais telle quelle.
402 payment_failedNon automatiquement : c'est une décision du client, pas un incident technique.
409 conflictNon sans avoir relu l'état de la ressource.
Réessayer une création de paiement

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)
Prévoyez le cas par défaut

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.