Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.sahelpay.ml/llms.txt

Use this file to discover all available pages before exploring further.

Guide d’intégration

Ce guide détaille le flow complet d’intégration SahelPay pour une application marchande.

Principes fondamentaux

Règles non négociables :
  • Votre app est un MERCHANT SahelPay, pas un PSP
  • Votre app NE calcule AUCUN frais - SahelPay gère tout
  • Le webhook est la SEULE source de vérité pour le statut paiement

Architecture

Flow détaillé

1. Créer le paiement (Backend)

// Votre API route: POST /api/payments/create
const response = await fetch('https://api.sahelpay.ml/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${SAHELPAY_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'X-Idempotency-Key': `order-${orderId}`, // Important!
  },
  body: JSON.stringify({
    amount: 5000,
    currency: 'XOF',
    payment_method: 'ORANGE_MONEY', // ou WAVE, MOOV, CARD, VISA, MASTERCARD, GIM_UEMOA
    country: 'ML',
    customer_phone: '+22370123456',
    return_url: `${APP_URL}/checkout/return?order_id=${orderId}`,
    client_reference: orderId,
    metadata: { order_id: orderId }
  })
});

const { data } = await response.json();
// Retourner data.redirect_url au frontend
Pour les paiements par carte (CARD, VISA, MASTERCARD, GIM_UEMOA), ajoutez customer_name et customer_email dans le body.

2. Rediriger le client

// Frontend
window.location.href = redirectUrl;

3. Recevoir le webhook

// POST /api/webhooks/sahelpay
export async function POST(request) {
  const rawBody = await request.text();
  const signature = request.headers.get('x-sahelpay-signature');

  // TOUJOURS vérifier la signature
  if (!verifySignature(rawBody, signature, WEBHOOK_SECRET)) {
    return Response.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const { event, data } = JSON.parse(rawBody);

  // Idempotence: vérifier si déjà traité
  const existing = await db.payments.findByTransactionId(data.id);
  if (existing?.status === 'success') {
    return Response.json({ received: true, already_processed: true });
  }

  switch (event) {
    case 'payment.success':
      await db.orders.update(data.metadata.order_id, { status: 'paid' });
      break;
    case 'payment.failed':
      // Laisser la commande en pending pour retry
      break;
  }

  return Response.json({ received: true });
}

Idempotence

Côté création

Utilisez X-Idempotency-Key basé sur l’order_id :
headers: {
  'X-Idempotency-Key': `order-${orderId}`
}

Côté webhook

Vérifiez si le paiement est déjà traité avant de mettre à jour :
const existing = await db.payments.findByTransactionId(data.id);
if (existing?.status === 'success') {
  return { already_processed: true };
}

Ce que votre app NE DOIT PAS faire

❌ Appeler les providers directement

Orange, Wave, Moov… Tout passe par SahelPay

❌ Calculer des frais

SahelPay gère les frais automatiquement

❌ Créer un wallet

Pas de stockage de solde dans votre app

❌ Marquer PAID sans webhook

Le return_url est pour l’UX uniquement