Skip to content

Quick start

Introduction

The Senfenico API offers two integration methods for payment processing: with redirection and without redirection, catering to diverse application needs.

With Redirection

  • Advantage: Streamlined user flow 🚀; users are redirected to Senfenico's secure payment page.
  • Disadvantage: Potential disruption in user experience as users exit the native application or website.
  • Workflow:
  • Initialize Checkout: Start the payment process by calling the designated endpoint.
  • Redirect the Customer: Send users to the provided authorization URL.

Without Redirection

  • Advantage: Complete control over the user experience, with all steps handled within your application or website.
  • Disadvantage: Increased complexity as you have to handle all the payment steps, including OTP (One-Time Password) submission if required.
  • Workflow:
  • Create a Charge: Set up the payment details.
  • Customer Action: Prompt users to approve the transaction via their mobile device.
  • Submit OTP: Submit the OTP received by the client.

Accepting Payment with Redirection

1️⃣ First Step: Initialize Checkout

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')
Parameter Type Optional Description
email string Yes Customer's email address
amount int No Amount to charge
success_url string No URL to redirect on success
cancel_url string No URL to redirect if the customer cancels

Sample Response

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
    }
}
Response Element Type Description
reference string Unique reference for the checkout
authorization_url string URL to redirect the user to proceed with the payment

2️⃣ Second Step: Redirect the User

Direct the user to the authorization_url to complete the payment process.

Accepting Payment Without Redirection

1️⃣ First Step: Create a 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.
  • For coris_bf: You can use any Burkina-Faso mobile phone number
  • For sank_bf: You can use any Burkina-Faso mobile phone number

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
    }
}

Interpretation of Responses:

  • For orange_bf: The customer must compose the provided USSD code to obtain an OTP and enter it on the platform.
  • For moov_bf: The customer will receive an OTP that must be entered on the platform in order to complete the payment.
  • For coris_bf: The customer will receive an OTP that must be entered on the platform in order to complete the payment.
  • For sank_bf: The customer will receive an OTP that must be entered on the platform in order to complete the payment.

2️⃣ Second Step: Customer Action

Instruct the customer to follow the provided instructions for payment validation.

3️⃣ Third Step: Submit OTP

Submit the OTP for a charge.

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"
}
Response Element Type Description
reference string Reference of the charge
amount int Charged amount
fees float Transaction fees in percent (e.g., 2%)
currency string Currency code ["XOF"]
status string Status of the charge
live_mode bool Indicates if it's a live transaction
payment_method string Payment method ["mobile_money"]
provider string Provider name ["orange_bf", "moov_bf", "coris_bf", "sank_bf"]
cancelled_at null Cancellation time (if any)
cancellation_reason null Reason for cancellation (if any)

📣 Webhooks

Independent of the chosen integration method, it's crucial to implement webhooks. This allows the handling of payment events or any other subscribed events.

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)

Importance of Verifying the Hash Verifying the hash is crucial for ensuring the integrity and security of the data received in webhooks. It helps to:

  1. Confirm Authenticity: Ensures that the payload comes from a trusted source, in this case, Senfenico.
  2. Prevent Tampering: Detects if the payload has been altered or tampered with during transmission.
  3. Enhance Security: Adds a layer of security by cross-verifying the data with a secret key known only to the sender and receiver.

Incorporating hash verification in your webhook implementations is a best practice for maintaining a secure and reliable integration.