SATIM Backend Integration
Introduction: Why payment_attempts Exists
payment_attempts is the audit and control table for SATIM payment initiation and follow-up.
It matters because it lets backend teams:
- track every payment try from
initiatedto final acknowledge state - keep a strict link between merchant
orderNumberand SATIM identifiers (orderId/mdOrder) - persist raw request/response payloads for support, dispute handling, and certification evidence
- avoid orphan transactions by ensuring a transaction always references a known payment attempt
- debug gateway issues quickly using status history and gateway messages
- stay ready if SATIM requests technical logs during certification or incident review
In short, payment_attempts is the source of truth for SATIM flow traceability and reconciliation.
This guide assumes a platform using one payment terminal configuration.
For dynamic multi-terminal setups, the SATIM flow and status logic remain the same, but force_terminal_id, userName, and password must be resolved from database configuration using business rules tied to the user performing the payment.
1. Critical Rules (Must Follow)
Do not call register.do before persisting the payment_attempts row with status initiated.
orderNumber FormatorderNumber must be alphanumeric and exactly 10 characters.
Example: K9m2X7qL4P
Backend must implement a unique generation mechanism for orderNumber (per order/payment attempt) and verify it does not already exist in the database before calling register.do (use retry/regenerate on conflict).
Use a bounded retry strategy (max retry depth/attempts) to avoid infinite loops, even if collision probability is low; if max attempts are reached, fail fast and return an internal error.
During tests, avoid sending the same orderNumber to register.do more than once. SATIM registers that orderNumber, and reusing it can return duplicate/already-processed errors.
amount sent to SATIM must be in centimes.
Formula: amount_for_satim = amount * 100
Example: 5966.56 DZD must be sent as 596656.
Persist business monetary amounts as decimal values with 2-digit precision (for example DECIMAL(15,2)), not integers.
Use integer centimes only at SATIM API boundaries (request/response mapping), then convert back to decimal for storage.
Log and persist request/response payloads for each SATIM endpoint (register.do and acknowledgeTransaction.do).
These logs are required for traceability and may be requested later by SATIM during certification, incident analysis, or audit reviews.
2. Data Model and Status Lifecycle
2.1 payment_attempts Required Fields
user_idorder_number(unique)gateway_order_id(nullable, stores SATIMorderId/ same logical value asmdOrder)form_url(nullable)amount(DECIMAL(15,2))statuspayment_method(nullable)payment_gateway(nullable, defaultSATIM)register_request_payload(JSON raw payload sent to SATIMregister.do, nullable)register_response_payload(JSON raw payload received from SATIMregister.do, nullable)acknowledge_request_payload(JSON raw payload sent to SATIMacknowledgeTransaction.do, nullable)acknowledge_response_payload(JSON raw payload received from SATIMacknowledgeTransaction.do, nullable)ip_address(nullable)created_atupdated_at
CREATE TABLE payment_attempts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
order_number VARCHAR(10) NOT NULL UNIQUE,
gateway_order_id VARCHAR(255) NULL,
form_url VARCHAR(1024) NULL,
amount DECIMAL(15,2) NOT NULL,
status VARCHAR(40) NOT NULL DEFAULT 'initiated',
payment_method VARCHAR(100) NULL,
payment_gateway VARCHAR(100) NULL DEFAULT 'SATIM',
register_request_payload JSONB NULL,
register_response_payload JSONB NULL,
acknowledge_request_payload JSONB NULL,
acknowledge_response_payload JSONB NULL,
ip_address VARCHAR(45) NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
2.2 payment_attempts Statuses
Use this status set consistently:
initiatedregisteredregistered_failedacknowledgedacknowledge_failed
2.3 Lifecycle Updates
- Generate unique
orderNumber. - Insert
payment_attemptsrow with statusinitiated. - Save raw register request JSON in
register_request_payload. - Call
register.do. - Save raw register response JSON in
register_response_payload. - Update same row:
registeredon successregistered_failedon failure
- Save raw acknowledge request JSON in
acknowledge_request_payload. - Call
acknowledgeTransaction.do. - Save raw acknowledge response JSON in
acknowledge_response_payload. - Update same row:
acknowledgedon successacknowledge_failedon failure
- Persist
transactionsrow linked topayment_attempts(always).
2.4 transactions Minimum Recommendation
payment_attempt_id(foreign key topayment_attempts.id)reference(unique)authorization_number(nullable)statuspayment_method(nullable)payment_gateway(nullable, defaultSATIM)gateway_error_message(nullable)gateway_success_message(nullable)ip_address(nullable)created_atupdated_at
Once a transactions record is created, it must never be modified or altered, whether manually or by application logic.
Any correction/reversal must be handled by creating a new compensating record and preserving full audit traceability.
3. Register (register.do)
Use environment variables or secret management for credentials.
Recommended keys in this guide: SATIM_USER, SATIM_PASSWORD, SATIM_TERMINAL_ID.
Success criterion used in this guide: treat register as successful when orderId is present in the SATIM response.
- PHP
- Node.js
- .NET
- Go
- Python
$endpoint = 'https://test2.satim.dz/payment/rest/register.do';
$payload = [
'userName' => getenv('SATIM_USER'),
'password' => getenv('SATIM_PASSWORD'),
'orderNumber' => 'K9m2X7qL4P',
'amount' => 500000,
'currency' => '012',
'returnUrl' => 'https://merchant.example.com/payments/satim/return',
'failUrl' => 'https://merchant.example.com/payments/satim/fail',
'description' => 'Order K9m2X7qL4P',
'language' => 'FR',
'jsonParams' => json_encode([
'force_terminal_id' => getenv('SATIM_TERMINAL_ID'),
'udf1' => 'customer-12345',
'udf5' => 'invoice-7788',
]),
];
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
]);
$raw = curl_exec($ch);
curl_close($ch);
$data = json_decode($raw, true);
if (! empty($data['orderId'])) {
// payment_attempts.status = registered
// payment_attempts.register_request_payload = $payload
// payment_attempts.register_response_payload = $data
} else {
// payment_attempts.status = registered_failed
// payment_attempts.register_request_payload = $payload
// payment_attempts.register_response_payload = $data
}
const registerParams = new URLSearchParams({
userName: process.env.SATIM_USER ?? '',
password: process.env.SATIM_PASSWORD ?? '',
orderNumber: 'K9m2X7qL4P',
amount: '500000',
currency: '012',
returnUrl: 'https://merchant.example.com/payments/satim/return',
failUrl: 'https://merchant.example.com/payments/satim/fail',
description: 'Order K9m2X7qL4P',
language: 'FR',
jsonParams: JSON.stringify({
force_terminal_id: process.env.SATIM_TERMINAL_ID ?? '',
udf1: 'customer-12345',
udf5: 'invoice-7788',
}),
});
const registerRes = await fetch('https://test2.satim.dz/payment/rest/register.do', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: registerParams.toString(),
});
const data = await registerRes.json();
if (Boolean(data?.orderId)) {
// payment_attempts.status = registered
// payment_attempts.register_request_payload = Object.fromEntries(registerParams)
// payment_attempts.register_response_payload = data
} else {
// payment_attempts.status = registered_failed
// payment_attempts.register_request_payload = Object.fromEntries(registerParams)
// payment_attempts.register_response_payload = data
}
using System.Net.Http;
using System.Text.Json;
var endpoint = "https://test2.satim.dz/payment/rest/register.do";
var payload = new Dictionary<string, string>
{
["userName"] = Environment.GetEnvironmentVariable("SATIM_USER") ?? "",
["password"] = Environment.GetEnvironmentVariable("SATIM_PASSWORD") ?? "",
["orderNumber"] = "K9m2X7qL4P",
["amount"] = "500000",
["currency"] = "012",
["returnUrl"] = "https://merchant.example.com/payments/satim/return",
["failUrl"] = "https://merchant.example.com/payments/satim/fail",
["description"] = "Order K9m2X7qL4P",
["language"] = "FR",
["jsonParams"] = JsonSerializer.Serialize(new Dictionary<string, string>
{
["force_terminal_id"] = Environment.GetEnvironmentVariable("SATIM_TERMINAL_ID") ?? "",
["udf1"] = "customer-12345",
["udf5"] = "invoice-7788"
})
};
using var http = new HttpClient();
using var content = new FormUrlEncodedContent(payload);
using var response = await http.PostAsync(endpoint, content);
var raw = await response.Content.ReadAsStringAsync();
using var json = JsonDocument.Parse(raw);
var hasOrderId = json.RootElement.TryGetProperty("orderId", out var orderId)
&& !string.IsNullOrWhiteSpace(orderId.GetString());
if (hasOrderId)
{
// payment_attempts.status = registered
// payment_attempts.register_request_payload = payload
// payment_attempts.register_response_payload = parsed JSON response
}
else
{
// payment_attempts.status = registered_failed
// payment_attempts.register_request_payload = payload
// payment_attempts.register_response_payload = parsed JSON response
}
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
)
func main() {
endpoint := "https://test2.satim.dz/payment/rest/register.do"
jsonParams, _ := json.Marshal(map[string]string{
"force_terminal_id": os.Getenv("SATIM_TERMINAL_ID"),
"udf1": "customer-12345",
"udf5": "invoice-7788",
})
payload := url.Values{
"userName": {os.Getenv("SATIM_USER")},
"password": {os.Getenv("SATIM_PASSWORD")},
"orderNumber": {"K9m2X7qL4P"},
"amount": {"500000"},
"currency": {"012"},
"returnUrl": {"https://merchant.example.com/payments/satim/return"},
"failUrl": {"https://merchant.example.com/payments/satim/fail"},
"description": {"Order K9m2X7qL4P"},
"language": {"FR"},
"jsonParams": {string(jsonParams)},
}
resp, err := http.PostForm(endpoint, payload)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data map[string]any
_ = json.Unmarshal(body, &data)
orderID := strings.TrimSpace(fmt.Sprint(data["orderId"]))
if orderID != "" && orderID != "<nil>" {
// payment_attempts.status = registered
// payment_attempts.register_request_payload = payload
// payment_attempts.register_response_payload = data
} else {
// payment_attempts.status = registered_failed
// payment_attempts.register_request_payload = payload
// payment_attempts.register_response_payload = data
}
}
import json
import os
import requests
endpoint = "https://test2.satim.dz/payment/rest/register.do"
payload = {
"userName": os.getenv("SATIM_USER", ""),
"password": os.getenv("SATIM_PASSWORD", ""),
"orderNumber": "K9m2X7qL4P",
"amount": "500000",
"currency": "012",
"returnUrl": "https://merchant.example.com/payments/satim/return",
"failUrl": "https://merchant.example.com/payments/satim/fail",
"description": "Order K9m2X7qL4P",
"language": "FR",
"jsonParams": json.dumps(
{
"force_terminal_id": os.getenv("SATIM_TERMINAL_ID", ""),
"udf1": "customer-12345",
"udf5": "invoice-7788",
}
),
}
res = requests.post(
endpoint,
data=payload,
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=30,
)
data = res.json()
if str(data.get("orderId", "")).strip():
# payment_attempts.status = registered
# payment_attempts.register_request_payload = payload
# payment_attempts.register_response_payload = data
pass
else:
# payment_attempts.status = registered_failed
# payment_attempts.register_request_payload = payload
# payment_attempts.register_response_payload = data
pass
3.1 Request Parameters
| Name | Type | Mandatory | Description |
|---|---|---|---|
userName | AN..30 | Yes | Merchant login received during SATIM registration. |
password | AN..30 | Yes | Merchant password received during SATIM registration. |
orderNumber | AN..10 | Yes | Merchant order identifier. Must be unique per transaction. |
amount | N..20 | Yes | Minimum amount is 50 DA. Must be sent in centimes (amount * 100). |
currency | N3 | Yes | ISO 4217 currency code (012 for DZD). |
returnUrl | AN..512 | Yes | Redirect URL after successful payment. |
failUrl | AN..512 | No | Redirect URL after failed payment. |
description | AN..512 | No | Free-form order description. |
language | A2 | Yes | ISO 639-1 language (AR, FR, EN). |
jsonParams | AN..1024 | Yes | Additional parameters map ({"param":"value"}) agreed during integration. |
amount must be in centimes.
5000 DA=>500000806.5 DA=>80650
3.2 jsonParams Parameter List
| Field | Format | Example | Mandatory | Description |
|---|---|---|---|---|
force_terminal_id | AN..16 | E0123456789 | Yes | Terminal ID assigned by bank to merchant. |
udf1 | AN..20 | Cmd123456 | Yes | Merchant custom value (invoice/order context). |
udf2 | AN..20 | value2 | No | Optional SATIM-specific custom value. |
udf3 | AN..20 | value3 | No | Optional SATIM-specific custom value. |
udf4 | AN..20 | value4 | No | Optional SATIM-specific custom value. |
udf5 | AN..20 | value5 | No | Optional SATIM-specific custom value. |
3.3 SATIM-Specific Optional Parameter
| Name | Method | Type | Description |
|---|---|---|---|
fundingTypeIndicator | register.do (in jsonParams[]) | String | Payment transaction type indicator. Current values: CP or 698 (Bill payment). |
3.4 Register Example URL
https://test2.satim.dz/payment/rest/register.do?userName=xxxxxxxx&password=xxxxxxxx&orderNumber=K9m2X7qL4P&amount=500000¤cy=012&language=fr&returnUrl=https://...&failUrl=https://...&jsonParams={"force_terminal_id":"${SATIM_TERMINAL_ID}","udf1":"2018105301346","udf5":"ggsf85s42524s5uhgsf"}
3.5 Response Parameters
| Name | Type | Mandatory | Description |
|---|---|---|---|
errorCode | N3 | No | 0 on success, other values indicate request/processing errors. |
orderId | ANS20 | No | Unique EPG order id (same logical value as mdOrder). Absent if registration fails. |
formUrl | AN..512 | No | SATIM payment page URL for customer redirection. Absent if registration fails. |
3.6 Register Error Codes
| Code | Message |
|---|---|
0 | No system error. |
1 | Order with given order number has already been processed or the childId is incorrect. |
1 | Order with this number was registered but not paid. |
1 | Submerchant is blocked or deleted. |
3 | Unknown currency. |
4 | Order number is not specified. |
4 | Merchant user name is not specified. |
4 | Amount is not specified. |
4 | Return URL cannot be empty. |
4 | Password cannot be empty. |
5 | Incorrect value of a request parameter. |
5 | Incorrect value in the Language parameter. |
5 | Access is denied. |
5 | Merchant must change the password. |
5 | Invalid jsonParams[]. |
7 | System error. |
14 | Paymentway is invalid. |
4. Acknowledge (acknowledgeTransaction.do)
acknowledgeTransaction.do confirms final order/payment details after customer redirection.
If this confirmation is not sent, the gateway may automatically cancel the order after a delay.
- PHP
- Node.js
- .NET
- Go
- Python
$endpoint = 'https://test2.satim.dz/payment/rest/public/acknowledgeTransaction.do';
$payload = [
'userName' => getenv('SATIM_USER'),
'password' => getenv('SATIM_PASSWORD'),
'mdOrder' => 'V721uPPfNNofVQAAABL3',
'language' => 'FR',
];
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
]);
$raw = curl_exec($ch);
curl_close($ch);
$data = json_decode($raw, true);
$isSuccess = isset($data['ErrorCode']) && (int) $data['ErrorCode'] === 0
&& isset($data['params']['respCode']) && $data['params']['respCode'] === '00'
&& isset($data['OrderStatus']) && (int) $data['OrderStatus'] === 2;
// derive gateway messages
$respCodeDesc = trim((string)($data['params']['respCode_desc'] ?? ''));
$actionCodeDescription = trim((string)($data['actionCodeDescription'] ?? ''));
$language = strtoupper((string)($payload['language'] ?? 'EN'));
$localizedRejected = match ($language) {
'FR' => 'Votre transaction a ete rejetee',
'AR' => 'تم رفض معاملتك',
default => 'Your transaction was rejected',
};
$gatewaySuccessMessage = null;
$gatewayErrorMessage = null;
if ($isSuccess) {
$attemptStatus = 'acknowledged';
$transactionStatus = 'acknowledged';
$gatewaySuccessMessage = $respCodeDesc !== '' ? $respCodeDesc : $actionCodeDescription;
} else {
$attemptStatus = 'acknowledge_failed';
$transactionStatus = 'acknowledge_failed';
$isRejectedCase1 = (string)($data['params']['respCode'] ?? '') === '00'
&& (int)($data['ErrorCode'] ?? -1) === 0
&& (int)($data['OrderStatus'] ?? -1) === 3;
$gatewayErrorMessage = $isRejectedCase1
? $localizedRejected
: ($respCodeDesc !== '' ? $respCodeDesc : $actionCodeDescription);
}
// update payment_attempts
// - status = $attemptStatus
// - acknowledge_request_payload = $payload
// - acknowledge_response_payload = $data
$paymentAttemptId = $paymentAttempt['id']; // current payment_attempts.id from DB context
$merchantReference = 'TXN-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(3))); // generate unique internal reference
$transactionData = [
'payment_attempt_id' => $paymentAttemptId,
'reference' => $merchantReference,
'authorization_number' => $data['approvalCode'] ?? $data['authorizationResponseId'] ?? null,
'status' => $transactionStatus,
'payment_method' => isset($data['Pan']) ? 'CIB/EDAHABIA' : null,
'payment_gateway' => 'SATIM',
'gateway_success_message' => $gatewaySuccessMessage,
'gateway_error_message' => $gatewayErrorMessage,
'ip_address' => $data['Ip'] ?? null,
];
// persist transactions row with $transactionData (always, even on failure)
const ackParams = new URLSearchParams({
userName: process.env.SATIM_USER ?? '',
password: process.env.SATIM_PASSWORD ?? '',
mdOrder: 'V721uPPfNNofVQAAABL3',
language: 'FR',
});
const ackRes = await fetch(
'https://test2.satim.dz/payment/rest/public/acknowledgeTransaction.do',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: ackParams.toString(),
},
);
const data = await ackRes.json();
const isSuccess =
Number(data?.ErrorCode) === 0 &&
data?.params?.respCode === '00' &&
Number(data?.OrderStatus) === 2;
const respCodeDesc = String(data?.params?.respCode_desc ?? '').trim();
const actionCodeDescription = String(data?.actionCodeDescription ?? '').trim();
const language = String(ackParams.get('language') ?? 'EN').toUpperCase();
const localizedRejected =
language === 'FR'
? 'Votre transaction a ete rejetee'
: language === 'AR'
? 'تم رفض معاملتك'
: 'Your transaction was rejected';
let attemptStatus;
let transactionStatus;
let gatewaySuccessMessage = null;
let gatewayErrorMessage = null;
if (isSuccess) {
attemptStatus = 'acknowledged';
transactionStatus = 'acknowledged';
gatewaySuccessMessage = respCodeDesc || actionCodeDescription;
} else {
attemptStatus = 'acknowledge_failed';
transactionStatus = 'acknowledge_failed';
const isRejectedCase1 =
data?.params?.respCode === '00' &&
Number(data?.ErrorCode) === 0 &&
Number(data?.OrderStatus) === 3;
gatewayErrorMessage = isRejectedCase1
? localizedRejected
: respCodeDesc || actionCodeDescription;
}
// update payment_attempts
// - status = attemptStatus
// - acknowledge_request_payload = Object.fromEntries(ackParams)
// - acknowledge_response_payload = data
const paymentAttemptId = paymentAttempt.id; // current payment_attempts.id from DB context
const merchantReference = `TXN-${Date.now()}-${Math.random().toString(36).slice(2, 8).toUpperCase()}`; // generate unique internal reference
const transactionData = {
payment_attempt_id: paymentAttemptId,
reference: merchantReference,
authorization_number: data?.approvalCode ?? data?.authorizationResponseId ?? null,
status: transactionStatus,
payment_method: data?.Pan ? 'CIB/EDAHABIA' : null,
payment_gateway: 'SATIM',
gateway_success_message: gatewaySuccessMessage,
gateway_error_message: gatewayErrorMessage,
ip_address: data?.Ip ?? null,
};
// persist transactions row with transactionData (always, even on failure)
using System.Net.Http;
using System.Text.Json;
var endpoint = "https://test2.satim.dz/payment/rest/public/acknowledgeTransaction.do";
var payload = new Dictionary<string, string>
{
["userName"] = Environment.GetEnvironmentVariable("SATIM_USER") ?? "",
["password"] = Environment.GetEnvironmentVariable("SATIM_PASSWORD") ?? "",
["mdOrder"] = "V721uPPfNNofVQAAABL3",
["language"] = "FR"
};
using var http = new HttpClient();
using var content = new FormUrlEncodedContent(payload);
using var response = await http.PostAsync(endpoint, content);
var raw = await response.Content.ReadAsStringAsync();
using var json = JsonDocument.Parse(raw);
var root = json.RootElement;
string GetString(JsonElement element, string name)
=> element.TryGetProperty(name, out var value) ? value.ToString().Trim() : "";
int GetInt(JsonElement element, string name)
=> int.TryParse(GetString(element, name), out var n) ? n : -1;
var paramsNode = root.TryGetProperty("params", out var p) ? p : default;
var respCode = paramsNode.ValueKind != JsonValueKind.Undefined ? GetString(paramsNode, "respCode") : "";
var respCodeDesc = paramsNode.ValueKind != JsonValueKind.Undefined ? GetString(paramsNode, "respCode_desc") : "";
var actionCodeDescription = GetString(root, "actionCodeDescription");
var language = (payload["language"] ?? "EN").ToUpperInvariant();
var isSuccess =
GetInt(root, "ErrorCode") == 0 &&
respCode == "00" &&
GetInt(root, "OrderStatus") == 2;
var localizedRejected = language == "FR"
? "Votre transaction a ete rejetee"
: language == "AR"
? "تم رفض معاملتك"
: "Your transaction was rejected";
string? gatewaySuccessMessage = null;
string? gatewayErrorMessage = null;
string attemptStatus;
string transactionStatus;
if (isSuccess)
{
attemptStatus = "acknowledged";
transactionStatus = "acknowledged";
gatewaySuccessMessage = !string.IsNullOrWhiteSpace(respCodeDesc) ? respCodeDesc : actionCodeDescription;
}
else
{
attemptStatus = "acknowledge_failed";
transactionStatus = "acknowledge_failed";
var isRejectedCase1 =
respCode == "00" &&
GetInt(root, "ErrorCode") == 0 &&
GetInt(root, "OrderStatus") == 3;
gatewayErrorMessage = isRejectedCase1
? localizedRejected
: (!string.IsNullOrWhiteSpace(respCodeDesc) ? respCodeDesc : actionCodeDescription);
}
var paymentAttemptId = 12345; // current payment_attempts.id from DB context
var transactionData = new Dictionary<string, object?>
{
["payment_attempt_id"] = paymentAttemptId,
["reference"] = $"TXN-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
["authorization_number"] =
GetString(root, "approvalCode") != "" ? GetString(root, "approvalCode") : GetString(root, "authorizationResponseId"),
["status"] = transactionStatus,
["payment_method"] = GetString(root, "Pan") != "" ? "CIB/EDAHABIA" : null,
["payment_gateway"] = "SATIM",
["gateway_success_message"] = gatewaySuccessMessage,
["gateway_error_message"] = gatewayErrorMessage,
["ip_address"] = GetString(root, "Ip")
};
// update payment_attempts with request/response payloads and final status
// persist transactions row (always, even on failure)
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func toInt(v any) int {
n, _ := strconv.Atoi(fmt.Sprint(v))
return n
}
func main() {
endpoint := "https://test2.satim.dz/payment/rest/public/acknowledgeTransaction.do"
payload := url.Values{
"userName": {os.Getenv("SATIM_USER")},
"password": {os.Getenv("SATIM_PASSWORD")},
"mdOrder": {"V721uPPfNNofVQAAABL3"},
"language": {"FR"},
}
resp, err := http.PostForm(endpoint, payload)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data map[string]any
_ = json.Unmarshal(body, &data)
params, _ := data["params"].(map[string]any)
respCode := fmt.Sprint(params["respCode"])
respCodeDesc := strings.TrimSpace(fmt.Sprint(params["respCode_desc"]))
actionCodeDescription := strings.TrimSpace(fmt.Sprint(data["actionCodeDescription"]))
language := strings.ToUpper(payload.Get("language"))
isSuccess := toInt(data["ErrorCode"]) == 0 && respCode == "00" && toInt(data["OrderStatus"]) == 2
localizedRejected := "Your transaction was rejected"
if language == "FR" {
localizedRejected = "Votre transaction a ete rejetee"
} else if language == "AR" {
localizedRejected = "تم رفض معاملتك"
}
attemptStatus := "acknowledge_failed"
transactionStatus := "acknowledge_failed"
var gatewaySuccessMessage any
var gatewayErrorMessage any
if isSuccess {
attemptStatus = "acknowledged"
transactionStatus = "acknowledged"
if respCodeDesc != "" {
gatewaySuccessMessage = respCodeDesc
} else {
gatewaySuccessMessage = actionCodeDescription
}
} else {
isRejectedCase1 := respCode == "00" && toInt(data["ErrorCode"]) == 0 && toInt(data["OrderStatus"]) == 3
if isRejectedCase1 {
gatewayErrorMessage = localizedRejected
} else if respCodeDesc != "" {
gatewayErrorMessage = respCodeDesc
} else {
gatewayErrorMessage = actionCodeDescription
}
}
paymentAttemptID := int64(12345) // current payment_attempts.id from DB context
authorizationNumber := data["approvalCode"]
if strings.TrimSpace(fmt.Sprint(authorizationNumber)) == "" || fmt.Sprint(authorizationNumber) == "<nil>" {
authorizationNumber = data["authorizationResponseId"]
}
var paymentMethod any
if pan := strings.TrimSpace(fmt.Sprint(data["Pan"])); pan != "" && pan != "<nil>" {
paymentMethod = "CIB/EDAHABIA"
}
_ = attemptStatus
_ = map[string]any{
"payment_attempt_id": paymentAttemptID, // current payment_attempts.id from DB context
"reference": fmt.Sprintf("TXN-%d", time.Now().UnixMilli()),
"authorization_number": authorizationNumber,
"status": transactionStatus,
"payment_method": paymentMethod,
"payment_gateway": "SATIM",
"gateway_success_message": gatewaySuccessMessage,
"gateway_error_message": gatewayErrorMessage,
"ip_address": data["Ip"],
}
// update payment_attempts with request/response payloads and final status
// persist transactions row (always, even on failure)
}
import os
import time
import requests
endpoint = "https://test2.satim.dz/payment/rest/public/acknowledgeTransaction.do"
payload = {
"userName": os.getenv("SATIM_USER", ""),
"password": os.getenv("SATIM_PASSWORD", ""),
"mdOrder": "V721uPPfNNofVQAAABL3",
"language": "FR",
}
res = requests.post(
endpoint,
data=payload,
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=30,
)
data = res.json()
params = data.get("params") or {}
resp_code = str(params.get("respCode", ""))
resp_code_desc = str(params.get("respCode_desc", "")).strip()
action_code_description = str(data.get("actionCodeDescription", "")).strip()
language = str(payload.get("language", "EN")).upper()
is_success = (
int(data.get("ErrorCode", -1)) == 0
and resp_code == "00"
and int(data.get("OrderStatus", -1)) == 2
)
localized_rejected = (
"Votre transaction a ete rejetee"
if language == "FR"
else "تم رفض معاملتك"
if language == "AR"
else "Your transaction was rejected"
)
if is_success:
attempt_status = "acknowledged"
transaction_status = "acknowledged"
gateway_success_message = resp_code_desc or action_code_description
gateway_error_message = None
else:
attempt_status = "acknowledge_failed"
transaction_status = "acknowledge_failed"
is_rejected_case1 = (
resp_code == "00"
and int(data.get("ErrorCode", -1)) == 0
and int(data.get("OrderStatus", -1)) == 3
)
gateway_success_message = None
gateway_error_message = (
localized_rejected if is_rejected_case1 else (resp_code_desc or action_code_description)
)
payment_attempt_id = 12345 # current payment_attempts.id from DB context
transaction_data = {
"payment_attempt_id": payment_attempt_id, # current payment_attempts.id from DB context
"reference": f"TXN-{int(time.time() * 1000)}",
"authorization_number": data.get("approvalCode") or data.get("authorizationResponseId"),
"status": transaction_status,
"payment_method": "CIB/EDAHABIA" if data.get("Pan") else None,
"payment_gateway": "SATIM",
"gateway_success_message": gateway_success_message,
"gateway_error_message": gateway_error_message,
"ip_address": data.get("Ip"),
}
# update payment_attempts with request/response payloads and final status
# persist transactions row (always, even on failure)
4.1 Request Parameters
| Name | Type | Mandatory | Description |
|---|---|---|---|
userName | AN..30 | Yes | Merchant login received during registration. |
password | AN..30 | Yes | Merchant password received during registration. |
mdOrder | ANS20 | Yes | Order number generated by EPG after registration. |
language | A2 | Yes | ISO 639-1 language (AR, FR, EN). |
4.2 Acknowledge Example URL
https://test2.satim.dz/payment/rest/public/acknowledgeTransaction.do?language=EN&mdOrder=6bbnSxXAZJ4eDAAAABHE&password=xxxxxxxxxxxxxxxx&userName=xxxxxxxxxxxxxxxx
4.3 Response Parameters
| Name | Type | Mandatory | Description |
|---|---|---|---|
expiration | N6 | No | Card expiration date in YYYYMM. Only for paid orders. |
cardholderName | AN..20 | No | Cardholder name. Valid chars: Latin letters, 0-9, $, ), (, -, ., space. Must start with a letter. Min 2, max 26, null allowed. |
depositAmount | N5 | Yes | Amount debited in order currency. |
currency | N3 | No | ISO 4217 payment currency code. |
authorizationResponseId (approvalCode deprecated name) | AN..6 | No | Network authorization code (fixed length 6). |
approvalCode | AN6 | No | IPS authorization code (fixed length 6). |
actionCode | N3 | Yes | Processing system authorization code. |
actionCodeDescription | AN..512 | Yes | Action code description in requested language. |
ErrorCode | N3 | Yes | Error code. |
ErrorMessage | AN..512 | No | Error description in requested language. |
OrderStatus | N2 | No | Order status in EPG. Absent if order ID is unknown. |
OrderNumber | AN..20 | Yes | Merchant order identifier. |
Pan | N..19 | No | Masked card number used for payment. |
Amount | N..20 | Yes | Amount in centimes (amount * 100). Minimum is 50 DA. |
Ip | AN..20 | No | Customer IP address. |
clientId | AN..255 | No | Customer identifier in merchant system. Mandatory for bindings. |
bindingId | AN..255 | No | Binding identifier if bindings are enabled/used. |
paymentAccountReference | ANS..999 | No | Payment account data when configured on host. |
Description | ANS..512 | No | Order description; omitted if order description is null. |
4.4 Acknowledge Error Codes
| Code | Message |
|---|---|
0 | Success. |
2 | Order declined due to payment credentials error. |
5 | Access is denied. |
5 | User must change password. |
5 | orderId is empty. |
6 | Unregistered order ID. |
7 | System error. |
4.5 OrderStatus Values
| Code | Description |
|---|---|
0 | Order registered but not paid. |
-1 | Generic decline fallback status. |
1 | Approved transaction (one-phase) or preauthorization hold (two-phase). |
2 | Amount deposited successfully. |
3 | Authorization reversed. |
4 | Transaction refunded. |
6 | Authorization declined. |
7 | Card added. |
8 | Card updated. |
9 | Card verified. |
10 | Recurring template added. |
11 | Debited. |
4.6 Acknowledge Response Example
{
"expiration": "202701",
"cardholderName": "cardholder Name",
"depositAmount": 100320,
"currency": "012",
"authorizationResponseId": "913180",
"approvalCode": "913180",
"actionCode": 0,
"actionCodeDescription": "Votre paiement a été accepté",
"ErrorCode": "0",
"ErrorMessage": "Success",
"OrderStatus": 2,
"OrderNumber": "CMD0000004",
"Pan": "6280****7215",
"Amount": 100320,
"Ip": "10.12.12.14",
"params": {
"respCode_desc": "Votre paiement a été accepté",
"udf1": "Bill00001",
"respCode": "00"
},
"SvfeResponse": "00"
}
4.7 Backend Mapping for gateway_success_message and gateway_error_message
Backend must mirror the same decision logic used by return-page rules so persisted transaction messages stay consistent with frontend behavior.
Use this mapping:
- Accepted payment condition:
params.respCode = "00"andErrorCode = "0"andOrderStatus = "2"- Set
gateway_success_message = respCode_desc(fallback toactionCodeDescriptionif empty) - Keep
gateway_error_message = null
- Rejected payment (Case 1):
params.respCode = "00"andErrorCode = "0"andOrderStatus = "3"- Set
gateway_error_messageto localized rejection text based on user selected language:Votre transaction a ete rejetee(FR)Your transaction was rejected(EN)تم رفض معاملتك(AR)
- Keep
gateway_success_message = null
- Rejected payment (Case 2 - otherwise):
- Set
gateway_error_message = respCode_desc - If
respCode_descis empty, setgateway_error_message = actionCodeDescription - Keep
gateway_success_message = null
- Set
Always persist SATIM support guidance (3020) in API response contracts or UI payload where applicable.
5. Backend Final Notes
- Keep SATIM credentials server-side only.
- Never trust only front-end redirect; always confirm with
acknowledgeTransaction.do. - Persist both merchant
orderNumberand SATIMorderId/mdOrder. - Log SATIM response fields for audit and support.
- Receipt generation is a backend responsibility: generate the receipt once after final payment confirmation, persist it (or its storage reference), and reuse it for print/download/email requests instead of regenerating each time.
- For multi-terminal platforms, keep the same SATIM register/acknowledge lifecycle and mapping logic; only terminal/credential resolution changes (load
force_terminal_id,userName, andpasswordfrom DB according to user-routing rules).