Guide d'implementation API

Tout ce qu'il faut pour integrer PayHub dans votre application : authentification, paiements, webhooks et exemples de code.

Base URL

Developpement : http://localhost:8888 — Remplacez par l'URL de votre instance en production.

Authentification HMAC

Toutes les requetes API doivent etre signees avec HMAC-SHA256. Chaque tenant possede une paire API_KEY / API_SECRET.

Headers requis sur chaque requete

X-API-KEYCle publique du tenant (key_id)
X-SIGNATUREHMAC-SHA256 de timestamp.body signe avec le secret_key
X-TIMESTAMPTimestamp Unix en secondes (tolerance : ±5 min)
X-NONCEValeur unique anti-rejeu (UUID ou hex aleatoire)
Calcul de la signature (PHP)
$timestamp = time();
$nonce = bin2hex(random_bytes(16));
$body = json_encode(['amount' => 5000, 'currency' => 'XAF']);

$signature = hash_hmac('sha256', $timestamp . '.' . $body, $apiSecret);

$headers = [
    'X-API-KEY'    => $apiKey,
    'X-SIGNATURE'  => $signature,
    'X-TIMESTAMP'  => $timestamp,
    'X-NONCE'      => $nonce,
    'Content-Type' => 'application/json',
];

Idempotence

Ajoutez le header Idempotency-Key sur les requetes POST pour eviter les doublons en cas de retry reseau.

✓Meme cle + meme body → reponse originale retournee depuis le cache
✗Meme cle + body different → 409 Conflict
ℹLes cles expirent apres 24h. Utilisez un UUID v4 par requete unique.

Paiements (PaymentIntent)

Le flux suit le modele Stripe : PaymentIntent → Charge → Refund/Payout.

PENDING PROCESSING SUCCEEDED / FAILED / REQUIRES_ACTION REFUNDED
REQUIRES_ACTION : Certains providers (PayPal, 2Checkout) necessitent une approbation utilisateur. La reponse contient un redirect_url vers lequel rediriger le client. Apres approbation, appelez POST /api/v1/payment-intents/{id}/capture pour finaliser.

Creer un paiement

POST /api/v1/payment-intents
// Requete
{
    "amount": 5000,
    "currency": "XAF",
    "provider": "mtn_momo",
    "customer_identifier": "+242064000000",
    "description": "Commande #1234",
    "external_reference": "ORDER-2026-001",
    "environment": "sandbox",
    "metadata": { "order_id": "1234" }
}

// Reponse 201
{
    "id": "01952f3a-7abc-...",
    "status": "succeeded",
    "amount": 5000,
    "currency": "XAF",
    "provider": "mtn_momo",
    "environment": "sandbox",
    "created_at": "2026-03-09T10:00:00+00:00"
}

Consulter un paiement

GET /api/v1/payment-intents/{id}

Lister les paiements

GET /api/v1/payment-intents?status=succeeded&currency=XAF&limit=20&offset=0

Annuler un paiement en attente

POST /api/v1/payment-intents/{id}/cancel

Capturer un paiement (PayPal / 2Checkout)

Apres l'approbation de l'utilisateur sur la page du provider, capturez le paiement :

POST /api/v1/payment-intents/{id}/capture

Environnement (Sandbox / Production)

Chaque requete API accepte un parametre environment pour determiner quel jeu de credentials utiliser. Un meme provider peut etre configure en sandbox et en production simultanement.

sandbox (defaut)

Environnement de test. Aucun mouvement d'argent reel. Utilisez les credentials sandbox du provider.

production

Environnement reel. Les paiements sont effectues et les montants debites. Credentials de production requis.

Exemple d'utilisation

// Paiement en production
{
    "amount": 5000,
    "currency": "XAF",
    "provider": "mtn_momo",
    "customer_identifier": "+242064000000",
    "environment": "production"
}

// Paiement en sandbox (defaut si omis)
{
    "amount": 5000,
    "currency": "XAF",
    "provider": "mtn_momo",
    "customer_identifier": "+242064000000",
    "environment": "sandbox"
}

Important : Si le parametre environment est omis, la valeur par defaut est sandbox. Assurez-vous de passer "environment": "production" en production.

Endpoints concernes :

  • POST /api/v1/payment-intents Paiements
  • POST /api/payments/create Paiements (simplifie)
  • POST /api/v1/payouts Retraits
  • POST /api/v1/emails/send Emails
  • POST /api/v1/sms/send SMS

API simplifiee

Endpoint alternatif avec des noms de champs simplifies, compatible avec le cahier des charges initial.

POST /api/payments/create
{
    "amount": 10000,
    "currency": "XAF",
    "payment_method": "MTN",
    "customer_phone": "242060000000",
    "callback_url": "https://example.com/webhook",
    "reference": "CMD-2026-001",
    "description": "Commande en ligne",
    "environment": "sandbox"
}

// Methodes acceptees : MTN, AIRTEL, PAYPAL, 2CHECKOUT
// environment : "sandbox" (defaut) ou "production"

Verifier le statut par reference

GET /api/payments/{reference}/status

Remboursements

POST /api/v1/refunds
// Remboursement total
{ "payment_intent_id": "01952f3a-7abc-..." }

// Remboursement partiel
{
    "payment_intent_id": "01952f3a-7abc-...",
    "amount": 2500,
    "reason": "Client insatisfait"
}

// Reponse 201
{
    "id": "01952f3b-...",
    "status": "succeeded",
    "amount": 2500,
    "payment_intent_id": "01952f3a-7abc-..."
}

Consulter un remboursement

GET /api/v1/refunds/{id}

Email & SMS transactionnels

PayHub permet d'envoyer des emails et SMS transactionnels via les providers configures pour votre tenant. Le provider par defaut est utilise si non specifie.

Envoyer un email

POST /api/v1/emails/send
// Requete
{
    "to": "[email protected]",
    "subject": "Confirmation de paiement",
    "body_html": "<h1>Merci !</h1><p>Votre paiement a ete confirme.</p>",
    "body_text": "Merci ! Votre paiement a ete confirme.",
    "from_email": "[email protected]",
    "from_name": "Mon Application",
    "provider": "smtp",
    "environment": "production"
}

// Reponse 200
{
    "success": true,
    "message_id": "abc123",
    "provider": "smtp"
}
Champs obligatoires : to, subject, body_html ou body_text. Optionnels : from_email, from_name, reply_to, cc, bcc, provider, environment.

Envoyer un SMS

POST /api/v1/sms/send
// Requete
{
    "to": "+242064000000",
    "message": "Votre code de validation est : 123456",
    "from": "PAYHUB",
    "provider": "allmysms",
    "environment": "production"
}

// Reponse 200
{
    "success": true,
    "message_id": "sms_xyz",
    "provider": "allmysms"
}
Champs obligatoires : to, message. Optionnels : from, provider, environment.

Providers disponibles

Email

  • smtp — SMTP natif
  • mailgun — Mailgun API
  • mailtrap — Mailtrap (test/staging)
  • mailcatcher — MailCatcher (dev)

SMS

  • allmysms — AllMySms
  • clickatell — Clickatell
  • bandwidth — Bandwidth
  • fakesms — FakeSms (dev)

Webhooks

PayHub envoie des notifications HTTP POST a vos URLs de webhook pour chaque evenement. Les webhooks sont signes avec HMAC-SHA256.

Enregistrer un endpoint

POST /api/v1/webhook-endpoints
{
    "url": "https://example.com/webhooks/payhub",
    "events": ["payment_intent.succeeded", "payment_intent.failed"]
}

// events vide = recevoir tous les evenements

Evenements disponibles

payment_intent.created PaymentIntent cree
payment_intent.succeeded Paiement confirme, montant credite
payment_intent.failed Paiement echoue
refund.created Demande de remboursement initiee
refund.succeeded Remboursement effectue
payout.succeeded Retrait effectue

Verification de la signature

Le header X-PayHub-Signature contient : t=timestamp,v1=signature

Verification (PHP)
$header = $_SERVER['HTTP_X_PAYHUB_SIGNATURE'];
preg_match('/t=(\d+),v1=(.+)/', $header, $m);
$timestamp = $m[1];
$receivedSig = $m[2];

$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256',
    $timestamp . '.' . $payload, $endpointSecret);

if (!hash_equals($expected, $receivedSig)) {
    http_response_code(401);
    exit('Signature invalide');
}

// Verifier la fraicheur (< 5 min)
if (abs(time() - (int)$timestamp) > 300) {
    http_response_code(401);
    exit('Timestamp expire');
}

$event = json_decode($payload, true);
// Traiter l'evenement...

Exemple de payload

{
    "event": "payment_intent.succeeded",
    "timestamp": "2026-03-09T10:05:00+01:00",
    "data": {
        "id": "01952f3a-7abc-...",
        "status": "succeeded",
        "amount": 5000,
        "currency": "XAF",
        "provider": "mtn_momo",
        "customer_identifier": "+242064000000"
    }
}

Retentatives : En cas d'echec (non-2xx), PayHub retente jusqu'a 5 fois avec backoff exponentiel (5s, 20s, 80s, 320s, ~1h). Repondez avec un code 2xx pour confirmer la reception.

Providers supportes

MTN

MTN Mobile Money

Congo-Brazzaville

provider: "mtn_momo"
Devises : XAF, XOF
Paiement asynchrone (202 Accepted → webhook/polling)
Airtel

Airtel Money

Congo-Brazzaville

provider: "airtel_money"
Devises : XAF, XOF
Auth OAuth2 (client_id / client_secret)
PayPal

PayPal

International

provider: "paypal"
Devises : USD, EUR
Auth OAuth2 (client_id / client_secret)
2Checkout

2Checkout

Cartes bancaires

provider: "two_checkout"
Devises : USD, EUR
Auth par merchant_code / secret_key

Gestion des erreurs

Format standard

{ "error": "error_type", "message": "Description lisible" }

Codes HTTP

200Succes
201Ressource creee
400Requete invalide (champs manquants, format incorrect)
401Authentification echouee (cle ou signature invalide)
404Ressource non trouvee
409Conflit d'idempotence
422Transition d'etat invalide / validation echouee
500Erreur interne du serveur

Types d'erreurs

errorDescription
auth_failedCle API ou signature HMAC invalide
invalid_nonceNonce deja utilise (rejeu detecte)
validation_errorChamps requis manquants ou invalides
invalid_transitionChangement d'etat non autorise
provider_errorErreur retournee par le provider de paiement
idempotency_conflictCle d'idempotence reutilisee avec un payload different
not_foundRessource introuvable

SDKs & exemples

PHP (SDK PayHub)

Installez avec : composer require senprog/payhub-sdk

use PayHub\PayHubClient;

$client = new PayHubClient('pk_live_xxx', 'sk_live_xxx',
    'https://pay.example.com');

// Creer un paiement
$payment = $client->createPayment(
    amount: 5000,
    currency: 'XAF',
    provider: 'mtn_momo',
    customerIdentifier: '242060000000',
    options: ['description' => 'Commande #1234',
              'environment' => 'production']
);
echo $payment['status']; // "succeeded"

// Rembourser
$refund = $client->createRefund($payment['id'],
    amount: 2500, reason: 'Retour produit');

// Verifier un webhook
$valid = PayHubClient::verifyWebhookSignature(
    $rawBody, $signatureHeader, $endpointSecret);

JavaScript / Node.js (SDK PayHub)

Installez avec : npm install @senprog/payhub-sdk

const PayHub = require('@senprog/payhub-sdk');

const client = new PayHub('pk_live_xxx', 'sk_live_xxx',
    'https://pay.example.com');

// Creer un paiement
const payment = await client.createPayment(
    5000, 'XAF', 'mtn_momo', '242060000000',
    { description: 'Commande #1234', environment: 'production' }
);
console.log(payment.status); // "succeeded"

// Rembourser
const refund = await client.createRefund(payment.id,
    2500, 'Retour produit');

// Verifier un webhook (Express.js)
app.post('/webhook', (req, res) => {
    const valid = PayHub.verifyWebhookSignature(
        req.rawBody, req.headers['x-payhub-signature'],
        process.env.WEBHOOK_SECRET);
    if (!valid) return res.status(401).end();
    // Traiter l'evenement...
    res.status(200).end();
});

Python (requests)

import hmac, hashlib, time, secrets, json, requests

API_KEY = 'pk_live_xxx'
API_SECRET = 'sk_live_xxx'
BASE_URL = 'https://pay.example.com'

def payhub_request(method, path, body=None):
    timestamp = str(int(time.time()))
    nonce = secrets.token_hex(16)
    body_str = json.dumps(body) if body else ''
    signature = hmac.new(
        API_SECRET.encode(),
        f'{timestamp}.{body_str}'.encode(),
        hashlib.sha256
    ).hexdigest()

    resp = requests.request(method, BASE_URL + path,
        headers={
            'X-API-KEY': API_KEY,
            'X-SIGNATURE': signature,
            'X-TIMESTAMP': timestamp,
            'X-NONCE': nonce,
            'Content-Type': 'application/json',
        },
        data=body_str or None)
    return resp.json()

# Creer un paiement
payment = payhub_request('POST', '/api/v1/payment-intents', {
    'amount': 5000,
    'currency': 'XAF',
    'provider': 'mtn_momo',
    'customer_identifier': '242060000000',
    'environment': 'production',
})
print(f"ID: {payment['id']}, Status: {payment['status']}")

cURL

TIMESTAMP=$(date +%s)
BODY='{"amount":5000,"currency":"XAF","provider":"mtn_momo","customer_identifier":"242060000000","environment":"production"}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "$API_SECRET" | cut -d' ' -f2)

curl -X POST https://pay.example.com/api/v1/payment-intents \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: $API_KEY" \
  -H "X-SIGNATURE: $SIGNATURE" \
  -H "X-TIMESTAMP: $TIMESTAMP" \
  -H "X-NONCE: $(uuidgen)" \
  -d "$BODY"