> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alphapay.me/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK PHP

> Intégrez les paiements AlphaPay côté serveur dans une application PHP via le SDK officiel.

Le **SDK PHP `alphapay/alphapay-php`** enveloppe l'API REST AlphaPay (checkout, softpay, liens de paiement, clients, webhooks...) avec retry automatique, gestion d'idempotence et exceptions typées par cas d'erreur. Toutes les ressources marchand sont déjà instanciées sur le client — aucun appel HTTP manuel à écrire.

Vous utilisez Laravel ? Préférez le [SDK Laravel](/laravel-sdk), qui enveloppe ce package avec une Facade, des Events et un trait Eloquent.

```bash theme={null}
composer require alphapay/alphapay-php
export ALPHAPAY_SECRET_KEY=sk_test_votre_cle
```

## Sommaire

1. [Checkout (session hébergée)](#1-checkout-session-hébergée)
2. [Softpay (encaissement direct)](#2-softpay-encaissement-direct)
3. [Lien de paiement](#3-lien-de-paiement)
4. [Balance](#4-balance)
5. [Clients (CRM)](#5-clients-crm)
6. [Webhooks](#6-webhooks)
7. [Reversements et transferts wallet](#7-reversements-et-transferts-wallet)
8. [Clés API et whitelist IP](#8-clés-api-et-whitelist-ip)
9. [Gestion des erreurs](#9-gestion-des-erreurs)

***

## 1. Checkout (session hébergée)

Une session de checkout génère sa propre page de paiement hébergée
(`checkout_url`) : le client choisit lui-même son réseau et saisit son
numéro sur cette page. Contrairement au softpay (section 2), vous n'avez
rien à construire côté frontend — juste rediriger le client vers l'URL
renvoyée.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    // 1. Créer la session -- idempotencyKey évite de générer deux sessions
    // pour un double-clic ou un retry réseau côté serveur.
    $session = $alphapay->checkoutSessions->create([
        'amount' => 5000,
        'currency' => 'XOF',
        'description' => 'Commande #1234',
        'customer_email' => 'ayaba@exemple.com',
        'customer_name' => 'Ayaba Client',
        'customer_phone' => '+22900000000',
        'return_url' => 'https://votre-site.test/merci',
    ], idempotencyKey: true);

    echo "Session créée : {$session['slug']}\n";
    echo "Redirigez le client vers : {$session['checkout_url']}\n";

    // 2. Plus tard (webhook transaction.success/failed, ou poll manuel) --
    // relire l'état de la session.
    $current = $alphapay->checkoutSessions->get($session['id']);
    echo "Statut actuel : {$current['status']}\n"; // PENDING / PAID / EXPIRED / CANCELLED

    // 3. Annuler une session encore inutilisée (ex. le client a changé de panier).
    // $alphapay->checkoutSessions->cancel($session['id']);
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
    exit(1);
}
```

***

## 2. Softpay (encaissement direct)

Pousse directement une demande de paiement (USSD/mobile money) au numéro du
client, sans page de checkout à afficher — utile pour une app mobile ou un
parcours de caisse où vous gérez vous-même l'UI. `customer` exige les 4
champs (`email`, `first_name`, `last_name`, `phone`) : contrairement à une
session de checkout, il n'y a pas de page hébergée où le client les
saisirait lui-même après coup.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    $payment = $alphapay->transactions->payinInitialize([
        'amount' => 5000,
        'currency' => 'XOF',
        'country' => 'BJ',
        'network' => 'mtn_bj', // cf. section "Codes réseau" du README -- toujours en minuscules
        'customer' => [
            'email' => 'ayaba@exemple.com',
            'first_name' => 'Ayaba',
            'last_name' => 'Client',
            'phone' => '+22900000000',
        ],
        'description' => 'Commande #1234',
    ], idempotencyKey: true); // recommandé : évite un double push en cas de retry réseau

    echo "Transaction : {$payment['id']} -- statut {$payment['status']}\n";

    // Certains réseaux (Coris Bénin, Wizall Sénégal) exigent un second appel
    // avec le code reçu par SMS APRÈS ce push initial.
    if (!empty($payment['otp_required'])) {
        echo "OTP requis -- collectez le code SMS puis :\n";
        // $confirmed = $alphapay->transactions->payinConfirmOtp($payment['id'], $otpSaisiParLeClient);
    }

    // Consigne à afficher MAINTENANT (ex. "Composez #144*82#") -- vide pour
    // les réseaux qui envoient un prompt spontané (MTN, Wave...).
    if (!empty($payment['instructions'])) {
        echo 'Consigne : ' . json_encode($payment['instructions']) . "\n";
    }

    // Sondage manuel du vrai statut (l'API interroge le gateway) -- à faire
    // depuis un webhook `transaction.*` en production plutôt qu'une boucle.
    sleep(3);
    $verified = $alphapay->transactions->payinVerify($payment['id']);
    echo "Statut vérifié : {$verified['status']}\n";

    // Le numéro était erroné / le client n'a pas répondu à temps : relancer
    // SUR LA MÊME transaction (même montant/client), éventuellement avec un
    // autre gateway.
    // $retry = $alphapay->transactions->payinRetry($payment['id'], ['preferred_gateway' => 'PAYDUNIA']);
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
    if ($e->getFieldErrors() !== null) {
        fwrite(STDERR, 'Champs en erreur : ' . json_encode($e->getFieldErrors()) . "\n");
    }
    exit(1);
}
```

***

## 3. Lien de paiement

Un lien de paiement est **réutilisable** (contrairement à une session de
checkout, one-shot) : sa propre page publique ne nécessite AUCUNE clé API,
donc `getPublic()`/`createPublicCheckout()` sont les 2 seules méthodes de ce
SDK à pouvoir tourner côté client (jamais les autres, qui portent la clé secrète).

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    // 1. Créer le lien (côté serveur, clé secrète) -- options avancées incluses.
    $link = $alphapay->paymentLinks->create([
        'name' => 'Facture #42',
        'amount_type' => 'FIXED', // ou 'FREE' pour laisser le client saisir le montant
        'amount' => 5000,
        'currency' => 'XOF',
        'require_phone' => true,
        'google_ads_id' => 'AW-123456789',
        'custom_fields' => [
            ['key' => 'reference_client', 'label' => 'Référence client', 'required' => true],
        ],
        // 'show_confirmation_page' => false, 'redirect_url' => 'https://votre-site.test/merci', // sinon page de confirmation par défaut
    ]);
    echo "Lien créé : {$link['url']} (slug: {$link['slug']})\n";

    // 2. Côté PUBLIC (page web/app du lien) -- sans clé API, seul le slug circule.
    $publicLink = $alphapay->paymentLinks->getPublic($link['slug']);
    echo "Montant affiché : {$publicLink['amount']} {$publicLink['currency']}\n";

    $checkout = $alphapay->paymentLinks->createPublicCheckout($link['slug'], [
        'customer' => [
            'email' => 'client@exemple.com',
            'first_name' => 'Client',
            'last_name' => 'Test',
            'phone' => '+22900000000', // requis car require_phone=true ci-dessus
        ],
        'custom_field_values' => ['reference_client' => 'CMD-42'],
    ]);
    echo "CheckoutSession créée : {$checkout['slug']}\n";
    echo "Redirigez le client vers : {$checkout['checkout_url']}\n";
    // A partir d'ici, même flux que la section 1 (checkoutSessions->get(), webhook, etc.)

    // 3. Gestion courante du lien.
    $alphapay->paymentLinks->update($link['id'], ['is_active' => false]); // désactive sans supprimer
    $links = $alphapay->paymentLinks->list(['is_active' => true]);
    echo 'Liens actifs : ' . count($links['results']) . "\n";
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
    exit(1);
}
```

***

## 4. Balance

Solde disponible par pays/devise. Lecture seule : les mouvements naissent
des autres ressources (paiements, reversements, transferts). Le grand livre
détaillé (`ledgerEntries`) est **dashboard-only** — inaccessible via clé API.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    $balances = $alphapay->balances->list();
    foreach ($balances['results'] as $balance) {
        echo "{$balance['country']} ({$balance['currency']}) : {$balance['available_amount']} disponible\n";
    }

    if ($balances['results'] !== []) {
        $first = $alphapay->balances->get($balances['results'][0]['id']);
        echo "Détail : " . json_encode($first) . "\n";
    }

    // Dashboard-only -- lève AlphaPayPermissionException (403, "dashboard_only")
    // via une clé API, quel que soit son scope. Gardé ici pour documenter la
    // forme réelle, pas pour un usage effectif avec ce client.
    // $alphapay->balances->ledgerEntries(['country' => 'BJ']);
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
    exit(1);
}
```

***

## 5. Clients (CRM)

Répertoire de clients propre au marchand (distinct de `customer` passé à
`payinInitialize`/`checkoutSessions->create`, qui ne fait que remplir une
transaction). `country` ici est un **UUID** (ForeignKey `geo.Country` côté
API), pas un code ISO2 — contrairement à `country` sur les
paiements/retraits/reversements. Aucune ressource `countries` dans ce SDK
pour résoudre l'UUID : passez par le dashboard ou `GET /countries/` via
`$alphapay->http->request('GET', '/countries/')`.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    $countryUuid = 'b03ed84a-6072-442c-beb2-50fc6cd40084'; // Bénin -- relevé depuis le dashboard ou GET /countries/

    $customer = $alphapay->customers->create([
        'country' => $countryUuid,
        'full_name' => 'Ayaba Client',
        'email' => 'ayaba@exemple.com',
        'phone' => '+22900000000',
    ]);
    echo "Client créé : {$customer['id']}\n";

    $alphapay->customers->update($customer['id'], ['email' => 'nouvelle-adresse@exemple.com']);

    $history = $alphapay->customers->transactions($customer['id'], ['page_size' => 20]);
    echo 'Transactions de ce client : ' . count($history['results']) . "\n";

    // $alphapay->customers->delete($customer['id']);
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
    exit(1);
}
```

***

## 6. Webhooks

Créer/gérer un endpoint webhook est **dashboard-only** (403 via clé API) --
seuls `list()`/`get()` fonctionnent. En revanche, la **vérification de
signature** d'un webhook reçu est justement ce que ce SDK sert à faire côté
serveur marchand.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;
use AlphaPay\Exceptions\AlphaPayWebhookSignatureException;
use AlphaPay\Webhook;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

// --- Consultation (fonctionne via clé API) ---
try {
    $webhooks = $alphapay->webhookEndpoints->list();
    foreach ($webhooks['results'] as $endpoint) {
        echo "{$endpoint['url']} ({$endpoint['environment']}), actif: " . ($endpoint['is_active'] ? 'oui' : 'non') . "\n";
    }

    // La création/modification se fait depuis le dashboard AlphaPay, pas via
    // ce SDK -- gardé ici pour documenter la forme réelle des paramètres.
    // $alphapay->webhookEndpoints->create([
    //     'url' => 'https://votre-site.test/webhooks/alphapay',
    //     'environment' => 'live',
    //     'payment_link' => null, // ou l'id d'un lien précis pour ne recevoir QUE ses événements
    // ]);
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
}

// --- Réception d'un webhook (à placer dans le contrôleur qui reçoit les POST AlphaPay) ---
function handleIncomingWebhook(): void
{
    $rawBody = file_get_contents('php://input'); // corps BRUT -- jamais json_decode() avant vérification
    $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
    $timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
    $secret = getenv('ALPHAPAY_WEBHOOK_SECRET'); // depuis le dashboard, visible une seule fois

    try {
        $event = Webhook::verifySignature($rawBody, $signature, $timestamp, $secret);
    } catch (AlphaPayWebhookSignatureException $e) {
        http_response_code(400);
        echo json_encode(['error' => $e->getMessage()]);
        return;
    }

    // Signature vérifiée -- $event['event'] (ex. "transaction.success") et
    // $event['data'] sont maintenant fiables.
    switch ($event['event']) {
        case 'transaction.success':
            // Créditer la commande locale correspondant à $event['data']['reference'].
            break;
        case 'transaction.failed':
            // Notifier le client, libérer le stock réservé, etc.
            break;
    }

    http_response_code(200);
    echo json_encode(['success' => true]);
}
```

***

## 7. Reversements et transferts wallet

`settlements` (reversement vers un compte bancaire/mobile money enregistré)
et `walletTransfers` (entre wallets multi-pays d'un même marchand) sont
**entièrement inaccessibles via clé API, y compris en lecture** (403,
`dashboard_only`, vérifié en conditions réelles) -- restriction volontaire
côté API : ces mouvements ne peuvent être déclenchés/consultés que depuis un
compte utilisateur connecté au dashboard. Ce SDK garde ces méthodes pour
documenter la forme réelle des endpoints, pas pour un usage effectif ici.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;
use AlphaPay\Exceptions\AlphaPayPermissionException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    $alphapay->settlements->list();
} catch (AlphaPayPermissionException $e) {
    echo "Attendu : {$e->getErrorCode()} -- reversements pilotables uniquement depuis le dashboard.\n";
}

try {
    $alphapay->walletTransfers->create([
        'from_country' => 'CI',
        'to_country' => 'BJ',
        'from_amount' => 10000,
    ], idempotencyKey: true);
} catch (AlphaPayPermissionException $e) {
    echo "Attendu : {$e->getErrorCode()} -- transferts wallet pilotables uniquement depuis le dashboard.\n";
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
}
```

***

## 8. Clés API et whitelist IP

Comme les webhooks, créer/lister/révoquer une clé API est **dashboard-only**
(une clé compromise ne doit pas pouvoir en créer d'autres pour elle-même).
Seule la whitelist IP (requise pour les payouts) fonctionne via clé API.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    $entry = $alphapay->apiKeys->ipWhitelist->create([
        'ip_address' => '203.0.113.42',
        'label' => 'Serveur de production',
    ]);
    echo "IP autorisée : {$entry['id']}\n";

    $entries = $alphapay->apiKeys->ipWhitelist->list();
    echo 'IPs en whitelist : ' . count($entries['results']) . "\n";

    $alphapay->apiKeys->ipWhitelist->update($entry['id'], ['status' => 'INACTIVE']);
    // $alphapay->apiKeys->ipWhitelist->delete($entry['id']);
} catch (AlphaPayException $e) {
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()}\n");
    exit(1);
}
```

***

## 9. Gestion des erreurs

Toute erreur API se normalise en une sous-classe d'`AlphaPayException` --
jamais un code HTTP brut à interpréter vous-même.

```php theme={null}
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use AlphaPay\AlphaPayClient;
use AlphaPay\Exceptions\AlphaPayAuthenticationException;
use AlphaPay\Exceptions\AlphaPayException;
use AlphaPay\Exceptions\AlphaPayIdempotencyException;
use AlphaPay\Exceptions\AlphaPayRateLimitException;
use AlphaPay\Exceptions\AlphaPayValidationException;

$alphapay = new AlphaPayClient(getenv('ALPHAPAY_SECRET_KEY'));

try {
    $alphapay->transactions->payinInitialize([
        'amount' => 5000,
        'currency' => 'XOF',
        'country' => 'BJ',
        'network' => 'mtn_bj',
        'customer' => ['email' => 'a@a.com', 'first_name' => 'A', 'last_name' => 'B', 'phone' => '+22900000000'],
    ]);
} catch (AlphaPayValidationException $e) {
    // 400/422 -- $e->getFieldErrors() : ["champ" => ["message", ...], ...] si erreur de validation par champ.
    fwrite(STDERR, "Validation : " . json_encode($e->getFieldErrors()) . "\n");
} catch (AlphaPayAuthenticationException $e) {
    // 401 -- clé API absente/invalide/révoquée.
    fwrite(STDERR, "Auth invalide : {$e->getMessage()}\n");
} catch (AlphaPayIdempotencyException $e) {
    // 409 -- même Idempotency-Key réutilisée avec un payload DIFFÉRENT.
    fwrite(STDERR, "Conflit d'idempotence : {$e->getMessage()}\n");
} catch (AlphaPayRateLimitException $e) {
    // 429 -- déjà retenté automatiquement par le SDK (backoff + Retry-After) avant d'arriver ici.
    fwrite(STDERR, "Rate limit persistant après retries.\n");
} catch (AlphaPayException $e) {
    // Filet générique -- tout statut/erreur non couvert ci-dessus.
    fwrite(STDERR, "Erreur AlphaPay [{$e->getStatus()}] {$e->getErrorCode()}: {$e->getMessage()} (request: {$e->getRequestId()})\n");
}
```
