Skip to content

Démarrage rapide

Introduction

L'API Senfenico propose deux méthodes d'intégration pour le traitement des paiements : avec redirection et sans redirection, répondant ainsi aux divers besoins des applications.

Avec Redirection

  • Avantage : Parcours utilisateur simplifié 🚀; les utilisateurs sont redirigés vers la page de paiement sécurisée de Senfenico.
  • Inconvénient : Perturbation potentielle de l'expérience utilisateur lorsque les utilisateurs quittent l'application native ou le site web.
  • Processus :
  • Initialiser le paiement : Commencer le processus de paiement en appelant le point de terminaison désigné.
  • Rediriger le client : Envoyer les utilisateurs vers l'URL d'autorisation renvoyé par la première requête.

Sans Redirection

  • Avantage : Contrôle total de l'expérience utilisateur, toutes les étapes étant gérées au sein de votre application ou site web.
  • Inconvénient : Complexité accrue car vous devez gérer toutes les étapes du paiement, y compris la soumission du OTP (One-Time Password) si nécessaire.
  • Processus :
  • Créer un paiement : Configurer les détails du paiement.
  • Action du client : Demander aux utilisateurs d'approuver la transaction sur leur téléphone.
  • Soumettre le code OTP : Soumettre le OTP reçu par le client.

Accepter un Paiement avec Redirection

1️⃣ Première Étape : Initialiser le Paiement

POST /v1/payment/checkouts/initialize/
curl --location 'https://api.senfenico.com/v1/payment/checkouts/initialize/' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'X-API-KEY: sk_test_...' \
--data '{
    "email": "customer@mail.com",
    "amount": 600,
    "success_url": "https://example.com/sucess",
    "cancel_url": "https://example.com/cancel"
}'
POST /v1/payment/checkouts/initialize/
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\n  \"email\": \"customer@mail.com\",\n  \"amount\": 600,\n  \"success_url\": \"https://example.com/sucess\",\n  \"cancel_url\": \"https://example.com/cancel\"\n}");
Request request = new Request.Builder()
.url("https://api.senfenico.com/v1/payment/checkouts/initialize/")
.method("POST", body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("X-API-KEY", "sk_test_...")
.build();
Response response = client.newCall(request).execute();
POST /v1/payment/checkouts/initialize/
var https = require('follow-redirects').https;
var fs = require('fs');

var options = {
    'method': 'POST',
    'hostname': 'api.senfenico.com',
    'path': '/v1/payment/checkouts/initialize/',
    'headers': {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-API-KEY': 'sk_test_...'
    },
    'maxRedirects': 20
};

var req = https.request(options, function (res) {
    var chunks = [];

    res.on("data", function (chunk) {
        chunks.push(chunk);
    });

    res.on("end", function (chunk) {
        var body = Buffer.concat(chunks);
        console.log(body.toString());
    });

    res.on("error", function (error) {
        console.error(error);
    });
});

var postData = JSON.stringify({
    "email": "customer@mail.com",
    "amount": 600,
    "success_url": "https://example.com/sucess",
    "cancel_url": "https://example.com/cancel"
});

req.write(postData);

req.end();
POST /v1/payment/checkouts/initialize/
$senfenico = new \Senfenico\Senfenico('sk_test_...');
$senfenico->checkout->initialize([
    'amount' => 1000,
    'success_url' => 'https://yourwebsite.com/success',
    'cancel_url' => 'https://yourwebsite.com/cancel',
]);
POST /v1/payment/checkouts/initialize/
import senfenico
senfenico.api_key = 'sk_test_...'

senfenico.Checkout.initialize(
    amount=100, 
    success_url='http://website.com/success', 
    cancel_url='http://www.website.com/cancel')
Paramètre Type Optionnel Description
email chaîne Oui Adresse e-mail du client
amount entier Non Montant à facturer
success_url chaîne Non URL de redirection en cas de succès
cancel_url chaîne Non URL de redirection si le client annule

Exemple de reponse

Checkout initialized
{
    "status": true,
    "message": "Checkout initialized",
    "data": {
        "reference": "chk_de1ca053573c40a29a9fe95e7ad91d7e",
        "authorization_url": "https://api.senfenico.com/v1/checkout_payment/payment_page/chk_de1ca053573c40a29a9fe95e7ad91d7e/",
        "live_mode": false
    }
}
Élément de Réponse Type Description
référence chaîne Référence unique pour le paiement
url_autorisation chaîne URL pour rediriger l'utilisateur afin de procéder au paiement

2️⃣ Deuxième Étape : Rediriger l'Utilisateur

Dirigez l'utilisateur vers l'authorization_url pour compléter le processus de paiement.

Accepter un Paiement Sans Redirection

1️⃣ Première Étape : Créer une Charge

POST /v1/payment/charges/
curl --location 'https://api.senfenico.com/v1/payment/charges/' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'X-API-KEY: sk_test_...' \
--data '{
    "amount": "500",
    "currency": "XOF",
    "payment_method": "mobile_money",
    "payment_method_details": {
        "phone": "76xxxxxx",
        "provider": "orange_bf"
    }
}'
POST /v1/payment/charges/
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\n  \"amount\": \"500\",\n  \"currency\": \"XOF\",\n  \"payment_method\": \"mobile_money\",\n  \"payment_method_details\": {\n    \"phone\": \"76xxxxxx\",\n    \"provider\": \"orange_bf\"\n  }\n}");
Request request = new Request.Builder()
    .url("https://api.senfenico.com/v1/payment/charges/")
    .method("POST", body)
    .addHeader("Content-Type", "application/json")
    .addHeader("Accept", "application/json")
    .addHeader("X-API-KEY", "sk_test_...")
    .build();
Response response = client.newCall(request).execute();
POST /v1/payment/charges/
var https = require('follow-redirects').https;
var fs = require('fs');

var options = {
    'method': 'POST',
    'hostname': 'api.senfenico.com',
    'path': '/v1/payment/charges/',
    'headers': {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-API-KEY': 'sk_test_...'
    },
    'maxRedirects': 20
};

var req = https.request(options, function (res) {
var chunks = [];

res.on("data", function (chunk) {
    chunks.push(chunk);
});

res.on("end", function (chunk) {
    var body = Buffer.concat(chunks);
    console.log(body.toString());
});

res.on("error", function (error) {
    console.error(error);
});
});

var postData = JSON.stringify({
    "amount": "500",
    "currency": "XOF",
    "payment_method": "mobile_money",
    "payment_method_details": {
        "phone": "76xxxxxx",
        "provider": "orange_bf"
    }
});

req.write(postData);

req.end();
POST /v1/payment/charges/
$senfenico = new \Senfenico\Senfenico('sk_test_...');
$senfenico->charge->create([
    'amount' => 1000,
    'phone' => '76XXXXXX',
    'provider' => 'orange_bf'
]);
POST /v1/payment/charges/
import senfenico
senfenico.api_key = 'sk_test_...'

senfenico.Charge.create(amount=2000, phone='65000000', provider='orange_bf')
Parameter Type Optional Description
amount int No Amount to charge
currency string No Currency code ("XOF")
payment_method string No Payment method ("mobile_money")
payment_method_details object No Details about the payment method
phone string No your client phone number
provider object No the client payment platform operator.
Available options:
  • orange_bf
  • moov_bf
  • coris_bf
  • sank_bf

Note on Provider Constraints:

  • For orange_bf: Use an Orange BF phone number (8 digits). Valid prefixes: 04, 05, 06, 07, 54, 55, 56, 57, 64, 65, 66, 67, 74, 75, 76, 77.
  • For moov_bf: Use a Moov BF phone number (8 digits). Valid prefixes: 70, 71, 72, 73, 60, 61, 62, 63, 50, 51, 52, 53, 01, 02, 03.
  • Pour coris_bf: Vous pouvez utiliser n'importe quelle numero de téléphone portable burkinabè
  • Pour sank_bf: Vous pouvez utiliser n'importe quelle numero de téléphone portable burkinabè

Sample Response

provider:orange_bf sample response
{
    "status": true,
    "message": "Charge attempted",
    "data": {
        "reference": "ch_1ba1b4d1268b41079ee84c777e999f51",
        "status": "send_otp",
        "display_text": "Composez *144*4*6*500# pour obtenir un code OTP, puis entrez-le sur notre plateforme pour finaliser la transaction.",
        "live_mode": false
    }
}
provider:moov_bf sample response
{
    "status": true,
    "message": "Charge attempted",
    "data": {
        "reference": "ch_4f60d14ace534fd19ee3cce88fd6ba73",
        "status": "send_otp",
        "display_text": "Veuillez entrer le code OTP reçu sur votre téléphone pour finaliser la transaction.",
        "live_mode": false
    }
}

Interprétation des réponses

  • Pour orange_bf : Le client doit composer le code USSD fourni afin de recevoir un OTP, qu'il devra ensuite saisir sur la plateforme.
  • Pour moov_bf : Le client recevra un OTP (sans action de sa part), qu'il devra ensuite saisir sur la plateforme.
  • Pour coris_bf : Le client recevra un OTP (sans action de sa part), qu'il devra ensuite saisir sur la plateforme.
  • Pour sank_bf : Le client recevra un OTP (sans action de sa part), qu'il devra ensuite saisir sur la plateforme.

2️⃣ Deuxième étape : Action requise de la part du client

Demandez au client de suivre les instructions données pour confirmer le paiement.

3️⃣ Troisième étape : Saisie de l'OTP

Procédez à la saisie de l'OTP pour effectuer la transaction.

POST /v1/payment/charges/submit
curl --location 'https://api.senfenico.com/v1/payment/charges/submit' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'X-API-KEY: sk_test_...' \
--data '{
    "otp": "123456",
    "charge_reference": "ch_39728149f97a46f1806a694dbb620b23"
}'
POST /v1/payment/charges/submit
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\n  \"otp\": \"123456\",\n  \"charge_reference\": \"ch_39728149f97a46f1806a694dbb620b23\"\n}");
Request request = new Request.Builder()
    .url("https://api.senfenico.com/v1/payment/charges/submit")
    .method("POST", body)
    .addHeader("Content-Type", "application/json")
    .addHeader("Accept", "application/json")
    .addHeader("X-API-KEY", "sk_test_...")
    .build();
Response response = client.newCall(request).execute();
POST /v1/payment/charges/submit
var https = require('follow-redirects').https;
var fs = require('fs');

var options = {
    'method': 'POST',
    'hostname': 'api.senfenico.com',
    'path': '/v1/payment/charges/submit',
    'headers': {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-API-KEY': 'sk_test_...'
    },
    'maxRedirects': 20
};

var req = https.request(options, function (res) {
var chunks = [];

res.on("data", function (chunk) {
    chunks.push(chunk);
});

res.on("end", function (chunk) {
    var body = Buffer.concat(chunks);
    console.log(body.toString());
});

res.on("error", function (error) {
        console.error(error);
    });
});

var postData = JSON.stringify({
    "otp": "123456",
    "charge_reference": "ch_39728149f97a46f1806a694dbb620b23"
});

req.write(postData);

req.end();
POST /v1/payment/charges/submit
$senfenico = new \Senfenico\Senfenico('sk_test_...');
$senfenico->charge->submitOtp([
    'otp' => 123456,
    'charge_reference' => 'ch_39728149f97a46f1806a694dbb620b23',
]);
POST /v1/payment/charges/submit
import senfenico
senfenico.api_key = 'sk_test_...'

senfenico.Charge.submit_otp(otp="123456", charge_reference="ch_39728149f97a46f1806a694dbb620b23")
Parameter Type Optional Description
otp string No One-Time Password (OTP)
charge_reference string No Reference of the charge

Sample Response

Charge completed successfully
{
    "status": true,
    "message": "charge completed successfully",
    "data": {
        "reference": "ch_39728149f97a46f1806a694dbb620b23",
        "amount": 1000,
        "fees": 3.5,
        "currency": "XOF",
        "transaction_date": "2024-08-18T11:43:47.380565+00:00",
        "ip_address": "172.18.0.1",
        "status": "success",
        "live_mode": false,
        "payment_method": "mobile_money",
        "provider": "orange_bf",
        "cancelled_at": null,
        "cancellation_reason": null,
        "confirmation_attempts": [
            {
                "ip_address": "172.18.0.1",
                "attempt_date": "2024-08-18T11:44:16.683423+00:00",
                "confirmation_status": "success"
            }
        ]
    }
}
OTP submission failed
{
    "status": false,
    "message": "Charge operation failed. The OTP is invalid. Please try again.",
    "error_code": "OTP_INVALID"
}
Élément de Réponse Type Description
reference chaîne Référence de la transaction
amount entier Montant facturé
fees flottant Frais de transaction en pourcentage (ex. : 2 %)
currency chaîne Code de devise ["XOF"]
status chaîne Statut de la transaction
live_mode booléen Indique s'il s'agit d'une transaction en direct
payment_method chaîne Méthode de paiement ["mobile_money"]
provider chaîne Nom du fournisseur ["orange_bf", "moov_bf", "coris_bf" , "sank_bf"]
cancelled_at null Heure d'annulation (si applicable)
cancellation_reason null Raison de l'annulation (si applicable)

📣 Webhooks

Indépendamment de la méthode d'intégration choisie, il est crucial d'implémenter des webhooks. Cela permet de gérer les événements de paiement ou tout autre événement.

Spring Boot Webhook Implementation
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@RestController
public class WebhookController {

    private static final String WEBHOOK_SECRET_KEY = "..."; // Replace with your webhook secret key

    @PostMapping("/webhook")
    public ResponseEntity<String> handleWebhook(@RequestBody String payload,
                                                @RequestHeader("X-Webhook-Hash") String receivedHash) {
        try {
            if (!verifyHash(payload, receivedHash)) {
                return ResponseEntity.status(400).body("Hash mismatch, data might be tampered");
            }

            // Process the webhook payload
            System.out.println(payload);

            return ResponseEntity.ok("Webhook received successfully");
        } catch (NoSuchAlgorithmException e) {
            return ResponseEntity.status(500).body("Server error: " + e.getMessage());
        }
    }

    private boolean verifyHash(String payload, String receivedHash) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = digest.digest((payload + WEBHOOK_SECRET_KEY).getBytes(StandardCharsets.UTF_8));
        String computedHash = Base64.getEncoder().encodeToString(hashBytes);
        return computedHash.equals(receivedHash);
    }
}
ExpressJS Webhook Implementation
// In an ExpressJS application, handling webhooks is straightforward. Here's a sample implementation:

const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');

const app = express();
app.use(bodyParser.json());

const webhookSecretKey = '...'; // Replace with your webhook secret key

function verifyHash(payload, receivedHash) {
    const hash = crypto.createHmac('sha256', webhookSecretKey)
                    .update(JSON.stringify(payload))
                    .digest('hex');
    return hash === receivedHash;
}

app.post('/webhook', (req, res) => {
    const receivedHash = req.headers['x-webhook-hash'];

    if (!verifyHash(req.body, receivedHash)) {
        return res.status(400).send({'error': 'Hash mismatch, data might be tampered'});
    }

    // Process the webhook payload
    console.log(req.body);

    res.status(200).send({'message': 'Webhook received successfully'});
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});
<?php
require_once 'senfenico-php/init.php';
$payload = file_get_contents('php://input');
$webhook_hash = $_SERVER['HTTP_X_WEBHOOK_HASH'];
$webhook_key = 'your_secret_webhook_key...';

try{
    $webhook = \Senfenico\Webhook::constructEvent($payload, $webhook_hash, $webhook_key);
} catch(\Exception $e) {
    // Invalid payload
    http_response_code(400);
    exit;
}

// Handle the event
switch ($webhook->event) {
    case 'checkout.pending':
        //handle checkout pending event
        break;
    case 'checkout.success':
        //chandle checkout success event
        break;
    // ... handle other event types
    default:
        echo 'Received unknown event type ' . $webhook->event;
}

http_response_code(200);
exit;
$payload = $request->getContent();
$webhook_hash = $request->header('X-Webhook-Hash');
$webhook_key = '9e191505-9637-4c2b-a55e-da813b882732';

try{
    $webhook = \Senfenico\Webhook::constructEvent($payload, $webhook_hash, $webhook_key);
} catch(\Exception $e) {
    Log::debug('Message: ' .$e->getMessage());
    return response()->json(['message' => 'une erreur sest produite'], 400);
}

// Handle the event
switch ($webhook->event) {
    case 'checkout.pending':
        //handle checkout pending event
        break;
    case 'checkout.success':
        //chandle checkout success event
        break;
    // ... handle other event types
    default:
        Log::debug('Received unknown event type ' . $webhook->event);
}

return response()->json(['message' => 'Webhook received'], 200);
webhook_key = 'your_secret_webhook_key...'  # replace with your secret key

@app.route('/webhook', methods=['POST'])
def handle_webhook():    
    # Extract the hash from headers
    received_hash = request.headers.get('X-Webhook-Hash')

    # Get the payload from the body
    payload = request.json
    try:
        webhook = senfenico.Webhook.construct_event(payload, received_hash, webhook_key)
    except ValueError as e:
        print('Invalid payload')
        return jsonify({'error': str(e)}), 400

    if webhook.event == 'checkout.pending':
        print('checkout pending')
    elif webhook.event == 'checkout.success':
        print('checkout completed')
    # ... handle other events
    else:
        print('Unhandled event type {}'.format(webhook.event))

    return jsonify({'message': 'Webhook received successfully'}), 200
webhook_key = 'your_webhook_secret_key...'  # replace with your secret key

@csrf_exempt
def handle_webhook(request):
    # Extract the hash from headers
    received_hash = request.headers.get('X-Webhook-Hash')

    # Get the payload from the body
    payload = request.body
    try:
        webhook = senfenico.Webhook.construct_event(payload, received_hash, webhook_key)
    except ValueError as e:
        print('Invalid payload')
        return HttpResponse(status=400)

    if webhook.event == 'checkout.pending':
        print('checkout pending')
    elif webhook.event == 'checkout.success':
        print('checkout completed')
    # ... handle other events
    else:
        print('Unhandled event type {}'.format(webhook.event))

    return HttpResponse(status=200)

L'Importance de Vérifier le Hash Vérifier le hash est essentiel pour garantir l'intégrité et la sécurité des données reçues via les webhooks. Cela contribue à :

  1. Confirmer l'Authenticité : S'assurer que les données proviennent d'une source fiable, en l'occurrence Senfenico.
  2. Empêcher les Altérations : Permet de détecter si les données ont été modifiées ou altérées durant leur transmission.
  3. Renforcer la Sécurité : Ajoute une couche supplémentaire de sécurité en vérifiant les données avec une clé secrète uniquement connue de l'expéditeur et du destinataire.

Intégrer la vérification de hash dans la mise en œuvre de vos webhooks est une pratique recommandée pour assurer une intégration sûre et fiable.