Must Know
-
Upon payment success or failure, the gateway will redirect the user to the provided
failUrl
orreturnUrl
and call thewebhook_url
if one is provided. -
User must implement payment and confirmation pages with these specific criteria:
-
The site must have SSL security.
-
The redirection to the SATIM payment page must be done on a web browser independent of the merchant's website or application.
-
The payment page show the final amount to pay, CIB logo, captcha, checkbox to accept the general conditions of online payment.
-
Display all the fields of the ePay confirmation. The fields are:
-
Display this message on the confirmation page "En cas de problème de paiement, veuillez contacter le numéro vert de la SATIM 3020
"
-
From confirmation page, web merchant must allow:
-
Printing receipt
-
Downloading receipt as PDF
-
Sending receipt via email
-
-
-
It is possible to request status of your transaction on endpoint
/gateway/status/details/
with a max of 5 times per minunte. More than that, your request will get throttled. -
The gateway includes a built-in safety mechanism to handle potential failures. It automatically monitors transactions that remain pending for an extended period, updates their status, and notifies your system via the webhook. To ensure smooth operations and avoid missed updates, we strongly recommend implementing disaster recovery to fully leverage this feature.
-
Webhooks may be called more than once for the same transaction, so ensure your code is designed to handle multiple notifications gracefully.
-
All timestamps are based on the
Algeria/Algiers
timezone. -
For both redirection URLs (returnUrl, failUrl) and webhook calls, the user must recalculate the signature and compare it with the one sent by the gateway to verify the legitimacy of the request. The gateway calculates the hash by applying HMAC using your API secret key on the data 'invoice_id': ID-OF-ORDER, 'total': str(PRICE-TO-PAY). Here's how the hash is included in each case:
-
Redirection URLs:
The hash is appended to the URL as a query parameter along with the id of the order (e.g.,
?id=14&hash=abc123
) to ensure the request originates from the gateway. -
Webhook Calls:
The hash is included in the payload headers (e.g.,
X-Signature: abc123
) to authenticate the incoming request.
-
-
Here is an example of a webhook payload of a successfully paid checkout:
{
"invoice_id": 99,
"satim_order_id": "UmXcQHI7aAYGUQAAFQTH",
"tamayyuz_epay_id": 1298,
"epay_amount": "2500.59",
"date": "2023-10-01T12:34:56Z",
"refunded": false,
"refund_amount": null,
"status": "S",
"description": "Transaction succeeded",
"cardholder_name": "",
"satim_description": "Votre paiement a été accepté",
"approval_code": 123456,
"return_url": "https://your-website.com/return",
"fail_url": "https://your-website.com/fail",
}
- Here is an example of a webhook endpoint you may implement to receive our webhook calls.
- Python
- JavaScript/TypeScript
- PHP (Laravel)
class EpayWebhookView(views.APIView):
def post(self, request):
invoice_id = request.data.get('invoice_id')
try:
invoice = Invoice.objects.get(id=invoice_id)
except ObjectDoesNotExist:
return Response({"detail": "Invoice does not exist..."}, status=status.HTTP_404_NOT_FOUND)
# If invoice already confirmed, skip updating it again.
if invoice.status == 'S' or invoice.status == 'F':
return Response({"detail": _("Invoice already updated...")}, status=status.HTTP_200_OK)
# Verify hash
total = invoice.total
hash_value = self.request.headers.get('X-Signature')
data = {'invoice_id': invoice_id, 'total': str(total)}
# Ensure consistent formatting
json_string = json.dumps(data, separators=(',', ':'), sort_keys=True, default=str).encode('utf-8')
calculated_signature = hmac.new(
EPAY_SECRET_KEY.encode('utf-8'),
msg=json_string,
digestmod=hashlib.sha256
).hexdigest().upper()
if calculated_signature != hash_value:
return Response({"detail": _('Invalid hash')}, status=status.HTTP_400_BAD_REQUEST)
# Update invoice status
try:
if request.data.get('status') == 'S':
invoice.status = 'S'
invoice.save(update_fields=['status'])
except Exception as e:
return Response({"detail": "Failed to update Invoice - Error: %(formatted_text)s" % {"formatted_text": str(e)}}, status=status.HTTP_404_NOT_FOUND)
return Response({"detail": "Invoice has been updated"}, status=status.HTTP_200_OK)
import express from 'express';
import crypto from 'crypto';
import { Invoice } from '../models/Invoice.js'; // Replace with your model
const router = express.Router();
// replace with your webhook endpoint
router.post('/epay-webhook', async (req, res) => {
const { invoice_id } = req.body;
try {
const invoice = await Invoice.findByPk(invoice_id);
if (!invoice) {
return res.status(404).json({ detail: "Invoice does not exist..." });
}
if (invoice.status === 'S' || invoice.status === 'F') {
return res.status(200).json({ detail: "Invoice already updated..." });
}
// Verify hash
const total = invoice.total;
const hashValue = req.headers['x-signature'];
const data = { invoice_id, total: total.toString() };
const jsonString = JSON.stringify(data, Object.keys(data).sort(), 0);
const calculatedSignature = crypto
.createHmac('sha256', process.env.EPAY_SECRET_KEY)
.update(jsonString)
.digest('hex')
.toUpperCase();
if (calculatedSignature !== hashValue) {
return res.status(400).json({ detail: "Invalid hash" });
}
// Update invoice status
if (req.body.status === 'S') {
await invoice.update({ status: 'S' });
}
return res.status(200).json({ detail: "Invoice has been updated" });
} catch (error) {
if (error.name === 'SequelizeEmptyResultError') { // Adjust based on your ORM
return res.status(404).json({ detail: "Invoice does not exist..." });
}
return res.status(404).json({
detail: `Failed to update Invoice - Error: ${error.message}`
});
}
});
export default router;
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Invoice;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class EpayWebhookController extends Controller
{
public function handleWebhook(Request $request)
{
$invoiceId = $request->input('invoice_id');
try {
$invoice = Invoice::findOrFail($invoiceId);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return response()->json(['detail' => 'Invoice does not exist...'], 404);
}
if ($invoice->status === 'S' || $invoice->status === 'F') {
return response()->json(['detail' => 'Invoice already updated...'], 200);
}
// Verify hash
$total = $invoice->total;
$hashValue = $request->header('X-Signature');
$data = ['invoice_id' => $invoiceId, 'total' => (string)$total];
ksort($data); // Ensure keys are sorted
$jsonString = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$calculatedSignature = strtoupper(
hash_hmac('sha256', $jsonString, env('EPAY_SECRET_KEY'))
);
if ($calculatedSignature !== $hashValue) {
return response()->json(['detail' => 'Invalid hash'], 400);
}
// Update invoice status
try {
if ($request->input('status') === 'S') {
$invoice->status = 'S';
$invoice->save(['timestamps' => false]); // Disable timestamps if needed
}
} catch (\Exception $e) {
return response()->json([
'detail' => "Failed to update Invoice - Error: {$e->getMessage()}"
], 404);
}
return response()->json(['detail' => 'Invoice has been updated'], 200);
}
}