HSP Wallet واجهة دفع التاجر
واجهة دفع التاجر

واجهة دفع HSP — التحصيل من موقع آخر

تتيح هذه الواجهة لموقع التاجر تحصيل اليورو في حساب HSP. يُحوَّل الزبون إلى صفحة HSP، ويدخل هاتف ورمز PIN لتطبيق HSP Wallet، ثم يعود إلى موقعك. لا يُرسل الرمز أبدًا إلى خادمك.

العنوان الأساسي https://comptehsp.comEURHMAC-SHA256
  1. 1المفتاح في المساحة
  2. 2رابط الدفع
  3. 3Webhook موقَّع
مساحة التاجر

1. الحصول على مفتاح الواجهة (مفتاح واحد لكل تاجر)

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

2. توقيع HMAC لكل طلب

كل طلب تاجر يرسل:
- X-HSP-Key : key_id
- X-HSP-Timestamp : وقت يونكس بالثواني
- X-HSP-Signature : HMAC-SHA256 بنظام hexadecimal

الرسالة الموقَّعة أربع أسطر مفصولة بـ \n:
timestamp
METHOD
path
sha256_hex(body)

الأساس: https://comptehsp.com

3. إنشاء عملية دفع

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 الموقَّع.

4. منع الدفع المزدوج والانتهاء والحالات

لا يمكن خصم المرجع إلا مرة واحدة.
- PENDING ما زال صالحًا: يُعاد الرابط نفسه.
- PENDING منتهٍ أو CANCELLED أو FAILED بدون خصم: يُعاد فتح المرجع برابط جديد.
- PAID أو خصم جارٍ: HTTP 409 والرمز already_paid.
- تأكيدان في الوقت نفسه يخصمان مرة واحدة فقط.
- بعد الانتهاء تصبح الحالة EXPIRED ويرفض النموذج (HTTP 410، payment_expired).
- خمسة رموز خاطئة على الرابط توقفه. خمسة رموز خاطئة على الحساب تقفله 15 دقيقة.

الحالات: PENDING و PROCESSING و PAID و EXPIRED و CANCELLED و FAILED.

5. التحقق من الحالة

GET /api/v1/checkout/payments/{id}
GET /api/v1/checkout/payments/reference/{reference}

نفس توقيع HMAC وجسم فارغ. لا تعتبر الطلب مدفوعًا إلا إذا كانت status تساوي PAID.

6. Webhook

ترسل 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.

7. رموز الخطأ

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

التحقق من الحالة

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()));

التحقق من الـ webhook

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