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
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"
}'
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();
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();
Parameter | Type | Optional | Description |
---|---|---|---|
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
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
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"
}
}'
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();
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();
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:
|
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
{
"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
}
}
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.
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"
}'
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();
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();
Parameter | Type | Optional | Description |
---|---|---|---|
otp | string | No | One-Time Password (OTP) |
charge_reference | string | No | Reference of the charge |
Sample Response
{
"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"
}
]
}
}
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.
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);
}
}
// 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:
- Confirm Authenticity: Ensures that the payload comes from a trusted source, in this case, Senfenico.
- Prevent Tampering: Detects if the payload has been altered or tampered with during transmission.
- 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.