HSP Wallet
واجهة دفع التاجر
تتيح هذه الواجهة لموقع التاجر تحصيل اليورو في حساب HSP. يُحوَّل الزبون إلى صفحة HSP، ويدخل هاتف ورمز PIN لتطبيق HSP Wallet، ثم يعود إلى موقعك. لا يُرسل الرمز أبدًا إلى خادمك.
1. ادخل مساحة التاجر: https://comptehsp.com/commercant (الهاتف + رمز التطبيق).
2. افتح بطاقة « واجهة الدفع للموقع ».
3. اضغط « الحصول على المفتاح ». تعرض HSP:
- key_id عام، يبدأ بـ hspk_
- secret خاص، يبدأ بـ hsps_
4. انسخ السر فورًا. لن يُعرض مرة أخرى. « إعادة التوليد » ينشئ زوجًا جديدًا ويلغي القديم.
5. « الإلغاء » يوقف الواجهة.
التوثيق: /pay/checkout/docs?lang=fr|en|es|de|pt|ar
كل طلب تاجر يرسل:
- X-HSP-Key : key_id
- X-HSP-Timestamp : وقت يونكس بالثواني
- X-HSP-Signature : HMAC-SHA256 بنظام hexadecimal
الرسالة الموقَّعة أربع أسطر مفصولة بـ \n:
timestamp
METHOD
path
sha256_hex(body)
الأساس: https://comptehsp.com
POST /api/v1/checkout/payments
| الحقل | إلزامي | الوصف |
|---|---|---|
| reference | نعم | رقم طلبك، فريد. من 1 إلى 80 حرفًا |
| amount | نعم | المبلغ باليورو من 0.01 إلى 50000. رقم JSON |
| currency | نعم | EUR فقط |
| return_url | نعم | عنوان https عام لعودة الزبون بعد الدفع |
| cancel_url | لا | عنوان https عام عند الإلغاء |
| webhook_url | لا | عنوان https عام يستقبل النتيجة |
| description | لا | نص يظهر للزبون، 180 حرفًا |
| expires_in | لا | عمر الرابط بالثواني من 60 إلى 86400. الافتراضي 1800 |
| locale | لا | fr أو en أو es أو de أو pt أو ar |
| customer.email | لا | يُحفظ لمتابعتك |
| customer.name | لا | اسم حر |
| metadata | لا | كائن JSON حتى 2000 حرف |
الاستجابة 201 تتضمن checkout_url. حوّل الزبون إليها. ثم تعيد HSP التوجيه إلى return_url مع hsp_payment_id و hsp_reference و hsp_status. أكّد دائمًا عبر حالة الدفع أو webhook الموقَّع.
لا يمكن خصم المرجع إلا مرة واحدة.
- PENDING ما زال صالحًا: يُعاد الرابط نفسه.
- PENDING منتهٍ أو CANCELLED أو FAILED بدون خصم: يُعاد فتح المرجع برابط جديد.
- PAID أو خصم جارٍ: HTTP 409 والرمز already_paid.
- تأكيدان في الوقت نفسه يخصمان مرة واحدة فقط.
- بعد الانتهاء تصبح الحالة EXPIRED ويرفض النموذج (HTTP 410، payment_expired).
- خمسة رموز خاطئة على الرابط توقفه. خمسة رموز خاطئة على الحساب تقفله 15 دقيقة.
الحالات: PENDING و PROCESSING و PAID و EXPIRED و CANCELLED و FAILED.
GET /api/v1/checkout/payments/{id}
GET /api/v1/checkout/payments/reference/{reference}
نفس توقيع HMAC وجسم فارغ. لا تعتبر الطلب مدفوعًا إلا إذا كانت status تساوي PAID.
ترسل HSP طلب POST بصيغة JSON إلى webhook_url عندما تصبح العملية PAID أو EXPIRED أو CANCELLED أو FAILED. حتى 6 محاولات إذا لم تجب 2xx.
الترويسات: X-HSP-Event و X-HSP-Timestamp و X-HSP-Signature.
التوقيع هو HMAC-SHA256 من timestamp ثم سطر جديد ثم الجسم الخام المستلم.
الأحداث: checkout.payment.paid و .expired و .cancelled و .failed.
يتضمن payment الحقول id و reference و status و amount و currency و paid_at و payer_label و ledger_ref و metadata.
قارن التوقيع بالجسم الخام. ارفض الطابع الزمني خارج 5 دقائق. سلّم الطلب فقط إذا تطابق reference و amount وكانت الحالة PAID.
missing_signature 401، invalid_signature 401، stale_timestamp 401، replayed_signature 401، unknown_api_key 401، revoked_api_key 401، already_paid 409، reference_conflict 409، payment_expired 410، invalid_amount 400، invalid_currency 400، invalid_url 400، invalid_pin 400، insufficient_balance 400، too_many_attempts 429.
لـ Node.js و PHP و Python: HSP_KEY (hspk_…) و HSP_SECRET (hsps_…). في ووردبريس، الصقهما في الإعدادات → HSP Paiement.
const crypto = require('crypto');
function canonical(value) {
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonical(value[k])).join(',') + '}';
}
function sign({ secret, timestamp, method, path, body }) {
const payload = body == null ? '' : canonical(body);
const message = [timestamp, method.toUpperCase(), path, crypto.createHash('sha256').update(payload).digest('hex')].join('\n');
return crypto.createHmac('sha256', secret).update(message).digest('hex');
}
const body = {
reference: 'ORDER-' + Date.now(),
amount: 1,
currency: 'EUR',
description: 'Commande de test',
return_url: 'https://shop.example/hsp/return',
cancel_url: 'https://shop.example/hsp/cancel',
webhook_url: 'https://shop.example/hsp/webhook',
expires_in: 600,
locale: 'fr',
metadata: { cart: '1001' }
};
const timestamp = String(Math.floor(Date.now() / 1000));
const path = '/api/v1/checkout/payments';
const signature = sign({ secret: process.env.HSP_SECRET, timestamp, method: 'POST', path, body });
fetch('https://comptehsp.com' + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-HSP-Key': process.env.HSP_KEY,
'X-HSP-Timestamp': timestamp,
'X-HSP-Signature': signature
},
body: JSON.stringify(body)
}).then(async (res) => {
const payment = await res.json();
console.log(res.status, payment.checkout_url || payment);
});<?php
function hsp_canonical($value) {
if (!is_array($value)) {
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
$isList = function_exists('array_is_list')
? array_is_list($value)
: ($value === [] || array_keys($value) === range(0, count($value) - 1));
if ($isList) {
return '[' . implode(',', array_map('hsp_canonical', $value)) . ']';
}
ksort($value);
$parts = [];
foreach ($value as $key => $item) {
$parts[] = json_encode((string) $key, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ':' . hsp_canonical($item);
}
return '{' . implode(',', $parts) . '}';
}
function hsp_sign($secret, $timestamp, $method, $path, $body) {
$payload = $body === null ? '' : hsp_canonical($body);
$message = $timestamp . "\n" . strtoupper($method) . "\n" . $path . "\n" . hash('sha256', $payload);
return hash_hmac('sha256', $message, $secret);
}
$key = getenv('HSP_KEY');
$secret = getenv('HSP_SECRET');
$body = [
'reference' => 'ORDER-' . time(),
'amount' => 1,
'currency' => 'EUR',
'description' => 'Commande de test',
'return_url' => 'https://shop.example/hsp/return',
'cancel_url' => 'https://shop.example/hsp/cancel',
'webhook_url' => 'https://shop.example/hsp/webhook',
'expires_in' => 600,
'locale' => 'fr',
'metadata' => ['cart' => '1001'],
];
$path = '/api/v1/checkout/payments';
$timestamp = (string) time();
$signature = hsp_sign($secret, $timestamp, 'POST', $path, $body);
$ch = curl_init('https://comptehsp.com' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-HSP-Key: ' . $key,
'X-HSP-Timestamp: ' . $timestamp,
'X-HSP-Signature: ' . $signature,
],
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$payment = json_decode($response, true);
echo $code . ' ' . ($payment['checkout_url'] ?? $response) . PHP_EOL;import hashlib, hmac, json, os, time, urllib.request
def canonical(value):
if isinstance(value, dict):
parts = [
json.dumps(key, ensure_ascii=False) + ":" + canonical(value[key])
for key in sorted(value)
]
return "{" + ",".join(parts) + "}"
if isinstance(value, list):
return "[" + ",".join(canonical(item) for item in value) + "]"
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def sign(secret, timestamp, method, path, body):
payload = "" if body is None else canonical(body)
message = "\n".join([timestamp, method.upper(), path, hashlib.sha256(payload.encode()).hexdigest()])
return hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
body = {
"reference": "ORDER-" + str(int(time.time())),
"amount": 1,
"currency": "EUR",
"description": "Commande de test",
"return_url": "https://shop.example/hsp/return",
"cancel_url": "https://shop.example/hsp/cancel",
"webhook_url": "https://shop.example/hsp/webhook",
"expires_in": 600,
"locale": "fr",
"metadata": {"cart": "1001"},
}
path = "/api/v1/checkout/payments"
timestamp = str(int(time.time()))
signature = sign(os.environ["HSP_SECRET"], timestamp, "POST", path, body)
req = urllib.request.Request(
"https://comptehsp.com" + path,
data=json.dumps(body).encode(),
headers={
"Content-Type": "application/json",
"X-HSP-Key": os.environ["HSP_KEY"],
"X-HSP-Timestamp": timestamp,
"X-HSP-Signature": signature,
},
method="POST",
)
with urllib.request.urlopen(req) as res:
payment = json.loads(res.read().decode())
print(res.status, payment.get("checkout_url"))const reference = 'ORDER-1001';
const path = '/api/v1/checkout/payments/reference/' + encodeURIComponent(reference);
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = sign({ secret: process.env.HSP_SECRET, timestamp, method: 'GET', path, body: null });
fetch('https://comptehsp.com' + path, {
headers: {
'X-HSP-Key': process.env.HSP_KEY,
'X-HSP-Timestamp': timestamp,
'X-HSP-Signature': signature
}
}).then(async (res) => console.log(res.status, await res.json()));<?php
$reference = 'ORDER-1001';
$path = '/api/v1/checkout/payments/reference/' . rawurlencode($reference);
$timestamp = (string) time();
$signature = hsp_sign(getenv('HSP_SECRET'), $timestamp, 'GET', $path, null);
$ch = curl_init('https://comptehsp.com' . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-HSP-Key: ' . getenv('HSP_KEY'),
'X-HSP-Timestamp: ' . $timestamp,
'X-HSP-Signature: ' . $signature,
],
]);
echo curl_exec($ch) . PHP_EOL;
curl_close($ch);from urllib.parse import quote
reference = "ORDER-1001"
path = "/api/v1/checkout/payments/reference/" + quote(reference, safe="")
timestamp = str(int(time.time()))
signature = sign(os.environ["HSP_SECRET"], timestamp, "GET", path, None)
req = urllib.request.Request(
"https://comptehsp.com" + path,
headers={
"X-HSP-Key": os.environ["HSP_KEY"],
"X-HSP-Timestamp": timestamp,
"X-HSP-Signature": signature,
},
)
with urllib.request.urlopen(req) as res:
print(json.loads(res.read().decode())["status"])const crypto = require('crypto');
function verifyWebhook(secret, rawBody, timestamp, signature) {
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(timestamp + '\n' + rawBody).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(String(signature || ''), 'hex');
return a.length === b.length && a.length === 32 && crypto.timingSafeEqual(a, b);
}
// Express : app.use(express.raw({ type: 'application/json' })) sur cette route
app.post('/hsp/webhook', (req, res) => {
const raw = req.body.toString('utf8');
const ok = verifyWebhook(process.env.HSP_SECRET, raw, req.get('x-hsp-timestamp'), req.get('x-hsp-signature'));
if (!ok) return res.status(401).end();
const event = JSON.parse(raw);
if (event.payment.status === 'PAID' && event.payment.amount === expectedAmount) {
// livrer la commande une seule fois (idempotence sur payment.reference)
}
res.status(200).json({ received: true });
});<?php
$secret = getenv('HSP_SECRET');
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_HSP_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_HSP_SIGNATURE'] ?? '';
$now = time();
if (!ctype_digit($timestamp) || abs($now - intval($timestamp)) > 300) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $timestamp . "\n" . $raw, $secret);
if (!hash_equals($expected, strtolower($signature))) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
if (($event['payment']['status'] ?? '') === 'PAID') {
// livrer une seule fois selon payment.reference
}
http_response_code(200);
echo json_encode(['received' => true]);import hashlib, hmac, json, os, time
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["HSP_SECRET"].encode()
@app.post("/hsp/webhook")
def webhook():
raw = request.get_data()
timestamp = request.headers.get("X-HSP-Timestamp", "")
signature = request.headers.get("X-HSP-Signature", "")
try:
ts = int(timestamp)
except ValueError:
return ("", 401)
if abs(int(time.time()) - ts) > 300:
return ("", 401)
expected = hmac.new(SECRET, timestamp.encode() + b"\n" + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature.lower()):
return ("", 401)
event = json.loads(raw)
if event["payment"]["status"] == "PAID":
pass # livrer une seule fois selon payment["reference"]
return {"received": True}