hrpay / hrpay
HRPay PHP SDK
Requires
- php: >=8.1
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- ramsey/uuid: ^4.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.40
- guzzlehttp/guzzle: ^7.0
- php-http/mock-client: ^1.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-13 06:40:48 UTC
README
Le SDK PHP officiel pour l'API HRPay : Mobile Money (MTN, Orange...), cartes virtuelles, paiement par carte hébergé, factures, airtime/data, payroll, liens de paiement, webhooks, analytics.
L'idée : tu ne devrais jamais avoir à construire une requête HTTP à la main pour parler à HRPay. Ce SDK type chaque appel, gère les retries et l'idempotence pour toi, et te laisse la main sur ce qui compte vraiment — ton token, ton flux métier.
Sommaire
- Installation
- Authentification
- Sandbox vs Production
- Intégration Laravel
- Modules disponibles
- cashIn — Encaisser (Mobile Money)
- cashOut — Décaisser (Mobile Money)
- transactions — Suivi des transactions
- wallet — Solde et mouvements
- airtime — Recharge de crédit
- data — Forfaits internet
- bills — Paiement de factures
- cards — Cartes virtuelles (Cartevo)
- cardPayments — Paiement par carte hébergé (E-NKAP)
- payroll — Paie en masse
- commissions — Commissions revendeur (VAS)
- paymentLinks — Liens de paiement
- countries — Pays et opérateurs supportés
- analytics — Statistiques marchand
- webhooks — Événements et vérification de signature
- Enums de référence
- Gestion des erreurs
- Fonctionnalités transverses
- Tests
Installation
composer require hrpay/hrpay
Il te faut PHP 8.1+ et un client HTTP compatible PSR-18. Si tu en as déjà un dans ton projet (Guzzle, Symfony HttpClient...), le SDK le trouvera tout seul via php-http/discovery — rien à configurer. Sinon :
composer require guzzlehttp/guzzle
Authentification
Deux étapes, et c'est toi qui gardes la main sur le token — le SDK ne le stocke jamais et ne le rafraîchit jamais tout seul :
- Tu échanges ta clé publique + ta clé secrète contre un token de transaction.
- Tu instancies le client avec ta clé publique et ce token.
require 'vendor/autoload.php'; use HRPay\HRPay; use HRPay\Exceptions\HRPayException; $publicKey = 'hrsk_pk_test_xxxxx'; // hrsk_pk_test_... en sandbox, hrsk_pk_live_... en prod $secretKey = 'hrsk_sk_test_xxxxx'; // Étape 1 — le sandbox est détecté automatiquement dès que la clé publique contient "_test_" $token = HRPay::getTransactionToken($publicKey, $secretKey); // Étape 2 — c'est parti $hrpay = new HRPay(publicKey: $publicKey, secretKey: $secretKey, token: $token); try { $response = $hrpay->cashIn->mobileMoney( phone: '677123456', operator: 'MTN', amount: 5000, currency: 'XAF', country: 'CM', // idempotencyKey: '...' // optionnel, généré automatiquement si tu l'omets ); print_r($response); } catch (\HRPay\Exceptions\ValidationException $e) { echo "Erreur de validation : " . $e->getMessage() . "\n"; print_r($e->getErrors()); } catch (\HRPay\Exceptions\AuthenticationException $e) { echo "Clé API invalide.\n"; } catch (HRPayException $e) { echo "Erreur HRPay : " . $e->getMessage() . "\n"; }
Chaque requête part déjà avec X-Public-Key, Authorization: Bearer <token> et X-Transaction-Token — tu n'as rien d'autre à ajouter.
Un token, ça expire. Le jour où tu prends une AuthenticationException, la réponse est toujours la même : redemande-en un via HRPay::getTransactionToken().
Sandbox vs Production
Rien à changer dans ton code : dès que ta clé publique contient _test_, le SDK préfixe automatiquement chaque appel avec /sandbox.
$hrpay->isSandbox(); // true si ta publicKey contient "_test_"
Tu peux aussi forcer explicitement l'URL de base (le SDK est assez malin pour ne pas doubler le /sandbox si ton URL le contient déjà) :
$hrpay = new HRPay( publicKey: $publicKey, secretKey: $secretKey, token: $token, baseUrl: 'https://api.hrskills-pay.com/sandbox', );
Un piège à connaître sur le module bills : les 4 sous-modules par facturier (eneo, camwater, canalPlus, customs) n'existent tout simplement pas en sandbox — ils répondent 404 sous /sandbox. Pour tester tes intégrations factures en sandbox, passe par les méthodes génériques bills->lookup() et bills->pay() à la place.
Intégration Laravel
Le SDK embarque un ServiceProvider auto-découvert. Publie sa config :
php artisan vendor:publish --tag=hrpay-config
Puis renseigne ton .env :
PSP_HRPAY_KEY="hrsk_pk_test_..." PSP_HRPAY_SECRET="hrsk_sk_test_..." PSP_HRPAY_BASE_URL="https://api.hrskills-pay.com/sandbox" PSP_HRPAY_TOKEN="..." # optionnel : un token statique, si tu ne veux pas le gérer toi-même
Le client est enregistré en singleton (alias hrpay), donc tu peux directement l'injecter :
public function collect(HRPay $hrpay) { return $hrpay->cashIn->mobileMoney( phone: '677123456', operator: 'MTN', amount: 5000, currency: 'XAF', country: 'CM', ); }
Une nuance importante : Laravel n'obtient pas le transaction_token à ta place. C'est toujours à ton application de l'obtenir, de le stocker (cache, session, whatever te convient) et de le fournir — soit via PSP_HRPAY_TOKEN, soit en reconstruisant le client toi-même après l'avoir rafraîchi.
Modules disponibles
Tout part de $hrpay. Chaque propriété ci-dessous est un module dédié à une famille de fonctionnalités :
| Propriété | Rôle |
|---|---|
$hrpay->cashIn |
Encaisser (push Mobile Money) |
$hrpay->cashOut |
Décaisser vers un wallet Mobile Money |
$hrpay->transactions |
Consulter/lister/rembourser des transactions |
$hrpay->wallet |
Solde et mouvements du wallet marchand |
$hrpay->airtime |
Recharge de crédit téléphonique |
$hrpay->data |
Envoi de forfaits internet |
$hrpay->bills |
Paiement de factures (ENEO, CAMWATER, Canal+, Douanes) |
$hrpay->cards |
Cartes virtuelles Visa/Mastercard (Cartevo) |
$hrpay->cardPayments |
Paiement par carte via checkout hébergé (E-NKAP) |
$hrpay->payroll |
Paie en masse (import, exécution, rapport) |
$hrpay->commissions |
Barème et historique des commissions VAS |
$hrpay->paymentLinks |
Création de liens de paiement partageables |
$hrpay->countries |
Pays et opérateurs supportés par ton compte |
$hrpay->analytics |
Statistiques (résumé, volumes, revenus) |
$hrpay->webhooks |
Liste des événements + vérification de signature |
$hrpay->auth |
Aide à l'obtention manuelle d'un token |
cashIn — Encaisser (Mobile Money)
C'est le point d'entrée le plus courant : tu déclenches un push Mobile Money, et le client valide le paiement lui-même sur son téléphone.
$hrpay->cashIn->mobileMoney( phone: '677123456', operator: 'MTN', // MTN, ORANGE, CAMTEL, NEXTTEL, WAVE, MPESA, AIRTEL, MOOV, TMONEY, FLOOZ... amount: 5000, currency: 'XAF', country: 'CM', description: 'Facture #1234', metadata: ['order_id' => 1234], reference: 'ORDER-1234', // ta propre référence, idéalement idempotente idempotencyKey: null, // header HTTP Idempotency-Key, généré pour toi si tu l'omets ); // Alias utilisé par certaines intégrations existantes — fait exactement la même chose $hrpay->cashIn->initiate(/* mêmes paramètres */);
Une fois le paiement initié, deux façons de savoir ce qu'il devient : interroger $hrpay->transactions->status($reference) toi-même, ou écouter les webhooks payment.succeeded / payment.failed (bien plus propre en production).
cashOut — Décaisser (Mobile Money)
Le symétrique du précédent : tu envoies de l'argent vers le wallet Mobile Money de quelqu'un.
$hrpay->cashOut->mobileMoney( phone: '677123456', operator: 'ORANGE', amount: 15000, currency: 'XAF', country: 'CM', description: 'Remboursement commande #42', reference: 'REFUND-42', );
transactions — Suivi des transactions
// Statut d'un paiement par référence $hrpay->transactions->status('ORDER-1234'); // Détail complet $hrpay->transactions->get('ORDER-1234'); // Liste filtrée $hrpay->transactions->list( status: 'SUCCESS', // PENDING, SUCCESS, FAILED, REFUNDED, HOLD type: 'CASHIN', // CASHIN ou CASHOUT operator: 'MTN', from: '2026-01-01', to: '2026-01-31', page: 1, limit: 20, ); // Rembourser une transaction réussie $hrpay->transactions->refund('ORDER-1234'); // Barème de frais $hrpay->transactions->fees();
Pas d'endpoint webhook sous la main pour l'instant, ou tu veux juste un script simple qui attend la fin d'un paiement ? poll() bloque jusqu'à un statut terminal :
$result = $hrpay->transactions->poll( reference: 'ORDER-1234', interval: 2.0, // secondes entre chaque vérification timeout: 120.0, // délai maximum total onStatus: function (string $status, int $attempt) { echo "Tentative {$attempt} : {$status}\n"; }, );
Ceci dit, dès que tu peux exposer un webhook, préfère toujours ça au polling — c'est plus rapide et ça ne consomme pas de quota API pour rien.
wallet — Solde et mouvements
$hrpay->wallet->balance(); $hrpay->wallet->movements( from: '2026-01-01', to: '2026-01-31', page: 1, limit: 50, );
airtime — Recharge de crédit
$hrpay->airtime->recharge( operator: 'MTN', // MTN, ORANGE, CAMTEL, NEXTTEL phone: '677123456', amount: 1000, customerPhone: '677000000', // téléphone du payeur, pour le reçu / la référence customerEmail: 'client@example.com', reference: 'RECHARGE-1', ); // Plusieurs recharges en un seul appel $hrpay->airtime->batch([ ['operator' => 'MTN', 'phone' => '677111111', 'amount' => 500], ['operator' => 'ORANGE', 'phone' => '699222222', 'amount' => 1000], ]); // Dénominations disponibles par opérateur $hrpay->airtime->offers();
À savoir : ORANGE et NEXTTEL passent la validation côté SDK mais sont actuellement désactivés côté fournisseur — même en production, ils renvoient une erreur. Ce n'est pas un bug du SDK, c'est l'état réel de l'intégration en amont.
data — Forfaits internet
$hrpay->data->packages(operator: 'MTN'); // forfaits disponibles $hrpay->data->send( operator: 'MTN', phone: '677123456', amount: 1000, );
bills — Paiement de factures
Deux façons de payer une facture : les méthodes génériques (indépendantes du facturier, celles à utiliser en sandbox) et 4 sous-modules spécifiques par facturier (production uniquement).
use HRPay\Enums\Biller; // Générique — marche en sandbox comme en production $hrpay->bills->lookup(Biller::ENEO_PREPAID, '01234567890'); $hrpay->bills->pay( biller: Biller::CANALPLUS, accountNumber: 'DECODER123', amount: 10000, customerPhone: '677123456', ); // ENEO (électricité) — production uniquement $hrpay->bills->eneo->invoice('01234567890'); // facture postpayée $hrpay->bills->eneo->prepaidLookup('01234567890'); // consultation compteur prépayé $hrpay->bills->eneo->prepaid('01234567890', 5000); // achat de jetons prépayés $hrpay->bills->eneo->postpaid('01234567890', 15000); // paiement facture postpayée // CAMWATER (eau) — production uniquement $hrpay->bills->camwater->invoice('METER123'); $hrpay->bills->camwater->pay('METER123', 8000); // Canal+ (abonnement TV) — production uniquement $hrpay->bills->canalPlus->lookup('DECODER123'); $hrpay->bills->canalPlus->pay('DECODER123', 12000); // Douanes — production uniquement $hrpay->bills->customs->get('DECLARATION123'); $hrpay->bills->customs->pay('DECLARATION123', 250000);
Petit piège classique : Biller::ENEO tout seul résout vers le postpayé. Si tu veux du prépayé, utilise explicitement Biller::ENEO_PREPAID.
cards — Cartes virtuelles (Cartevo)
La suite complète pour émettre et gérer des cartes virtuelles Visa/Mastercard, alimentées par un wallet USD dédié. Le flux typique ressemble à ça :
cards->customers->create()— tu onboardes le porteur de carte (KYC)- Tu attends que son statut passe à
ENROLLED cards->wallet->fund()— tu approvisionnes le wallet USD depuis ton wallet principal (XAF)cards->virtualCards->create()— la carte est émise
use HRPay\Enums\IdDocumentType; use HRPay\Enums\CardBrand; // --- KYC du client --- $customer = $hrpay->cards->customers->create( firstName: 'Jean', lastName: 'Kamga', email: 'jean.kamga@example.com', country: 'Cameroon', countryIsoCode: 'CM', countryPhoneCode: '+237', phoneNumber: '677123456', street: 'Rue 123', city: 'Douala', state: 'Littoral', postalCode: '00000', identificationNumber: '123456789', idDocumentType: IdDocumentType::NIN, dateOfBirth: '1990-01-01', ); $hrpay->cards->customers->list(status: 'ENROLLED', search: 'Kamga'); $hrpay->cards->customers->get($customerId); $hrpay->cards->customers->cards($customerId); // cartes du client $hrpay->cards->customers->transactions($customerId); // transactions, toutes cartes confondues // --- Wallet USD (ce qui finance les cartes) --- $hrpay->cards->wallet->balance(); $hrpay->cards->wallet->pricing(); // plan tarifaire + taux de change du jour // Un aperçu avant de valider — fournis amountUsd OU amountXaf, pas les deux $hrpay->cards->wallet->quote(amountXaf: 65000, direction: 'fund'); $hrpay->cards->wallet->fund(amountXaf: 65000); // XAF -> wallet USD $hrpay->cards->wallet->withdraw(amountUsd: 50); // wallet USD -> wallet principal // --- Les cartes elles-mêmes --- $card = $hrpay->cards->virtualCards->create( customerId: $customerId, brand: CardBrand::VISA, amount: 20, // charge initiale en USD nameOnCard: 'JEAN KAMGA', label: 'Carte marketing', ); $hrpay->cards->virtualCards->list(customerId: $customerId, status: 'ACTIVE'); $hrpay->cards->virtualCards->get($cardId, reveal: false, sync: true); // reveal=true exige une autorisation renforcée $hrpay->cards->virtualCards->topup($cardId, 10); $hrpay->cards->virtualCards->withdraw($cardId, 5); $hrpay->cards->virtualCards->freeze($cardId); // réversible $hrpay->cards->virtualCards->unfreeze($cardId); $hrpay->cards->virtualCards->cancel($cardId); // définitif — pas de retour en arrière $hrpay->cards->virtualCards->transactions($cardId, page: 0, type: 'AUTHORIZATION'); // seule pagination 0-indexée du SDK // Tu préfères attendre que ça se stabilise plutôt que de gérer un webhook ? $hrpay->cards->virtualCards->poll($cardId);
cardPayments — Paiement par carte hébergé (E-NKAP)
Un checkout hébergé classique : tu crées la session, tu rediriges le client vers l'URL renvoyée, puis tu attends le statut final.
$payment = $hrpay->cardPayments->create( amount: 25000, currency: 'XAF', description: 'Commande #987', lang: 'fr', returnUrl: 'https://monsite.com/retour', cancelUrl: 'https://monsite.com/annulation', customer: ['name' => 'Jean Kamga', 'email' => 'jean.kamga@example.com', 'phone' => '677123456'], ); // Redirige ton client vers $payment['checkout_url'] $hrpay->cardPayments->get($reference); // CAPTURED est l'état qui compte vraiment — n'attends jamais SETTLED, il arrive bien plus tard (voire jamais dans certains cas) $hrpay->cardPayments->poll($reference, onStatus: fn($status, $attempt) => error_log("{$status} ({$attempt})")); // Le wallet "card-collect" (E-NKAP) s'authentifie différemment : un JWT du dashboard, pas ton Bearer habituel $hrpay->cardPayments->getCardCollectBalance($jwt); $hrpay->cardPayments->requestWithdrawal(amount: 100000, currency: 'XAF', jwt: $jwt); $hrpay->cardPayments->listWithdrawalRequests(status: 'PENDING', jwt: $jwt); $hrpay->cardPayments->listChargebacks(status: 'UNDER_REVIEW', jwt: $jwt);
payroll — Paie en masse
// 1. Tu importes le lot (recipients en tableau, fichier CSV/XLSX en base64, ou CSV brut — au choix) $batch = $hrpay->payroll->import( label: 'Paie Janvier 2026', currency: 'XAF', recipients: [ ['phone' => '677111111', 'operator' => 'MTN', 'amount' => 150000, 'name' => 'Employé 1'], ['phone' => '699222222', 'operator' => 'ORANGE', 'amount' => 200000, 'name' => 'Employé 2'], ], ); $batchId = $batch['batch_id'] ?? $batch['id']; // 2. Tu déclenches le versement $hrpay->payroll->execute($batchId); // 3. Tu suis le traitement $hrpay->payroll->status($batchId); // 4. Tu récupères le rapport détaillé par bénéficiaire $hrpay->payroll->report($batchId); // Tous les lots passés $hrpay->payroll->list(page: 1, limit: 20);
commissions — Commissions revendeur (VAS)
$hrpay->commissions->rates(); // barème des taux par service VAS $hrpay->commissions->history( service: 'AIRTIME', // AIRTIME, DATA, ENEO, CAMWATER, CANALPLUS, CUSTOMS from: '2026-01-01', to: '2026-01-31', ); $hrpay->commissions->summary(from: '2026-01-01', to: '2026-01-31');
paymentLinks — Liens de paiement
Pour les cas où tu veux juste un lien à envoyer, sans intégration côté client.
use HRPay\Enums\Currency; $link = $hrpay->paymentLinks->create( amount: 10000, description: 'Don association', currency: Currency::XAF, // XAF par défaut expiresAt: '2026-12-31T23:59:59Z', ); $hrpay->paymentLinks->list(page: 1, limit: 20);
countries — Pays et opérateurs supportés
// Particularité : ce endpoint veut un JWT du dashboard marchand, // pas le Bearer token de transaction que tu utilises partout ailleurs. $hrpay->countries->supported($dashboardJwt);
analytics — Statistiques marchand
$hrpay->analytics->summary(); // vue d'ensemble $hrpay->analytics->transactions(); // volumétrie $hrpay->analytics->revenue(); // revenus
webhooks — Événements et vérification de signature
$hrpay->webhooks->events(); // historique récent des livraisons
Règle numéro un des webhooks : ne traite jamais un payload sans avoir vérifié sa signature d'abord.
use HRPay\Resources\Webhooks; $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_HRPAY_SIGNATURE'] ?? ''; $event = Webhooks::constructEvent($payload, $signature, $webhookSecret); if ($event === null) { http_response_code(400); exit; } // Maintenant tu peux faire confiance à $event['type'] match ($event['type']) { 'payment.succeeded' => /* ... */ null, 'payment.failed' => /* ... */ null, default => null, };
Le SDK expose aussi HRPay\Utils\WebhookSignature::verifyHeader(), qui vérifie un en-tête horodaté au format t=<timestamp>,v1=<signature> (avec une tolérance de délai configurable, 300s par défaut) :
use HRPay\Utils\WebhookSignature; try { WebhookSignature::verifyHeader( payload: $payload, signatureHeader: $_SERVER['HTTP_HRPAY_SIGNATURE'] ?? '', secret: $webhookSecret, tolerance: 300, ); // signature valide } catch (\RuntimeException $e) { http_response_code(400); exit; }
Les deux méthodes existent parce que le format d'en-tête varie selon comment tu es intégré côté HRPay. Utilise celle qui correspond à ce que tu reçois réellement : Webhooks::constructEvent() pour un HMAC simple, WebhookSignature::verifyHeader() pour le format horodaté t=...,v1=....
Types d'événements (HRPay\Enums\WebhookEventType) :
| Paiement | Carte | Paiement carte (E-NKAP) |
|---|---|---|
payment.succeeded |
card.created |
card_payment.succeeded |
payment.failed |
card.funded |
card_payment.failed |
payment.hold |
card.withdrawn |
card_payment.canceled |
payment.refunded |
card.terminated |
card_payment.fraud_rejected |
| — | card.transaction.approved |
card_payment.chargeback_opened |
| — | card.transaction.declined |
card_payment.chargeback_evidence_needed |
| — | — | card_payment.chargeback_won |
| — | — | card_payment.chargeback_lost |
| — | — | card_balance.withdrawal_completed |
Enums de référence
Tous les enums sont des string enum PHP 8.1. Tu peux passer l'instance d'enum ou directement la chaîne équivalente, les deux marchent partout (Operator::MTN ou 'MTN', au choix).
| Enum | Valeurs |
|---|---|
Country |
CM, SN, CI, NG, GH, KE, TZ, UG, RW, ET, ML, BF, TG, BJ, NE, GW, GN, GM, CF, MZ, ZM, MW |
Currency |
XAF, XOF, NGN, GHS, KES, TZS, UGX, RWF, ETB, MWK, ZMW, GNF, USD, EUR |
Operator |
MTN, ORANGE, CAMTEL, NEXTTEL, WAVE, MPESA, AIRTEL, MOOV, TMONEY, FLOOZ, CELTIIS, CORIS, AFRIMONEY, QMONEY, WLIGDICASH |
AirtimeOperator |
MTN, ORANGE, CAMTEL, NEXTTEL |
TransactionStatus |
PENDING, SUCCESS, FAILED, REFUNDED, HOLD |
TransactionDirection |
CASHIN, CASHOUT |
Biller |
ENEO_PREPAID, ENEO_VOUCHER, ENEO_POSTPAID, ENEO_BILL, ENEO, CAMWATER, CAMWATER_BILL, CANALPLUS, CANALPLUS_SUB, CUSTOMS, CUSTOMS_PAY, DOUANES |
VasService |
AIRTIME, DATA, ENEO, CAMWATER, CANALPLUS, CUSTOMS |
CardBrand |
VISA, MASTERCARD |
CardStatus |
PENDING, ACTIVE, FROZEN, SUSPENDED, TERMINATED, FAILED |
CardTransactionType |
AUTHORIZATION, SETTLEMENT, FUNDING, WITHDRAWAL, DECLINE, REVERSAL, REFUND, CROSS-BORDER, TERMINATION |
CardPaymentStatus |
PENDING, AUTHORIZED, CAPTURED, SETTLED, REFUNDED, FAILED, CANCELED |
ChargebackStatus |
RECEIVED, UNDER_REVIEW, EVIDENCE_SUBMITTED, WON, LOST |
KycStatus |
PENDING_REVIEW, ENROLLING, ENROLLED, REJECTED_PROVIDER, REJECTED_LOCAL |
IdDocumentType |
NIN, PASSPORT, VOTERS_CARD, DRIVERS_LICENSE |
Environment |
sandbox, production |
WebhookEventType |
voir le tableau des événements ci-dessus |
Gestion des erreurs
Toutes les exceptions du SDK implémentent HRPay\Exceptions\HRPayException. Si tu ne veux pas distinguer les cas, un seul catch (HRPayException $e) suffit.
| Exception | Quand | Ce qu'elle t'apporte en plus |
|---|---|---|
ValidationException |
Réponse 422 |
getErrors(): array — le détail par champ |
AuthenticationException |
Réponse 401 / 403 |
— |
RateLimitException |
Réponse 429 |
getRetryAfter(): ?int (en secondes) |
NetworkException |
Erreur réseau, retries épuisés | — |
ApiErrorException |
Tout le reste | getStatusCode(): ?int |
use HRPay\Exceptions\{ValidationException, AuthenticationException, RateLimitException, NetworkException, HRPayException}; try { $hrpay->cashIn->mobileMoney(/* ... */); } catch (ValidationException $e) { print_r($e->getErrors()); } catch (RateLimitException $e) { sleep($e->getRetryAfter() ?? 5); } catch (AuthenticationException $e) { // ton token a expiré : redemandes-en un via HRPay::getTransactionToken() } catch (NetworkException | HRPayException $e) { echo $e->getMessage(); }
Fonctionnalités transverses
Quelques comportements qui tournent en arrière-plan, sans que tu aies à y penser :
- Retries automatiques — chaque requête réessaie toute seule (backoff exponentiel : 100ms, 200ms, 400ms...) sur erreur réseau,
429,503+ ou408. Tu contrôles ça viamaxRetriesau constructeur (2 par défaut). - Idempotence — la plupart des méthodes d'écriture acceptent un
idempotencyKeyoptionnel (headerIdempotency-Key). Tu l'omets ? Une valeur est générée pour toi. - Polling —
transactions->poll(),cardPayments->poll()etcards->virtualCards->poll()bloquent jusqu'au statut terminal, avec callbackonStatus(status, attempt), timeout et nombre max de tentatives configurables. Pratique quand un webhook n'est pas envisageable, mais pas un substitut idéal. - Injection HTTP — tu peux fournir tes propres implémentations PSR-18/17 au constructeur (
httpClient,requestFactory,streamFactory). Utile pour tes tests (voirHttp\Mock\Client, déjà enrequire-dev) ou pour brancher ton propre client HTTP (logging, proxy, etc.).
Tests
composer install vendor/bin/phpunit # tests unitaires, tout est mocké — aucun appel réseau vendor/bin/phpstan analyse # analyse statique
Le test end-to-end (tests/Integration/E2ETest.php) appelle le vrai sandbox HRPay. Il a besoin des variables d'environnement PSP_HRPAY_KEY, PSP_HRPAY_SECRET et PSP_HRPAY_BASE_URL — si elles ne sont pas définies, il est automatiquement skipped, pas d'échec surprise en CI.