انتقل إلى المحتوى
التوثيق

البدء

يقبل Payniko مدفوعات البطاقات عبر واجهة REST API واحدة موقّعة. ويمكنك إما إعادة توجيه العميل إلى صفحة الدفع في Payniko (Hosted API)، أو جمع بيانات البطاقة في نموذج الدفع الخاص بك (Direct API).

الرابط الأساسي والصيغ

الرابط الأساسي
https://payniko.com/api. ولا يتضمن المسار رقم إصدار.
الصيغة
JSON بترميز UTF-8. وتستخدم الأخطاء الصيغة application/problem+json.
المبالغ
نصوص بمنزلتين عشريتين، على سبيل المثال "100.00". ولا ترسل المبالغ مطلقًا بوصفها أرقام JSON.
العملة
USD أو EUR أو AED أو TRY. وفي حال حذف currency، تُطبَّق العملة الرئيسية للنظام.
الوقت
صيغة ISO 8601 بالتوقيت العالمي المنسق (UTC) مع أجزاء الألف من الثانية، على سبيل المثال 2026-11-01T12:00:00.000Z.
المعرّفات
تستخدم معرّفات الدفع والاسترداد والسحب والنزاعات الصيغة LUVEI-ABCD-123456. احتفظ بالمعرّف كاملاً.
اللغة
تُعاد الرسائل باللغة المحددة في Accept-Language (en، tr، de، ar، es، el)، والقيمة الافتراضية en. ولا تُترجم الرموز وأسماء الحقول مطلقًا.

الحصول على مفاتيح API

تحصل الحسابات الجديدة تلقائيًا على مفاتيح Sandbox وLive. إذا كان لديك حساب، سجّل الدخول وافتح مفاتيح الوصول. إذا استلمت ملف حساب، استخدم اسم المستخدم وكلمة المرور فيه دون التسجيل مرة أخرى.

1. أنشئ حسابك

  1. أنشئ حسابًا. أدخل بريدك المستخدم واضغط إرسال الرمز. أدخل الرمز المكوّن من ستة أرقام من البريد في صفحة التسجيل واضغط تحقق. إذا لم يصلك، راجع العنوان والبريد غير المرغوب؛ اطلب رمزًا جديدًا بعد انتهاء العداد.

  2. بعد تأكيد البريد، اختر رمز الدولة وأدخل رقم الهاتف المرتبط بحسابك الشخصي في Telegram. اضغط تحقق. التحقق يتم عبر Telegram وليس برسالة SMS.

  3. افتح رابط Telegram المعروض، أو امسح رمز QR بهاتفك إذا كنت تستخدم حاسوبًا. اضغط بدء / Start في البوت ثم شارك رقمك من زر مشاركة الرقم. كتابة الرقم كرسالة أو إرسال جهة اتصال لشخص آخر لا يحقق التوثيق. عد إلى التسجيل وانتظر ظهور التأكيد.

  4. اضغط المتابعة إلى بيانات الحساب. اختر اسم مستخدم وكلمة مرور، أعد كتابة كلمة المرور، وأكمل التأكيدات ثم اضغط إنشاء الحساب. اسم المستخدم من 4 إلى 32 محرفًا: حروف لاتينية صغيرة وأرقام ونقطة وشرطة سفلية أو عادية. كلمة المرور لا تقل عن 12 محرفًا.

  5. احفظ رمز الاسترداد الذي يظهر بعد إنشاء الحساب في مكان آمن. يظهر في هذه الشاشة فقط، وهو مختلف عن سر API. أكد حفظه واضغط حفظت الرمز، انتقل إلى اللوحة.

2. احصل على مفاتيح Sandbox ورمز التاجر

  1. افتح مفاتيح الوصول وابحث عن بطاقة Sandbox. مفاتيح الحساب الجديد جاهزة. إذا لم يكن للحساب القديم مفتاح نشط، أدخل تسمية اختيارية واضغط إنشاء مفتاح. لا يلزم تسجيل عنوان IP.

  2. خذ القيم الثلاث الموضحة في الجدول. استخدم رمز العين لعرض السر ورمز النسخ لنسخه. يمكن لصاحب الحساب عرض السر النشط مجددًا؛ لا تحتاج إلى تجديد المفتاح لمجرد عرضه.

  3. شارك القيم بأمان مع المطور أو احفظها في إعدادات الخادم الخاصة. يجب أن يكون المفتاح والسر من بطاقة Sandbox نفسها. لا تضع السر في JavaScript المتصفح أو ملف عام أو عنوان URL.

القيمةمكانهااستخدامها
رمز التاجرالرمز المكوّن من ستة أرقام في البطاقةالترويسة X-Merchant-Code؛ الرمز نفسه للحساب في Sandbox وLive.
مفتاح APIسطر المفتاح في بطاقة Sandboxالترويسة X-Api-Key؛ يحدد بيئة التشغيل.
سر APIرمزا العين والنسخ في البطاقة نفسهالتوقيع الطلبات والتحقق من توقيع Webhook؛ لا يُرسل كحقل في الطلب.

إنشاء دفعة تجريبية

اختبر التكامل في Sandbox دون حركة أموال حقيقية. نجاح الاختبار ليس شرطًا للحصول على مفاتيح Live؛ لكنه موصى به للتحقق من التوقيع وإعادة التوجيه وإشعارات Webhook.

  1. استخدم مثال خادم من دوال مساعدة للطلبات واضبط مفتاح Sandbox وسر Sandbox ورمز التاجر. لا يوجد نطاق API منفصل للتجربة؛ استخدم العنوان الأساسي الموثق. المفتاح هو الذي يحدد البيئة.

  2. راجع حدود المبالغ والعملات في Sandbox ضمن تجربة الدفع. اختر مبلغًا مسموحًا لا ينتهي بـ .77 لأن هذه النهاية تشغّل سيناريو النتيجة المعلقة.

  3. املأ مثال POST /api/payments/hosted: مرجع merchant_order_id فريد والمبلغ والعملة وcustomer.email / customer.phone وعناوين الإشعار والعودة الثلاثة. استخدم بيانات اتصال قابلة للوصول ورقمًا برمز الدولة. يجب أن يكون webhook_url عنوان HTTPS عامًا حتى في Sandbox؛ لا تُقبل localhost أو الشبكات الخاصة. أما success_url / fail_url فهي عناوين عودة المتصفح.

  4. أرسل الطلب الموقّع POST /api/payments/hosted من خادمك. استخدم مرجعًا وIdempotency-Key جديدين لكل تجربة جديدة؛ عند تكرار الطلب نفسه بسبب مشكلة اتصال، احتفظ بقيمة Idempotency-Key نفسها. HTTP 201 يعني إنشاء الدفعة وليس نجاح الدفع.

  5. افتح عنوان next_action.url كاملاً من الاستجابة في المتصفح. استخدم بطاقة الاختبار 4000000000000002 وتاريخ انتهاء مستقبلي وCVV من ثلاثة أرقام؛ مثلاً 12/30 و123. أكمل النتيجة الناجحة/المقبولة في شاشة 3D Secure التجريبية. لا تستخدم بطاقة حقيقية. السيناريوهات الأخرى في بطاقات الاختبار.

  6. بعد العودة، استعلم عبر GET /api/payments/{payment_id} أو عالج إشعار Webhook بعد التحقق من توقيعه. يجب أن تكون حالة API هي succeeded. الحالات requires_action / processing مؤقتة، وfailed لا يُحسب نجاحًا. ظهور صفحة النجاح وحده لا يثبت الدفع.

  7. تحقق من نتيجة الاختبار في سجل الطلبات. يُكمل السجل الناجح هذا السيناريو. يستخدم Live زوج مفاتيح مستقلًا.

الحصول على مفاتيح Live

يستخدم Sandbox وLive زوجين منفصلين من المفاتيح. لا تتطلب مفاتيح Live تسجيل IP أو نجاح دفعة Sandbox. تغيير البيئة في اللوحة لا يغير إعداد الخادم.

  1. افتح صفحة وصول Live وابحث عن بطاقة البيئة الفعلية. مفاتيح Live جاهزة للحسابات الجديدة؛ إذا لم تكن موجودة في حساب قديم فاضغط إنشاء مفتاح.

  2. خذ مفتاح API والسر من بطاقة Live نفسها. رمز التاجر هو نفسه في Sandbox. احفظ السر في إعداد الخادم الخاص فقط.

  3. استخدم زوج مفاتيح Live في إعدادات خادم الإنتاج بدلاً من زوج Sandbox. عنوان API لا يتغير وتبقى مفاتيح Sandbox صالحة للتجارب. عرض بطاقة في اللوحة لا يغير إعدادات خادمك.

  4. راجع حدود وعملات Live ضمن تجربة الدفع. مفاتيحك جاهزة الآن. طلبات Live تدخل مسار التحصيل الحقيقي؛ استخدم مفاتيح Sandbox عند متابعة اختبارات البطاقات.

إذا توقفت عند خطوة

الحالةما الذي تفعله
البريد أو Telegram غير موثّقأدخل رمز البريد في صفحة التسجيل. افتح البوت من رابط الصفحة وشارك رقمك المسجل بزر مشاركة الرقم. جدّد الرابط المنتهي من التسجيل.
يتعذر إنشاء مفتاحتحقق من أن الحساب نشط وأن تحصيل API مفعّل. لا يلزم تسجيل IP أو إكمال دفعة Sandbox.
مفتاح Live غير ظاهرافتح بطاقة Live. المفاتيح جاهزة للحساب الجديد؛ أنشئ مفتاحًا إذا لم يكن للحساب القديم مفتاح نشط. إذا استمرت المشكلة، تواصل مع الدعم.
لم تحفظ سر APIاستخدم العين أو النسخ في بطاقة المفتاح النشط. رمز الاسترداد ليس سر API.
لا تظهر خدمة تحصيل APIتواصل مع الدعم للتحقق من الوصول. قد تتاح روابط الدفع وواجهة API بشكل منفصل.

نظرة عامة على نقاط النهاية

نقطة النهايةالغرض
POST /api/payments/hostedإنشاء دفعة عبر Hosted API · Hosted API
GET /api/payments/{payment_id}الاستعلام عن دفعة · Hosted API · Direct API
GET /api/payments/bin/{bin6}فحص BIN (الأرقام الستة الأولى من البطاقة) · Direct API
POST /api/payments/directإنشاء دفعة عبر Direct API · Direct API
GET /api/account/balanceرصيد الحساب · الأرصدة
GET /api/account/available-balanceالرصيد القابل للسحب · الأرصدة
POST|GET /api/account/refundsالاسترداد والإبطال · الاسترداد
POST|GET /api/account/withdrawalsعمليات سحب USDT · عمليات السحب
GET /api/account/disputesالنزاعات والرد عليها · النزاعات
GET|POST /api/webhooks/deliveriesسجل تسليم إشعارات Webhook وإعادة إرسالها · عمليات التسليم
POST /api/sandbox/disputesإنشاء نزاع تجريبي (بالمفتاح التجريبي فقط) · البيئة التجريبية

تستخدم جميع نقاط النهاية آلية المصادقة نفسها، ويتطلب كل طلب POST كذلك الترويسة Idempotency-Key.

المصادقة

يحمل كل طلب مفتاح API الخاص بك وتوقيع HMAC-SHA256 محسوبًا بالمفتاح السري.

مفتاح API والمفتاح السري

القيمةالصيغةالوصف
مفتاح APIapi_ + 32 hexيُرسل في X-Api-Key. ويحدد المفتاح البيئة أيضًا.
المفتاح السري (API secret)64 hexيُستخدم لتوقيع الطلبات والتحقق من توقيع Webhook ولا يُرسل في الطلب. يمكن لصاحب الحساب عرض السر النشط أو نسخه مجددًا من رمزي العين والنسخ في اللوحة.
رمز التاجر[0-9]{6}رقم حسابك، ويُرسل في X-Merchant-Code.

ينتمي كل مفتاح API إلى بيئة واحدة فقط، تجريبية أو حية، ويحدد المفتاح البيئة التي يُنفَّذ فيها الطلب: استخدم المفتاح التجريبي للاختبار والمفتاح الحي للمدفوعات الحقيقية. والترويسة X-Environment اختيارية؛ وفي حال حذفها يُنفَّذ الطلب في بيئة المفتاح. وفي حال إرسالها، يجب أن تكون قيمتها sandbox أو live (دون تمييز بين الأحرف الكبيرة والصغيرة) وأن تطابق بيئة المفتاح؛ وإلا يُرفض الطلب بحالة HTTP 401 والرمز 0055502، ولا تُعالَج أي عملية. وترويسات المصادقة الإلزامية هي X-Api-Key وX-Merchant-Code وX-Timestamp وX-Signature، إضافةً إلى Idempotency-Key في كل طلب كتابة. ولا تدخل البيئة في السلسلة المراد توقيعها. ويُبيّن كل متن استجابة يتضمن environment (المدفوعات والأرصدة وعمليات الاسترداد وغيرها من الموارد) البيئة التي استُخدمت.

تدوير المفاتيح

  • أنشئ مفتاحًا جديدًا إذا احتُمل كشف المفتاح السري، أو عند مغادرة أحد الأشخاص المخوّلين بالوصول إليه. ويتوقف المفتاح السابق عن العمل فورًا.
  • تُوقَّع إشعارات Webhook وروابط العودة الخاصة بالدفعة بالمفتاح السري المرتبط بالمفتاح الذي أنشأ الدفعة؛ وينطبق ذلك أيضًا على إشعارات الاسترداد والنزاعات المرسلة بعد الدفعة بمدة طويلة. احتفظ بالمفتاح السري السابق ما دام من الممكن وصول مثل هذه الأحداث.

ترويسات الطلب

الترويسةإلزاميالقيمة
X-Api-Keyإلزاميمفتاح API الخاص بك.
X-Merchant-Codeإلزاميرمز التاجر المكوّن من ستة أرقام.
X-Timestampإلزاميوقت Unix الحالي بالثواني (10 أرقام).
X-Signatureإلزاميتوقيع الطلب: 64 حرفًا بالنظام الست عشري وبأحرف صغيرة.
Idempotency-Keyمشروطإلزامي في كل طلب POST. راجع Idempotency-Key.
Content-Typeمشروطapplication/json للطلبات التي تتضمن متنًا.
Accept-Languageاختياريلغة رسائل الخطأ: en (الافتراضية) أو tr أو de أو ar أو es أو el.

أرسل كل ترويسة من نوع X- مرة واحدة فقط. ويجب ألا تتضمن قيم الترويسات فواصل أو أسطرًا جديدة أو مسافات.

توقيع الطلبات

تتكون السلسلة المراد توقيعها من أربع قيم مفصولة بمحرف السطر الجديد (line feed): الطابع الزمني، وطريقة HTTP، والمسار، وقيمة تجزئة SHA-256 للمتن.

السلسلة المراد توقيعها

canonical   = X-Timestamp + "\n" + METHOD + "\n" + path + "\n" + hex(sha256(raw body))
X-Signature = hex(HMAC-SHA256(key = API secret, message = canonical))   // 64 lowercase hex characters
  1. رمّز المتن بصيغة JSON مرة واحدة، واحسب قيمة تجزئة SHA-256 لتلك البايتات تحديدًا (بالنظام الست عشري وبأحرف صغيرة). وللطلب الذي لا يتضمن متنًا، احسب قيمة تجزئة السلسلة الفارغة (e3b0c442…b855).

  2. استخدم المسار تمامًا كما يظهر في الرابط، بدءًا من /api/ ودون سلسلة الاستعلام. على سبيل المثال، في الطلب GET /api/payments/bin/540061?amount=100.00 يكون المسار الموقّع /api/payments/bin/540061.

  3. احسب HMAC-SHA256 باستخدام المفتاح السري مفتاحًا للتوقيع. واستخدم سلسلة المفتاح السري كما هي، دون فك ترميزها.

  4. أرسل الناتج في X-Signature، وأرسل في متن الطلب البايتات نفسها التي حُسبت قيمة تجزئتها دون أي تغيير.

هامش الطابع الزمني

يجب ألا يتجاوز الفرق بين X-Timestamp وتوقيت خوادم Payniko مدة 300 ثانية، في أي من الاتجاهين. واحرص على مزامنة ساعة خادمك عبر NTP. وإذا تجاوز الفرق ذلك، يُرفض الطلب بالرمز 401 0055504، ويتضمن detail توقيت خوادم Payniko.

مثال على التوقيع

استخدم هذه القيم النموذجية لاختبار تنفيذك. وباستخدام هذا المفتاح السري وهذا الطابع الزمني وهذا المتن، يجب أن تُنتج شيفرتك هذا التوقيع تمامًا.

المفتاح السري (قيمة نموذجية)

e3c4a5b6d7e8f90112233445566778899aabbccddeeff0011223344556677889

المتن (البايتات الفعلية)

{"merchant_order_id":"ORDER-2026-001","amount":"100.00","currency":"USD","webhook_url":"https://merchant.example/webhooks/payments","success_url":"https://merchant.example/payment/success","fail_url":"https://merchant.example/payment/failure","customer":{"email":"[email protected]","phone":"+37061234567"}}

sha256(body)

dca2bae6e50c13c26b22b57e3c31161ba3174147060fcbf22b2ba99cc13560ae

السلسلة المراد توقيعها

1793534400
POST
/api/payments/hosted
dca2bae6e50c13c26b22b57e3c31161ba3174147060fcbf22b2ba99cc13560ae

X-Signature

5522c27ff0b49ffa4c44712ed5db350c1c5a2cbfc0c568edbce39cff59ca620c

ترويسات الطلب

POST /api/payments/hosted
X-Api-Key: api_0123456789abcdef0123456789abcdef
X-Merchant-Code: 123456
X-Timestamp: 1793534400
X-Signature: 5522c27ff0b49ffa4c44712ed5db350c1c5a2cbfc0c568edbce39cff59ca620c
Idempotency-Key: 6f1c2a9e-1d2b-4c1a-9f0e-3b7d5a2c8e41
Content-Type: application/json
Accept: application/json

الدوال المساعدة للطلبات

أضف إحدى هذه الدوال المساعدة إلى خادمك. تقرأ الدالة API_KEY وAPI_SECRET وMERCHANT_CODE من متغيرات البيئة، ثم توقّع الطلب وترسله. وتستخدم أمثلة الشيفرة الأخرى هذه الدالة.

#!/usr/bin/env bash
# payniko.sh - signed request helper (bash, curl, openssl).
# Usage: ./payniko.sh METHOD PATH [JSON_BODY] [IDEMPOTENCY_KEY]
# Reads API_KEY, API_SECRET and MERCHANT_CODE. The key decides sandbox or live.
# Optional: SANDBOX_SCENARIO adds the X-Sandbox-Scenario header (sandbox keys only).
set -euo pipefail
: "${API_KEY:?set API_KEY}" "${API_SECRET:?set API_SECRET}" "${MERCHANT_CODE:?set MERCHANT_CODE}"
BASE_URL="https://payniko.com"
method="$(printf '%s' "${1:?METHOD}" | tr '[:lower:]' '[:upper:]')"
target="${2:?PATH}"            # may include a query string; only the path is signed
body="${3-}"                   # sent byte-for-byte as hashed
idempotency_key="${4-}"
path="${target%%\?*}"
timestamp="$(date +%s)"
body_hash="$(printf '%s' "$body" | openssl dgst -sha256 | sed 's/^.* //')"
signature="$(printf '%s\n%s\n%s\n%s' "$timestamp" "$method" "$path" "$body_hash" \
  | openssl dgst -sha256 -hmac "$API_SECRET" | sed 's/^.* //')"
args=(-sS --fail-with-body -X "$method" "$BASE_URL$target"
  -H "X-Api-Key: $API_KEY" -H "X-Merchant-Code: $MERCHANT_CODE" -H "X-Timestamp: $timestamp"
  -H "X-Signature: $signature" -H "Accept: application/json" -H "Accept-Language: en")
if [ -n "$body" ]; then args+=(-H "Content-Type: application/json" --data-binary "$body"); fi
if [ -n "$idempotency_key" ]; then args+=(-H "Idempotency-Key: $idempotency_key"); fi
if [ -n "${SANDBOX_SCENARIO-}" ]; then args+=(-H "X-Sandbox-Scenario: $SANDBOX_SCENARIO"); fi
curl "${args[@]}"
echo
// payniko.mjs - signed request helper (Node.js 20+, no dependencies).
import {createHash, createHmac, randomUUID} from 'node:crypto';

const base = 'https://payniko.com';
const key = process.env.API_KEY ?? '';
const secret = process.env.API_SECRET ?? '';
const merchant = process.env.MERCHANT_CODE ?? '';
if (!/^api_[0-9a-f]{32}$/.test(key) || !/^[0-9a-f]{64}$/.test(secret) || !/^[0-9]{6}$/.test(merchant)) {
  throw new Error('Set API_KEY, API_SECRET and MERCHANT_CODE.');
}

export async function api(method, path, body, idempotencyKey, extraHeaders = {}) {
  const url = new URL(path, base);
  if (url.origin !== base || !url.pathname.startsWith('/api/')) throw new Error('Invalid API URL');
  method = method.toUpperCase();
  const raw = body === undefined ? '' : JSON.stringify(body);   // hash and send the same bytes
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const canonical = [timestamp, method, url.pathname,
    createHash('sha256').update(raw, 'utf8').digest('hex')].join('\n');
  const signature = createHmac('sha256', secret).update(canonical, 'utf8').digest('hex');
  const response = await fetch(url, {
    method,
    redirect: 'error',
    headers: {
      ...extraHeaders,
      'X-Api-Key': key, 'X-Merchant-Code': merchant,
      'X-Timestamp': timestamp, 'X-Signature': signature,
      'Accept': 'application/json', 'Accept-Language': 'en',
      ...(raw ? {'Content-Type': 'application/json'} : {}),
      ...(idempotencyKey ? {'Idempotency-Key': idempotencyKey} : {}),
    },
    ...(raw ? {body: raw} : {}),
  });
  const text = await response.text();
  const data = text ? JSON.parse(text) : null;
  if (!response.ok) {
    throw Object.assign(new Error(`${response.status} ${data?.code ?? ''} ${data?.detail ?? ''}`.trim()),
      {status: response.status, problem: data, correlationId: response.headers.get('x-correlation-id')});
  }
  return data;
}
export {randomUUID};
<?php
// payniko.php - signed request helper (PHP 8.1+ with the curl extension).
function payniko_api(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null, array $extraHeaders = []): ?array
{
    $base = 'https://payniko.com';
    $key = getenv('API_KEY') ?: '';
    $secret = getenv('API_SECRET') ?: '';
    $merchant = getenv('MERCHANT_CODE') ?: '';
    if (!preg_match('/^api_[0-9a-f]{32}$/', $key) || !preg_match('/^[0-9a-f]{64}$/', $secret)
        || !preg_match('/^[0-9]{6}$/', $merchant)) {
        throw new RuntimeException('Set API_KEY, API_SECRET and MERCHANT_CODE.');
    }
    $method = strtoupper($method);
    // Hash and send the same bytes.
    $raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
    $timestamp = (string) time();
    $canonical = implode("\n", [$timestamp, $method, explode('?', $path, 2)[0], hash('sha256', $raw)]);
    $headers = array_merge($extraHeaders, [
        'X-Api-Key: ' . $key,
        'X-Merchant-Code: ' . $merchant,
        'X-Timestamp: ' . $timestamp,
        'X-Signature: ' . hash_hmac('sha256', $canonical, $secret),
        'Accept: application/json',
        'Accept-Language: en',
    ]);
    if ($raw !== '') $headers[] = 'Content-Type: application/json';
    if ($idempotencyKey !== null) $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    $ch = curl_init($base . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_TIMEOUT => 60,
    ]);
    if ($raw !== '') curl_setopt($ch, CURLOPT_POSTFIELDS, $raw);
    $response = curl_exec($ch);
    if ($response === false) throw new RuntimeException('Network error: ' . curl_error($ch));
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $data = $response === '' ? null : json_decode($response, true);
    if ($status >= 400) {
        throw new RuntimeException($status . ' ' . ($data['code'] ?? '') . ' ' . ($data['detail'] ?? ''), $status);
    }
    return $data;
}
# payniko.py - signed request helper (Python 3.9+, standard library only).
import hashlib, hmac, json, os, re, time, urllib.error, urllib.request

BASE_URL = "https://payniko.com"
KEY = os.environ.get("API_KEY", "")
SECRET = os.environ.get("API_SECRET", "")
MERCHANT = os.environ.get("MERCHANT_CODE", "")
if not (re.fullmatch(r"api_[0-9a-f]{32}", KEY) and re.fullmatch(r"[0-9a-f]{64}", SECRET)
        and re.fullmatch(r"[0-9]{6}", MERCHANT)):
    raise SystemExit("Set API_KEY, API_SECRET and MERCHANT_CODE.")


def api(method, path, body=None, idempotency_key=None, extra_headers=None):
    method = method.upper()
    # Hash and send the same bytes.
    raw = b"" if body is None else json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    timestamp = str(int(time.time()))
    canonical = "\n".join([timestamp, method, path.split("?", 1)[0], hashlib.sha256(raw).hexdigest()])
    signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    headers = dict(extra_headers or {})
    headers.update({
        "X-Api-Key": KEY, "X-Merchant-Code": MERCHANT,
        "X-Timestamp": timestamp, "X-Signature": signature,
        "Accept": "application/json", "Accept-Language": "en",
    })
    if raw:
        headers["Content-Type"] = "application/json"
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    request = urllib.request.Request(BASE_URL + path, data=raw or None, method=method, headers=headers)
    try:
        with urllib.request.urlopen(request, timeout=60) as response:
            text = response.read()
            return json.loads(text) if text else None
    except urllib.error.HTTPError as error:
        problem = json.loads(error.read() or b"{}")
        raise RuntimeError(f"{error.code} {problem.get('code')} {problem.get('detail')}") from None

التحقق من الإعداد

يُعد الاستعلام عن الرصيد استدعاءً أوليًا آمنًا، لأنه لا يُنشئ أي مورد.

./payniko.sh GET '/api/account/balance'
import {api} from './payniko.mjs';

const result = await api('GET', '/api/account/balance');
console.log(result);
<?php
require __DIR__ . '/payniko.php';

$result = payniko_api('GET', '/api/account/balance');
print_r($result);
from payniko import api

result = api("GET", "/api/account/balance")
print(result)

التوقيعات غير الصالحة المتكررة تقفل المفتاح مؤقتًا: 20 فشلًا من العنوان نفسه خلال 15 دقيقة تقفله لمدة 15 دقيقة، و300 فشل خلال ساعة تقفله لمدة ساعة (429 1000005 مع Retry-After). صحّح منطق التوقيع قبل إعادة المحاولة.

Idempotency-Key

يتطلب كل طلب POST الترويسة Idempotency-Key. ويُنفَّذ الطلب المُعاد إرساله بالمفتاح نفسه مرة واحدة فقط ويُعيد النتيجة الأصلية، مما يجعل إعادة المحاولة بعد انتهاء المهلة آمنة.

  • أنشئ مفتاحًا عشوائيًا جديدًا، مثل UUID، لكل عملية جديدة، واحفظه مع طلب الشراء قبل إرسال الطلب.
  • بعد انتهاء المهلة أو حدوث خطأ في الشبكة، أعِد إرسال المتن نفسه بالمفتاح نفسه.
  • لا تُعِد استخدام المفتاح مع متن مختلف مطلقًا؛ إذ يُرفض هذا الطلب بالرمز 409 (9000010، أو 0055503 في Direct API).
  • إذا كان الطلب الأصلي لا يزال قيد المعالجة، تكون الاستجابة 409: 9000039 في Hosted API (مع integration.retryable: true) أو 0055503 مع metadata.retry_class: retry_same_request في Direct API. أعد إرسال الطلب نفسه بعد انتظار قصير.
  • الصيغة: من 8 إلى 128 حرفًا من A-Z a-z 0-9 _ -. ويُعيد المفتاح المفقود أو غير الصالح 422 1000009.
  • يُربط الطلب الموقّع (X-Timestamp وX-Signature) لمدة 10 دقائق بأول Idempotency-Key أُرسل معه؛ والتوقيع نفسه مع مفتاح مختلف يُرفض بـ 409 9000010. أنشئ طابعًا زمنيًا وتوقيعًا جديدين لكل عملية جديدة. إذا كان هذا التحقق غير متاح مؤقتًا، تكون الاستجابة 503 1000011.

متطلبات الطلبات

  • أرسل الحقول الموثّقة فقط. وتُرفض الحقول غير المعروفة بالرمز 422 1000001.
  • يجب ألا تتضمن طلبات POST سلسلة استعلام. ويجب ألا تتضمن طلبات GET متنًا، ولا تقبل سوى معلمات الاستعلام الموثّقة.
  • يجب أن يكون المتن كائن JSON واحدًا لا يتجاوز عمق تداخله 7 مستويات، بما في ذلك الكائن ذو المستوى الأعلى.
  • تُطبّق حدود المعدل لكل عنوان IP (600 طلب في الدقيقة قبل المصادقة) ولكل مفتاح (300 طلب في الدقيقة). عند 429 (1000005)، انتظر مدة Retry-After ثم أعد المحاولة مع تباعد متزايد. تحمل كل استجابة X-RateLimit-Limit وX-RateLimit-Remaining.
الترويسةالمعنى
X-Correlation-Idالمعرّف الفريد للطلب. سجّله وأرفقه بطلبات الدعم.
Retry-Afterعدد الثواني الواجب انتظارها. يُرسل دائمًا مع 429، ومع 503 عندما تكون المدة معروفة.
X-RateLimit-Limitحد الطلبات للنافذة الحالية. لا توجد ترويسة X-RateLimit-Reset.
X-RateLimit-Remainingعدد الطلبات المتبقية في النافذة الزمنية الحالية.
Content-Languageلغة رسائل الاستجابة.

أخطاء المصادقة

الرمزHTTPالمعنىالإجراء الموصى به
0055502401 / 403رُفض الوصول. 401: الترويسة X-Api-Key أو X-Merchant-Code مفقودة، أو إحدى الترويسات مكررة أو غير صحيحة الصيغة، أو المفتاح غير معروف أو مُلغى، أو رمز التاجر غير مطابق، أو أُرسلت X-Environment بقيمة لا تطابق المفتاح. 403: الحساب أو صلاحية الوصول إلى Direct API غير مفعّلة.تحقّق من الترويسات ومن مفتاح API. لا تُعِد المحاولة تلقائيًا.
0055504401الترويسة X-Timestamp أو X-Signature مفقودة أو غير صحيحة الصيغة، أو أن ساعة خادمك تختلف عن توقيت خوادم Payniko بأكثر من 300 ثانية.اضبط ساعة خادمك (NTP)، ثم وقّع الطلب من جديد بطابع زمني حالي.
0055505401التوقيع لا يطابق الطلب.احسب قيمة التجزئة على بايتات المتن التي ترسلها تمامًا، ووقّع المسار دون سلسلة الاستعلام، واستخدم المفتاح السري الصحيح.
9000002401انتهت صلاحية الطلب قبل معالجته.وقّع الطلب من جديد بطابع زمني حالي، ثم أعِد إرساله.
1000009422الترويسة Idempotency-Key مفقودة أو غير صالحة.أرسل مع كل طلب POST مفتاحًا يتكون من 8 إلى 128 حرفًا من A-Z a-z 0-9 _ -.
9000010409سبق استخدام Idempotency-Key هذا لطلب مختلف، أو أُعيد إرسال الطلب الموقّع نفسه بمفتاح مختلف.أعِد المحاولة بالمفتاح نفسه والمتن نفسه؛ ولا تستخدم مفتاحًا جديدًا إلا لعملية جديدة.
1000005429عدد الطلبات كبير جدًا، أو حظر مؤقت بعد تكرار إخفاق التوقيع.انتظر المدة المحددة في Retry-After، ثم أعِد المحاولة مع زيادة فترات الانتظار تدريجيًا.

Hosted API

يُنشئ خادمك الدفعة ويُعيد توجيه العميل إلى صفحة الدفع في Payniko. ويجمع Payniko بيانات البطاقة وينفّذ مصادقة 3-D Secure، فلا تصل بيانات البطاقة إلى أنظمتك مطلقًا. ويُعد Hosted API أبسط خيارات التكامل.

خطوات التكامل

  1. إنشاء الدفعة

    يستدعي خادمك POST /api/payments/hosted. احفظ payment_id المُعاد مع طلب الشراء.

  2. إعادة توجيه العميل

    أعِد توجيه المتصفح إلى next_action.url أو افتحه في علامة تبويب جديدة؛ ولا تحمّله داخل iframe مطلقًا.

  3. إتمام الدفع

    يُدخل العميل بيانات البطاقة. وإذا اشترط البنك مصادقة 3-D Secure، تعرضها الصفحة. والرفض المؤقت في هذه المرحلة ليس نهائيًا؛ إذ تصل الدفعة إلى حالة نهائية دون أي إجراء من جانبك.

  4. عودة العميل

    تعرض الصفحة النتيجة مع عدّ تنازلي قصير، ثم تُعيد توجيه المتصفح مع معلمات موقّعة إلى success_url (فقط إذا كانت حالة الدفعة succeeded) أو إلى fail_url. راجع رابط العودة.

  5. تأكيد النتيجة

    انتظر إشعار Webhook أو استدعِ GET /api/payments/{payment_id}. ولا تنفّذ طلب الشراء إلا عندما تكون الحالة succeeded.

مسار الدفع

Your server                          Payniko                        Payer's browser
 1  POST /api/payments/hosted  ─────►  201  status: requires_action
                               ◄─────  next_action.url
 2  redirect (303) ───────────────────────────────────────────────►  payment page
 3                                     card + 3-D Secure  ◄────────  payer pays
 4                                     ───────────────────────────►  success_url / fail_url
                                                                     (signed parameters)
 5  webhook  ◄───────────────────────  payment.succeeded / payment.failed
    GET /api/payments/{payment_id} ──►  current status
POST/api/payments/hostedيتطلب Idempotency-Key

إنشاء دفعة

يُعيد 201 مع الدفعة الجديدة. وتؤدي إعادة إرسال الطلب نفسه بقيمة Idempotency-Key نفسها إلى إعادة 200 مع الدفعة نفسها.

متن الطلب

الحقلالنوعإلزاميالوصفمثال
merchant_order_idstringإلزاميمرجع طلب الشراء لديك: من 1 إلى 64 حرفًا من A-Z a-z 0-9 . _ -. ولا يُستخدم كل مرجع إلا لدفعة واحدة في كل بيئة، حتى إذا فشلت تلك الدفعة أو انتهت صلاحيتها. ولا يُفرَّق بين الأحرف الكبيرة والصغيرة عند التحقق من التفرد: إذ يُعامَل ORDER-1 وorder-1 بوصفهما المرجع نفسه.ORDER-2026-001
amountstringإلزاميمبلغ موجب بمنزلتين عشريتين ودون أصفار بادئة، على سبيل المثال "100.00". ويجب أن يكون ضمن حدود حسابك."100.00"
currencystringاختياريفي حال حذفه، تُطبَّق العملة الرئيسية للنظام.
USDEURAEDTRY
USD
webhook_urlstring · uriإلزامينقطة النهاية التي تستقبل webhooks هذه الدفعة. HTTPS عام على المنفذ 443، بحد أقصى 500 حرف، بدون معلومات مستخدم أو جزء (fragment). لا تُقبل العناوين الخاصة والمحلية، حتى في sandbox؛ ويجب أن يُحلّ اسم المضيف إلى عنوان IP عام وقت الطلب، وإلا 422 9000008.https://merchant.example/webhooks/payments
success_urlstring · uriإلزاميوجهة إعادة توجيه المتصفح بعد نجاح الدفعة. HTTPS، بحد أقصى 2048 حرفًا، ودون معلومات المستخدم (user info) أو المقطع (fragment). وتقبل البيئة التجريبية أيضًا http://localhost و127.0.0.1 و[::1].https://merchant.example/payment/success
fail_urlstring · uriإلزاميوجهة إعادة توجيه المتصفح عند فشل الدفعة أو انتهاء صلاحيتها أو إلغائها. وتسري عليها القواعد نفسها المطبقة على success_url.https://merchant.example/payment/failure
customerobjectإلزاميبيانات الاتصال الخاصة بالدافع؛ تُستخدم لنتيجة الدفع والإيصال وخطوات التحقق لدى البنك. يُقبل email وphone فقط؛ وأي حقل آخر داخل customer يُرفض بـ 422 1000001.{ … }
customer.emailstringإلزاميعنوان البريد الإلكتروني الحقيقي للدافع: RFC 5322، بحد أقصى 254 حرفًا، مع نطاق يحتوي على نقطة. يُحفظ بأحرف صغيرة.[email protected]
customer.phonestringإلزاميرقم الهاتف الحقيقي للدافع بالصيغة الدولية (+ رمز الدولة، E.164)، مثل +37061234567. قاعدة خاصة بتركيا: +90 متبوعًا بالرقم الوطني المكوّن من 10 أرقام (5xx جوال، 2xx و3xx و4xx ثابت، 8xx خاص). الرقم المرسل بدون رمز الدولة يُعامل كرقم تركي (+90).+37061234567

تنطبق هذه الروابط الثلاثة على هذه الدفعة فقط، ولا تؤخذ من إعدادات لوحة التحكم.

لا تطلب صفحة الدفع من الدافع عنوان البريد الإلكتروني ورقم الهاتف مرة أخرى. حفاظًا على الخصوصية، لا يُعاد customer في كائن الدفع ولا في الـ webhooks.

الحقلالسببالرسالة في errors
customer.emailصيغة غير صالحة أو أطول من 254 حرفًا.customer.email must be a valid e-mail address (max 254 characters).
customer.emailعنوان نائب أو نموذجي أو مؤقت.customer.email must be the payer's real address; placeholder, example or disposable addresses (example.com, test@…, mailinator…) are not accepted.
customer.phoneليس رقمًا صالحًا بالصيغة الدولية.customer.phone must be a valid phone number in international format (e.g. +37061234567).
customer.phoneنمط أرقام مكررة أو متسلسلة.customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted.
customer.phoneرقم تركي ليس بصيغة +90 متبوعًا بعشرة أرقام تبدأ بـ 2 أو 3 أو 4 أو 5 أو 8.customer.phone for Türkiye must be +90 followed by a 10-digit number starting with 2, 3, 4, 5 or 8 (e.g. +905321234567).

HTTP 422 · 1000001

{
  "type": "https://payniko.com/problems/1000001",
  "title": "Validation failed",
  "status": 422,
  "detail": "The submitted fields are not valid.",
  "code": "1000001",
  "correlation_id": "4c7a1e9d-2b3f-4d85-9a6e-0f1b2c3d4e5f",
  "errors": {
    "customer.email": [
      "customer.email must be the payer's real address; placeholder, example or disposable addresses (example.com, test@…, mailinator…) are not accepted."
    ],
    "customer.phone": [
      "customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted."
    ]
  }
}

إذا رُفض الطلب بسبب customer، فاعرض السبب للدافع بوضوح (مثلًا: أدخل عنوان بريدك الإلكتروني أو رقم هاتفك الخاص) واسمح له بتصحيح الحقل قبل إنشاء الدفع مرة أخرى. الرسائل في errors باللغة الإنجليزية ومخصصة لسجلاتك ولفريق الدعم.

تطبّق صفحات روابط الدفع في Payniko القواعد نفسها على عنوان البريد الإلكتروني ورقم الهاتف اللذين يدخلهما الدافع هناك، وتعرض له سبب الرفض.

متن الطلب · مثال

{
  "merchant_order_id": "ORDER-2026-001",
  "amount": "100.00",
  "currency": "USD",
  "webhook_url": "https://merchant.example/webhooks/payments",
  "success_url": "https://merchant.example/payment/success",
  "fail_url": "https://merchant.example/payment/failure",
  "customer": {
    "email": "[email protected]",
    "phone": "+37061234567"
  }
}
KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
./payniko.sh POST '/api/payments/hosted' '{
  "merchant_order_id": "ORDER-2026-001",
  "amount": "100.00",
  "currency": "USD",
  "webhook_url": "https://merchant.example/webhooks/payments",
  "success_url": "https://merchant.example/payment/success",
  "fail_url": "https://merchant.example/payment/failure",
  "customer": {
    "email": "[email protected]",
    "phone": "+37061234567"
  }
}' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const payment = await api('POST', '/api/payments/hosted', {
  "merchant_order_id": "ORDER-2026-001",
  "amount": "100.00",
  "currency": "USD",
  "webhook_url": "https://merchant.example/webhooks/payments",
  "success_url": "https://merchant.example/payment/success",
  "fail_url": "https://merchant.example/payment/failure",
  "customer": {
    "email": "[email protected]",
    "phone": "+37061234567"
  }
}, key);
console.log(payment);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$payment = payniko_api('POST', '/api/payments/hosted', [
    "merchant_order_id" => 'ORDER-2026-001',
    "amount" => '100.00',
    "currency" => 'USD',
    "webhook_url" => 'https://merchant.example/webhooks/payments',
    "success_url" => 'https://merchant.example/payment/success',
    "fail_url" => 'https://merchant.example/payment/failure',
    "customer" => [
        "email" => '[email protected]',
        "phone" => '+37061234567',
    ],
], $key);
print_r($payment);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
payment = api("POST", "/api/payments/hosted", {
    "merchant_order_id": "ORDER-2026-001",
    "amount": "100.00",
    "currency": "USD",
    "webhook_url": "https://merchant.example/webhooks/payments",
    "success_url": "https://merchant.example/payment/success",
    "fail_url": "https://merchant.example/payment/failure",
    "customer": {
        "email": "[email protected]",
        "phone": "+37061234567"
    }
}, key)
print(payment)

الاستجابة · 201

يُعيد { "data": Payment } مع status: requires_action وnext_action.url.

الاستجابة · مثال

{
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "requires_action",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": {
      "type": "redirect",
      "url": "https://payniko.com/l/example-checkout",
      "expires_at": "2026-11-02T12:00:00.000Z"
    },
    "card": null,
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": null
  }
}

استجابات أخرى

  • 200: أُعيد إرسال المفتاح والمتن نفسيهما. وتُعاد الدفعة الأصلية دون إنشاء دفعة جديدة.
  • 409 9000010: سبق استخدام هذا المفتاح مع متن مختلف.
  • 409 9000039: الطلب الأول بهذا المفتاح لا يزال قيد المعالجة. أعد إرسال الطلب نفسه بعد انتظار قصير.
  • 409 9000007: توجد دفعة مسبقًا لقيمة merchant_order_id هذه.
  • 422 9000005: المبلغ خارج حدود حسابك؛ ويتضمن limits النطاق المسموح به.
  • 422 1000001 مع customer أو customer.email أو customer.phone في errors: بيانات الاتصال مفقودة أو غير صالحة.
  • 422 1000001، 1000020، 1000021، 9000008، 9000038: أحد الحقول غير صالح؛ وتجد التفاصيل في errors وintegration.violations.

HTTP 422 · 9000005

{
  "type": "https://payniko.com/problems/9000005",
  "title": "Invalid amount",
  "status": 422,
  "detail": "The amount is outside the account limits.",
  "code": "9000005",
  "correlation_id": "6d1f0c2e-8a4b-4c7d-9e21-3f5a7b9c1d20",
  "limits": {
    "currency_code": "USD",
    "min_amount": "25.00",
    "max_amount": null
  }
}

تُقيَّم الحدود بالعملة الرئيسية للنظام (limits.currency_code) بعد تحويل مبلغك. وتعني max_amount: null عدم وجود حد أعلى. وقيم الحدود الواردة في هذا المثال توضيحية.

كائن الدفعة

تُعيد استدعاءات الإنشاء والاستعلام هذا الكائن داخل data، ويتضمنه كل إشعار Webhook خاص بالدفع. وبنيته متطابقة في Hosted API وDirect API.

الحقلالنوعالوصف
payment_idstring · LUVEI-ABCD-123456معرّف الدفعة في Payniko . احفظه مع طلب الشراء.
merchant_order_idstringمرجع طلب الشراء لديك، كما أُرسل تمامًا.
flowstringhosted لـ Hosted API، وdirect لـ Direct API.
hosteddirect
statusstringالحالة الحالية للدفعة؛ راجع جدول الحالات أدناه.
requires_actionprocessingsucceededfailedexpiredcancelled
amountstringالمبلغ بمنزلتين عشريتين.
currencystringعملة المبلغ؛ وهي العملة الرئيسية للنظام إذا لم تُحدَّد عملة.
environmentstringبيئة المفتاح الذي أنشأ الدفعة.
sandboxlive
next_actionobject | nullالإجراء التالي المطلوب من العميل. ولا يُعيَّن إلا ما دامت قيمة status هي requires_action.
next_action.typestringدائمًا redirect.
next_action.urlstring · uriافتح هذا الرابط في متصفح العميل تمامًا كما أُعيد. ولا تُنشئه أو تعدّله مطلقًا.
next_action.expires_atstring · date-timeالموعد النهائي لاستخدام الرابط. في Direct API، يمثل نهاية خطوة 3-D Secure وقد يسبق expires_at الخاص بالدفعة؛ وفي Hosted API، يمثل الموعد النهائي للدفعة. وبعد انقضائه، تنتقل الدفعة إلى حالتها التالية وتصبح قيمة next_action هي null.
cardobject | nullبيانات البطاقة المقنّعة، بمجرد معرفتها.
card.bin6string | nullالأرقام الستة الأولى.
card.last_fourstring | nullالأرقام الأربعة الأخيرة.
card.networkstring | nullشبكة البطاقة.
visamastercardamextroyunionpaydiscoverjcbdinersmaestro
card.bankstring | nullالبنك المُصدِر، إن كان معروفًا.
failureobject | nullسبب الفشل. ولا يُعيَّن إلا عندما تكون قيمة status هي failed.
failure.codestringرمز السبب لاستخدامه في منطق تطبيقك؛ راجع القائمة أدناه.
failure.messagestringرسالة قصيرة مناسبة لعرضها على العميل كنص عادي.
created_atstring · date-timeوقت إنشاء الدفعة.
expires_atstring · date-timeالموعد النهائي لإتمام الدفعة.
completed_atstring · date-time | nullوقت وصول الدفعة إلى حالة نهائية.

قد تكون قيمة next_action هي null لفترة وجيزة حتى في الحالة requires_action؛ وفي هذه الحالة استعلم عن الدفعة مجددًا. ولا يُعاد مطلقًا رقم البطاقة الكامل أو رمز CVV أو تاريخ انتهاء الصلاحية أو اسم حامل البطاقة. وقد تكون قيمتا card.network وcard.bank هي null عندما تكونان غير معروفتين.

بيانات customer المرسلة عند إنشاء الدفع ليست جزءًا من هذا الكائن ولا تظهر في الـ webhooks.

دفعة · مثال

{
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "requires_action",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": {
      "type": "redirect",
      "url": "https://payniko.com/l/example-checkout",
      "expires_at": "2026-11-02T12:00:00.000Z"
    },
    "card": null,
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": null
  }
}

حالات دفعات Hosted API

الحالةالمعنىالإجراء الموصى به
requires_actionأُنشئت الدفعة ولم يُكملها العميل بعد.أعِد توجيه العميل إلى next_action.url (حتى next_action.expires_at).
processingدفع العميل، واستجابة البنك قيد الانتظار.انتظر الـ webhook ولا تنشئ دفعة جديدة. إذا لم يرد البنك أبدًا، فقد تبقى الدفعة في processing حتى التسوية ثم تنتهي بـ failed أو expired.
succeededتم تحصيل الأموال.نفّذ طلب الشراء.
failedلم تكتمل الدفعة. ويتضمن failure السبب.اعرض failure.message. ولإعادة المحاولة، أنشئ دفعة جديدة بقيمة merchant_order_id جديدة.
expiredلم يُكمل العميل الدفع خلال 24 ساعة (expires_at). وإذا انصرف العميل عن الدفع في منتصفه، فقد تنتهي الدفعة بالحالة failed بدلًا من ذلك.إذا كان العميل لا يزال يرغب في الدفع، فأنشئ دفعة جديدة.
cancelledأُلغيت الدفعة.لا تنفّذ طلب الشراء.
  • الرفض المؤقت على صفحة الدفع ليس نهائيًا: فقد تبقى الدفعة في الحالة requires_action، وقد تتلقى payment.updated. وفي النهاية تتلقى نتيجة نهائية واحدة.
  • الحالات succeeded وfailed وexpired وcancelled حالات نهائية. وقد تنجح الدفعة الموجودة في الحالة processing حتى بعد expires_at.
  • تعامل مع أي حالة غير مدرجة هنا على أنها processing، واستعلم عن الدفعة مجددًا.

رموز الفشل

تتضمن الدفعة في الحالة failed الحقلين failure.code وfailure.message. استخدم الرمز في منطق تطبيقك، واعرض الرسالة على العميل كنص عادي.

الرمزالمعنى
payment_declinedرفض البنك الدفعة.
do_not_honorرفض البنك الدفعة دون ذكر السبب.
insufficient_fundsرصيد البطاقة غير كافٍ.
card_limit_reachedتم بلوغ أحد حدود البطاقة.
card_details_invalidبيانات البطاقة غير صالحة.
card_expiry_invalidتاريخ انتهاء الصلاحية غير صالح أو أن البطاقة منتهية الصلاحية.
card_security_code_invalidرمز CVV غير صالح.
card_not_activeالبطاقة محظورة أو غير مفعّلة.
card_brand_not_supportedشبكة هذه البطاقة غير مدعومة.
three_ds_failedفشلت مصادقة 3-D Secure أو لم تكتمل.
payment_not_permittedلا يُسمح باستخدام البطاقة لهذا النوع من المدفوعات.
amount_not_allowedالمبلغ غير مسموح به لهذه البطاقة.
currency_not_supportedالعملة غير مدعومة لهذه البطاقة.
payment_details_rejectedرفض البنك بيانات الدفع.
duplicate_paymentصنّف البنك الدفعة على أنها مكررة.
payment_timed_outلم تكتمل الدفعة في الوقت المحدد.
bank_unavailableتعذّر الاتصال بالبنك.
bank_requested_retryطلب البنك إعادة المحاولة في وقت لاحق.
service_unavailableتعذّرت معالجة الدفعة في الوقت الحالي.

قد تُضاف رموز جديدة؛ لذا تعامل مع أي رمز غير معروف على أنه payment_declined. وقد تتضمن failure.message النص الصادر عن البنك نفسه؛ وإلا فهي باللغة الإنجليزية في إشعارات Webhook، وتتبع Accept-Language في طلبات GET.

متطلبات إعادة التوجيه

  • أعِد توجيه الصفحة كاملة (303 أو window.location) إلى next_action.url، أو افتحه في علامة تبويب جديدة. ولا تُضمّنه في iframe؛ إذ تمنع الصفحة عرضها داخل الإطارات.
  • استخدم الرابط قبل next_action.expires_at. وبعد انتهاء صلاحيته، استعلم عن الدفعة.
  • تتبع لغة الصفحة إعدادات متصفح العميل؛ ولا يلزم إرسال أي معلمة.

رابط العودة

عند عودة العميل، يُعاد توجيه المتصفح إلى success_url فقط إذا كانت حالة الدفعة succeeded، وإلى fail_url في جميع الحالات الأخرى. وتُضاف إلى الرابط معلمات موقّعة. تحقّق منها لعرض الصفحة المناسبة، ولا تنفّذ طلب الشراء إلا بعد أن يؤكد إشعار Webhook أو طلب GET الحالة succeeded.

الحقلالوصف
payment_idمعرّف الدفعة .
merchant_order_idمرجع طلب الشراء لديك. وهذه القيمة غير موقّعة: استخدمها للعثور على طلب الشراء فقط، واعتمد في المطابقة على payment_id.
statusحالة الدفعة وقت إعادة التوجيه، بالقيم نفسها كما في GET. دائمًا succeeded على success_url. على fail_url، عادةً failed أو expired أو cancelled؛ ومع Hosted API قد تكون requires_action أو processing إذا غادر الدافع قبل وصول الدفعة إلى حالة نهائية، مثلًا عبر "العودة إلى المتجر".
failure_codeلا يظهر إلا عندما تكون قيمة status هي failed؛ وهو مطابق لـ failure.code.
tsوقت Unix الذي أُنشئ فيه رابط العودة. وقد يفتح العميل الرابط لاحقًا، لذا فإن قِدم ts وحده لا يدل على التزوير.
key_versionيحدد المفتاح الذي وقّع الرابط: 1 لأول مفتاح أُنشئ في تلك البيئة، و2 للمفتاح التالي، وهكذا. ولا تتغير القيمة مطلقًا للدفعة الواحدة.
sigقيمة HMAC-SHA256 بالنظام الست عشري وبأحرف صغيرة، مكونة من 64 حرفًا.

السلسلة المراد توقيعها

ts + "." + payment_id + "." + status + "." + failure_code   // failure_code empty: keep the last dot
  1. اختر المفتاح السري المرتبط بالمفتاح المحدد في key_version. وهو المفتاح الذي أنشأ الدفعة، حتى إذا استُبدلت المفاتيح منذ ذلك الحين.

  2. كوّن السلسلة من القيم بعد فك ترميز URL. وإذا لم يكن failure_code موجودًا، فأبقِ على النقطة الأخيرة.

  3. احسب HMAC-SHA256 وقارنه بـ sig باستخدام مقارنة ثابتة الزمن. وتأكّد من أن payment_id يخص إحدى دفعاتك.

  4. استعلم عن الدفعة باستخدام GET /api/payments/{payment_id} (أو انتظر إشعار Webhook) قبل تنفيذ طلب الشراء.

  • يُحتفظ بمعلمات الاستعلام الخاصة بك. وإذا كان الرابط يتضمن مسبقًا معلمة تحمل أحد الأسماء المذكورة أعلاه، فإنها تُستبدل.
  • إذا تعذّر توقيع الرابط بسبب خطأ مؤقت، فلن يتضمن الرابط أيًا من هذه المعلمات. اعتبر النتيجة غير معروفة واستعلم عن الدفعة.
// Node.js 20+: check the signed parameters on success_url / fail_url.
import {createHmac, timingSafeEqual} from 'node:crypto';

// query: URLSearchParams of the return URL.
// secretFor(keyVersion): the API secret of that key, or undefined if you do not know it.
export function verifyReturn(query, secretFor) {
  const sig = query.get('sig') ?? '';
  const secret = secretFor(query.get('key_version'));
  if (!secret || !/^[0-9a-f]{64}$/.test(sig)) return false;
  const signed = [query.get('ts') ?? '', query.get('payment_id') ?? '',
    query.get('status') ?? '', query.get('failure_code') ?? ''].join('.');
  const expected = createHmac('sha256', secret).update(signed, 'utf8').digest();
  return timingSafeEqual(expected, Buffer.from(sig, 'hex'));
}
// Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
<?php
// PHP 8.1+: check the signed parameters on success_url / fail_url.
// $secretFor(string $keyVersion): the API secret of that key, or null if you do not know it.
function payniko_verify_return(array $query, callable $secretFor): bool
{
    $sig = (string) ($query['sig'] ?? '');
    $secret = $secretFor((string) ($query['key_version'] ?? ''));
    if (!$secret || !preg_match('/^[0-9a-f]{64}$/', $sig)) return false;
    $signed = implode('.', [(string) ($query['ts'] ?? ''), (string) ($query['payment_id'] ?? ''),
        (string) ($query['status'] ?? ''), (string) ($query['failure_code'] ?? '')]);
    return hash_equals(hash_hmac('sha256', $signed, $secret), $sig);
}

// $ok = payniko_verify_return($_GET, fn (string $v) => $secretsByKeyVersion[$v] ?? null);
// Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
# Python 3.9+: check the signed parameters on success_url / fail_url.
import hashlib, hmac, re


def verify_return(query, secret_for) -> bool:
    # query: dict-like (for example Flask request.args); secret_for(key_version) -> secret or None
    sig = query.get("sig", "")
    secret = secret_for(query.get("key_version"))
    if not secret or not re.fullmatch(r"[0-9a-f]{64}", sig):
        return False
    signed = ".".join([query.get("ts", ""), query.get("payment_id", ""),
                       query.get("status", ""), query.get("failure_code", "")])
    expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

# Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.

روابط العودة · مثال

https://merchant.example/payment/success?payment_id=LUVEI-PAYN-654321&merchant_order_id=ORDER-2026-001&status=succeeded&ts=1793001234&key_version=1&sig=200e8a5d4a7a03ead362a2c076408f9d8764f8f636f58bd4c17d78a50d59eb8a
string to sign: 1793001234.LUVEI-PAYN-654321.succeeded.

https://merchant.example/payment/failure?payment_id=LUVEI-PAYN-654321&merchant_order_id=ORDER-2026-001&status=failed&failure_code=insufficient_funds&ts=1793001234&key_version=1&sig=1cd0a104ec4ec4f5b2933d48d14c0eb33d949dd7f27936a067ab8f68b413b70d
string to sign: 1793001234.LUVEI-PAYN-654321.failed.insufficient_funds

secret used in this example (fake): example_secret_not_real
GET/api/payments/{payment_id}

الاستعلام عن دفعة

يُعيد { "data": Payment } مع الحالة الحالية. ولا يؤدي الاستعلام عن الدفعة إلى تعديلها مطلقًا، لذا يمكنك استدعاء نقطة النهاية هذه كلما لزم الأمر: بعد العودة، أو عندما يبدو إشعار Webhook غير متسق، أو بعد انتهاء المهلة.

المعلمات

الحقلالنوعإلزاميالوصف
payment_idstring · pathإلزاميقيمة payment_id المُعادة عند إنشاء الدفعة. ويمكن لأي مفتاح من البيئة نفسها الاستعلام عنها.
./payniko.sh GET '/api/payments/LUVEI-PAYN-123456'
import {api} from './payniko.mjs';

const payment = await api('GET', '/api/payments/LUVEI-PAYN-123456');
console.log(payment);
<?php
require __DIR__ . '/payniko.php';

$payment = payniko_api('GET', '/api/payments/LUVEI-PAYN-123456');
print_r($payment);
from payniko import api

payment = api("GET", "/api/payments/LUVEI-PAYN-123456")
print(payment)

الأخطاء المحتملة

إشعارات Webhook الخاصة بالدفع

تُرسل الأحداث التالية إلى webhook_url الخاص بالدفعة. ويُرسل كل حدث دفع مرة واحدة على الأكثر لكل دفعة.

الحدثالمعنى
payment.succeededحالة الدفعة succeeded. نفّذ طلب الشراء.
payment.failedانتهت الدفعة دون نجاح: failed أو expired أو cancelled. راجع data.status.
payment.updatedلا يزال مطلوبًا اتخاذ إجراء من العميل (requires_action)، على سبيل المثال بعد رفض مؤقت. وهذه ليست نتيجة نهائية.
refund.* · payment.voided · dispute.*الأحداث اللاحقة لهذه الدفعة (الاسترداد والإبطال والنزاعات). راجع إشعارات Webhook.

payment.succeeded

{
  "event_id": "e0743f4c-a297-4bf0-8085-ceb7d9d5e181",
  "event_type": "payment.succeeded",
  "environment": "sandbox",
  "occurred_at": "2026-11-01T12:02:00.000Z",
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "succeeded",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": null,
    "card": {
      "bin6": "400000",
      "last_four": "0002",
      "network": "visa",
      "bank": null
    },
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": "2026-11-01T12:02:00.000Z"
  }
}

تحقّق من كل إشعار Webhook: يجب أن تساوي قيمة HMAC-SHA256 لـ X-Timestamp + "." + raw body، المحسوبة بالمفتاح السري المرتبط بالمفتاح الذي أنشأ الدفعة، قيمةَ X-Signature، وأن يكون X-Timestamp ضمن 300 ثانية. احفظ كل event_id مرة واحدة، وأرسل استجابة 2xx دون تأخير. وللاطلاع على أمثلة الشيفرة وسلوك إعادة المحاولة، راجع إشعارات Webhook.

الأخطاء

الرمزHTTPالمعنىالإجراء الموصى به
1000001422الطلب غير صالح: حقل غير معروف، أو صيغة غير صالحة، أو سلسلة استعلام في طلب POST، أو معلمة استعلام غير مدعومة، أو متن ليس كائن JSON. ويتضمن errors الحقول المعنية.صحّح الحقول المذكورة.
1000009422الترويسة Idempotency-Key مفقودة أو غير صالحة.أرسل مع كل طلب POST مفتاحًا يتكون من 8 إلى 128 حرفًا من A-Z a-z 0-9 _ -.
9000010409سبق استخدام Idempotency-Key هذا لطلب مختلف، أو أُعيد إرسال الطلب الموقّع نفسه بمفتاح مختلف.أعِد المحاولة بالمفتاح نفسه والمتن نفسه؛ ولا تستخدم مفتاحًا جديدًا إلا لعملية جديدة.
9000039409الطلب الأول بهذا الـ Idempotency-Key لا يزال قيد المعالجة (Hosted API).أعد إرسال الطلب نفسه بعد انتظار قصير.
9000007409توجد دفعة مسبقًا لقيمة merchant_order_id هذه (Hosted API).استعلم عن الدفعة القائمة أو استخدم مرجعًا جديدًا لطلب الشراء.
9000005422المبلغ خارج حدود حسابك (Hosted API). ويتضمن limits النطاق المسموح به.عدّل المبلغ.
1000020422صيغة المبلغ غير صالحة.أرسل المبلغ نصًا موجبًا بمنزلتين عشريتين ودون أصفار بادئة، على سبيل المثال "100.00".
1000021422العملة غير مدعومة.استخدم USD أو EUR أو AED أو TRY، أو احذف الحقل currency.
9000008422قيمة webhook_url غير صالحة.استخدم رابط HTTPS عامًا على المنفذ 443 لا يتجاوز طوله 500 حرف.
9000038422قيمة success_url أو fail_url غير صالحة.استخدم رابط HTTPS لا يتجاوز طوله 2048 حرفًا. وتقبل البيئة التجريبية أيضًا http://localhost.
9000040403Hosted API غير مفعّل لحسابك.تواصل مع فريق الدعم.
9000013409عملية البيئة التجريبية غير متاحة، على سبيل المثال عند استدعاء نقطة نهاية مخصصة للبيئة التجريبية بالمفتاح الحي.استخدم المفتاح التجريبي أو أعِد المحاولة لاحقًا.
9000004403حسابك مقيّد ولا يتيح تنفيذ هذه العملية.تواصل مع فريق الدعم.
3000001503سعر الصرف غير متاح مؤقتًا.أعِد المحاولة لاحقًا.
1000011503الخدمة غير متاحة مؤقتًا.أعِد إرسال الطلب نفسه. واستعلم عن الموارد القائمة قبل إنشاء موارد جديدة.
1000013503هذه الميزة غير متاحة بعد.أعِد المحاولة بعد مرور retry_after ثانية.
1000014503تجري حاليًا صيانة مجدولة. ويتضمن until وnote التفاصيل.أعِد المحاولة بعد انقضاء المدة المحددة في Retry-After.

رفض البطاقة ليس خطأ HTTP؛ إذ تنتهي الدفعة بالحالة failed مع failure.code. وللاطلاع على القائمة الكاملة، راجع رموز الأخطاء.

الاختبار ببطاقات البيئة التجريبية

أنشئ الدفعة بالمفتاح التجريبي، وافتح next_action.url، وادفع بإحدى البطاقات أدناه، مع استخدام أي تاريخ انتهاء صلاحية مستقبلي وأي رمز CVV من 3 أرقام. وللمزيد من التفاصيل، راجع البيئة التجريبية والبطاقات التجريبية.

رقم البطاقةالنتيجة
4000000000000010موافقة دون 3-D Secure. وفي Direct API، تُعيد استجابة الإنشاء الحالة succeeded مباشرةً.
4000000000000002 · 4355090000044242خطوة 3-D Secure تجريبية، تليها الحالة succeeded.
4000000000000051 · 4355090000040000مرفوضة قبل 3-D Secure (do_not_honor). Hosted API: قد تبقى الدفعة في requires_action نحو 5 دقائق وترسل payment.updated قبل أن تنتهي بـ failed. Direct API: تعيد استجابة الإنشاء failed مباشرة.
4000000000000093 · 4355090000005151خطوة 3-D Secure تجريبية، تليها الحالة failed مع insufficient_funds.
4000000000000044لا استجابة من البنك ولا 3-D Secure؛ وتبقى الدفعة في الحالة processing.
4355090000089999Hosted API: processing بدون خطوة 3-D Secure. Direct API: خطوة 3-D Secure تجريبية ثم processing.

مثال متكامل

خادم Node.js مبسّط يُنشئ دفعة، ويُعيد توجيه العميل، ويعالج العودة وإشعار Webhook. ويستخدم الدالة المساعدة للطلبات ودوال التحقق الواردة في إشعارات Webhook ورابط العودة.

server.mjs

// server.mjs: Hosted API from start to end (Node.js 20+, no dependencies).
import http from 'node:http';
import {randomUUID} from 'node:crypto';
import {api} from './payniko.mjs';                  // request helper (Authentication)
import {verifyWebhook} from './verify-webhook.mjs'; // webhook check (Webhooks)
import {verifyReturn} from './verify-return.mjs';   // return URL check (Return URL)

const site = 'https://merchant.example';
const orders = new Map();      // use your database
const seenEvents = new Set();  // use your database
const readBody = async (req) => { const parts = []; for await (const p of req) parts.push(p); return Buffer.concat(parts); };

http.createServer(async (req, res) => {
  const url = new URL(req.url, site);
  // 1. Create the payment, 2. redirect the payer.
  if (req.method === 'POST' && url.pathname === '/checkout') {
    const form = JSON.parse((await readBody(req)).toString('utf8')); // your checkout form: the payer's e-mail and phone
    const orderId = 'ORDER-' + Date.now();
    const key = randomUUID();
    orders.set(orderId, {key, status: 'new'});      // save the key before you call the API
    const {data} = await api('POST', '/api/payments/hosted', {
      merchant_order_id: orderId, amount: '100.00', currency: 'USD',
      customer: {email: form.email, phone: form.phone},
      webhook_url: `${site}/webhooks/payniko`,
      success_url: `${site}/payment/success`, fail_url: `${site}/payment/failure`,
    }, key);
    Object.assign(orders.get(orderId), {paymentId: data.payment_id, status: data.status});
    // next_action can be null for a moment: read the payment again before you redirect.
    res.writeHead(303, {Location: data.next_action ? data.next_action.url : `/orders/${orderId}`}).end();
    return;
  }
  // Return URL: show a page, but trust only the webhook or a GET.
  if (req.method === 'GET' && (url.pathname === '/payment/success' || url.pathname === '/payment/failure')) {
    const signed = verifyReturn(url.searchParams, () => process.env.API_SECRET);
    let status = 'unknown';
    if (signed) status = (await api('GET', `/api/payments/${url.searchParams.get('payment_id')}`)).data.status;
    res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
    res.end(status === 'succeeded' ? 'Thank you, payment received.' : 'Payment not completed.');
    return;
  }
  // Webhook: verify, store event_id once, answer 2xx fast.
  if (req.method === 'POST' && url.pathname === '/webhooks/payniko') {
    const raw = await readBody(req);
    if (!verifyWebhook(raw, req.headers, process.env.API_SECRET)) { res.writeHead(401).end(); return; }
    const event = JSON.parse(raw.toString('utf8'));
    if (!seenEvents.has(event.event_id)) {          // use a unique index in your database
      seenEvents.add(event.event_id);
      const order = orders.get(event.data.merchant_order_id);
      if (order && event.event_type.startsWith('payment.')) order.status = event.data.status;
    }
    res.writeHead(204).end();
    return;
  }
  res.writeHead(404).end();
}).listen(8080);

إشعارات Webhook

ترسل Payniko طلب POST موقّعًا إلى webhook_url الخاص بك عند تغيّر حالة دفعة أو استرداد أو نزاع أو سحب؛ وتتم المحاولة الأولى عادةً خلال دقيقة من الحدث. تحقق من التوقيع، واحفظ الحدث، وأجب بـ 2xx قبل معالجته.

الأحداث

الحدثالمعنى
payment.succeededحالة الدفعة succeeded. نفّذ طلب الشراء.
payment.failedانتهت الدفعة دون نجاح: failed أو expired أو cancelled. راجع data.status.
payment.updatedلا يزال مطلوبًا اتخاذ إجراء من العميل (requires_action)، على سبيل المثال بعد رفض مؤقت. وهذه ليست نتيجة نهائية.
refund.completedاكتملت عملية استرداد.
payment.voidedاكتملت عملية إبطال.
refund.rejectedرُفضت عملية استرداد أو إبطال.
refund.failedفشلت عملية استرداد أو إبطال.
refund.cancelledأُلغيت عملية استرداد أو إبطال.
dispute.openedفُتح نزاع.
dispute.in_defenseالنزاع قيد المراجعة.
dispute.wonحُسم النزاع لصالحك.
dispute.lostحُسم النزاع ضدك.
withdrawal.sentأُرسلت عملية سحب.
withdrawal.rejectedرُفضت عملية سحب.

تُرسل أحداث الدفع والاسترداد والنزاعات إلى webhook_url الخاص بالدفعة، وأحداث السحب إلى webhook_url الخاص بعملية السحب. ويُرسل كل حدث دفع مرة واحدة على الأكثر لكل دفعة، وكل حدث استرداد أو نزاع أو سحب مرة واحدة على الأكثر لكل عملية استرداد أو نزاع أو عملية سحب ولكل حالة.

محتوى الإشعار

الحقلالنوعالوصف
event_idstring · uuidالمعرّف الفريد للحدث؛ وهو مطابق للترويسة X-Event-Id.
event_typestringاسم الحدث، كما هو مدرج في الجدول أعلاه.
environmentstringsandbox أو live.
occurred_atstring · date-timeوقت وقوع الحدث.
subjectobjectلا يظهر إلا في أحداث الاسترداد والنزاعات والسحب: type وid لعملية الاسترداد أو النزاع أو عملية السحب.
dataobjectفي أحداث الدفع، كائن الدفعة (راجع Hosted API)؛ وفي الأحداث الأخرى، الحقول المدرجة أدناه.

payment.succeeded

{
  "event_id": "e0743f4c-a297-4bf0-8085-ceb7d9d5e181",
  "event_type": "payment.succeeded",
  "environment": "sandbox",
  "occurred_at": "2026-11-01T12:02:00.000Z",
  "data": {
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001",
    "flow": "hosted",
    "status": "succeeded",
    "amount": "100.00",
    "currency": "USD",
    "environment": "sandbox",
    "next_action": null,
    "card": {
      "bin6": "400000",
      "last_four": "0002",
      "network": "visa",
      "bank": null
    },
    "failure": null,
    "created_at": "2026-11-01T12:00:00.000Z",
    "expires_at": "2026-11-02T12:00:00.000Z",
    "completed_at": "2026-11-01T12:02:00.000Z"
  }
}

أحداث الاسترداد والنزاعات والسحب

العنصر المعنيالحقل
refundid، status، operation_type، amount، currency، payment_id أو payment_code، merchant_order_id
disputeid، status، amount، currency، payment_id أو payment_code، merchant_order_id، reason، evidence_due_at (عند تعيينه)
withdrawalid، status، amount، currency، usdt_amount، asset، network، tx_hash
  • تحمل أحداث الاسترداد والنزاعات مرجعًا واحدًا للدفعة: payment_id، أو payment_code في بعض الأحداث. ويتضمن الحدث refund.completed دائمًا payment_id. وعندما يحمل الحدث payment_code، طابِق طلب الشراء باستخدام merchant_order_id.
  • قبل اتخاذ أي إجراء بناءً على حدث، استعلم عن عملية الاسترداد أو النزاع أو عملية السحب باستخدام GET للتأكد من حالتها الحالية.

refund.completed

{
  "event_id": "c4b2e7d1-8a3f-4e65-b0d9-5f1a2c3e4d70",
  "event_type": "refund.completed",
  "environment": "sandbox",
  "occurred_at": "2026-11-02T09:20:00.000Z",
  "subject": {
    "type": "refund",
    "id": "LUVEI-RFND-100001"
  },
  "data": {
    "id": "LUVEI-RFND-100001",
    "status": "completed",
    "operation_type": "refund",
    "amount": "100.00",
    "currency": "USD",
    "payment_id": "LUVEI-PAYN-123456",
    "merchant_order_id": "ORDER-2026-001"
  }
}

withdrawal.sent

{
  "event_id": "b3e8f0a2-5c71-4d96-8e14-7a2c9d0f6b35",
  "event_type": "withdrawal.sent",
  "environment": "sandbox",
  "occurred_at": "2026-11-02T10:00:00.000Z",
  "subject": {
    "type": "withdrawal",
    "id": "LUVEI-WTHD-100002"
  },
  "data": {
    "id": "LUVEI-WTHD-100002",
    "status": "sent",
    "amount": "50.00",
    "currency": "USD",
    "usdt_amount": "49.950000",
    "asset": "USDT",
    "network": "TRC20",
    "tx_hash": "9f2b6c1d4e8a7f30b5c2d9e1a6f4b7c08d3e5a2f1b9c6d4e7a0f3b8c5d2e1f6a"
  }
}

التحقق من التوقيع

الترويسةالقيمة
X-Event-Idمطابق لـ event_id.
X-Environmentsandbox أو live. استخدم المفتاح السري الخاص بتلك البيئة.
X-Timestampوقت Unix لمحاولة التسليم هذه.
X-Signatureقيمة HMAC-SHA256 بالنظام الست عشري وبأحرف صغيرة لـ X-Timestamp + "." + raw body.
  1. اقرأ بايتات المتن الخام قبل تحليل JSON.

  2. تحقّق من أن X-Timestamp يتكون من 10 أرقام وأنه ضمن 300 ثانية من توقيت خادمك.

  3. احسب HMAC-SHA256 لـ X-Timestamp + "." + raw body باستخدام المفتاح السري، وقارن الناتج بـ X-Signature باستخدام مقارنة ثابتة الزمن.

  4. إذا فشل التحقق، فارفض الطلب بالرمز 401.

// Node.js 20+: verify a Payniko notification before parsing it.
import {createHmac, timingSafeEqual} from 'node:crypto';

export function verifyWebhook(rawBody, headers, apiSecret, now = Math.floor(Date.now() / 1000)) {
  const timestamp = headers['x-timestamp'];
  const signature = headers['x-signature'];
  if (typeof timestamp !== 'string' || !/^[0-9]{10}$/.test(timestamp) ||
      typeof signature !== 'string' || !/^[0-9a-f]{64}$/.test(signature) ||
      Math.abs(now - Number(timestamp)) > 300) return false;
  const expected = createHmac('sha256', apiSecret).update(timestamp + '.').update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}

// Example with node:http - read the raw bytes first, never re-serialise the JSON.
// import http from 'node:http';
// http.createServer((req, res) => {
//   const chunks = [];
//   req.on('data', (c) => chunks.push(c)).on('end', () => {
//     const raw = Buffer.concat(chunks);
//     const secret = req.headers['x-environment'] === 'live' ? process.env.LIVE_API_SECRET : process.env.SANDBOX_API_SECRET;
//     if (!verifyWebhook(raw, req.headers, secret)) { res.writeHead(401).end(); return; }
//     const event = JSON.parse(raw.toString('utf8'));
//     // Save event.event_id (unique) and the event, then answer quickly.
//     res.writeHead(204).end();
//   });
// }).listen(8080);
<?php
// PHP 8.1+: verify a Payniko notification before parsing it.
function payniko_verify_webhook(string $rawBody, string $timestamp, string $signature, string $apiSecret, ?int $now = null): bool
{
    $now ??= time();
    if (!preg_match('/^[0-9]{10}$/', $timestamp) || !preg_match('/^[0-9a-f]{64}$/', $signature)) return false;
    if (abs($now - (int) $timestamp) > 300) return false;
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $apiSecret);
    return hash_equals($expected, $signature);
}

$raw = file_get_contents('php://input');            // raw bytes, before json_decode
$environment = $_SERVER['HTTP_X_ENVIRONMENT'] ?? '';
$secret = $environment === 'live' ? getenv('LIVE_API_SECRET') : getenv('SANDBOX_API_SECRET');
if (!payniko_verify_webhook($raw, $_SERVER['HTTP_X_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_SIGNATURE'] ?? '', (string) $secret)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true, 32, JSON_THROW_ON_ERROR);
// Save $event['event_id'] (unique) and the event, then answer quickly.
http_response_code(204);
# Python 3.9+: verify a Payniko notification before parsing it.
import hashlib, hmac, re, time


def verify_webhook(raw_body: bytes, headers, api_secret: str, now=None) -> bool:
    timestamp = headers.get("X-Timestamp", "")
    signature = headers.get("X-Signature", "")
    if not re.fullmatch(r"[0-9]{10}", timestamp) or not re.fullmatch(r"[0-9a-f]{64}", signature):
        return False
    if abs(int(now or time.time()) - int(timestamp)) > 300:
        return False
    expected = hmac.new(api_secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask example:
# @app.post("/webhooks/payments")
# def payniko_webhook():
#     raw = request.get_data()                    # raw bytes, before JSON parsing
#     env = request.headers.get("X-Environment")
#     secret = os.environ["LIVE_API_SECRET" if env == "live" else "SANDBOX_API_SECRET"]
#     if not verify_webhook(raw, request.headers, secret):
#         return "", 401
#     event = json.loads(raw)
#     # Save event["event_id"] (unique) and the event, then answer quickly.
#     return "", 204

معالجة كل حدث مرة واحدة

  1. احفظ event_id في جدول عليه فهرس فريد. وإذا كان موجودًا مسبقًا، فأرسل استجابة 2xx وأوقف المعالجة.

  2. أرسل استجابة 2xx خلال ثوانٍ معدودة، ونفّذ الأعمال المستغرقة للوقت (رسائل البريد الإلكتروني، وتحديثات المخزون) لاحقًا عبر قائمة انتظار.

  3. قد تصل الأحداث متأخرة، أو أكثر من مرة، أو بترتيب مختلف. وقد يُبلَّغ عن الدفعة التي تنتهي بانقضاء المهلة بعد دقيقة أو أكثر. واستند في قراراتك إلى الحالة الواردة في data، أو استعلم عن الكائن مجددًا باستخدام GET.

  4. لا تنفّذ طلب الشراء مطلقًا لمجرد عودة المتصفح إلى success_url.

إعادة المحاولة

  • تُعد أي استجابة 2xx خلال 10 ثوانٍ تقريبًا تسليمًا ناجحًا. ولا تُتبع عمليات إعادة التوجيه، بل تُعد إخفاقًا.
  • يُعاد إرسال عمليات التسليم الفاشلة بعد مُدد تقارب دقيقة واحدة، و5 دقائق، و15 دقيقة، وساعة واحدة، و6 ساعات. ويبقى المتن وX-Event-Id دون تغيير، بينما يُعاد إنشاء X-Timestamp وX-Signature في كل محاولة.
  • بعد المحاولة الأخيرة، تُعيَّن حالة التسليم إلى failed. ولا تُعاد المحاولة في بعض الأخطاء (وجهة بعنوان IP خاص، أو مفتاح توقيع مفقود)، فتُعيَّن حالة التسليم إلى failed فورًا. ويمكن إعادة إرسال عمليات التسليم الفاشلة عبر واجهة API لعمليات التسليم.
  • يجب أن يشير webhook_url الخاص بك، عند تحليل اسم النطاق، إلى عنوان IP عام في كل محاولة.

عمليات تسليم إشعارات Webhook

تُحفظ كل عملية إرسال لإشعار Webhook بوصفها عملية تسليم. استخدم واجهة API هذه لتحديد عمليات التسليم الفاشلة وإعادة إرسالها.

GET/api/webhooks/deliveries

سرد عمليات التسليم والاستعلام عنها

./payniko.sh GET '/api/webhooks/deliveries?status=failed&page=1&per_page=20'
import {api} from './payniko.mjs';

const deliveries = await api('GET', '/api/webhooks/deliveries?status=failed&page=1&per_page=20');
console.log(deliveries);
<?php
require __DIR__ . '/payniko.php';

$deliveries = payniko_api('GET', '/api/webhooks/deliveries?status=failed&page=1&per_page=20');
print_r($deliveries);
from payniko import api

deliveries = api("GET", "/api/webhooks/deliveries?status=failed&page=1&per_page=20")
print(deliveries)

صفِّ النتائج حسب status (pending، delivered، failed)، وقسّمها إلى صفحات باستخدام page وper_page. وتتضمن القائمة عمليات التسليم الخاصة ببيئة مفتاحك. كما يُعيد GET /api/webhooks/deliveries/{id} الحقلين url وpayload.

الحقلالنوعالوصف
idstring · uuidمعرّف عملية التسليم؛ وهو مطابق لـ event_id في إشعار Webhook.
eventstringاسم الحدث، على سبيل المثال payment.succeeded.
statusstringحالة التسليم.
pendingdeliveredfailed
payment_idstring · LUVEI-ABCD-123456 | nullالدفعة المرتبطة، إن وُجدت.
subject_typestring | nullrefund أو dispute أو withdrawal؛ وnull في أحداث الدفع.
subject_idstring | nullمعرّف عملية الاسترداد أو النزاع أو عملية السحب.
attempt_countintegerعدد المحاولات المنفذة حتى الآن.
last_status_codeinteger | nullحالة HTTP التي أعادتها نقطة النهاية لديك في المحاولة الأخيرة.
last_error_codestring | nullسبب فشل المحاولة الأخيرة؛ راجع الجدول أدناه.
last_error_messagestring | nullوصف مقروء لـ last_error_code.
last_errorstring | nullنص خطأ حر، وقد يكون باللغة التركية. استخدم last_error_code بدلًا منه.
next_attempt_atstring · date-time | nullموعد المحاولة التلقائية التالية.
delivered_atstring · date-time | nullوقت قبول نقطة النهاية لديك لعملية التسليم.
created_atstring · date-timeوقت إنشاء إشعار Webhook.
urlstringفي استجابة التفاصيل فقط: الرابط المستهدف.
payloadobject | nullفي استجابة التفاصيل فقط: متن إشعار Webhook المخزّن بعد إعادة ترميزه (تظهر مراجع الدفع بصيغة payment_id). ولا يطابق البايتات الموقّعة؛ فلا تتحقق من التواقيع بالاستناد إليه.

الاستجابة · مثال

{
  "id": "0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70",
  "event": "payment.succeeded",
  "status": "failed",
  "payment_id": "LUVEI-PAYN-123456",
  "subject_type": null,
  "subject_id": null,
  "attempt_count": 6,
  "last_status_code": 500,
  "last_error": "Bildirim adresi başarısız yanıt verdi.",
  "last_error_code": "webhook_response_unsuccessful",
  "last_error_message": "Webhook delivery failed.",
  "next_attempt_at": null,
  "delivered_at": null,
  "created_at": "2026-11-01T12:02:00.000Z"
}
الرمزالمعنى
webhook_response_unsuccessfulردّت نقطة النهاية لديك بحالة غير 2xx (راجع last_status_code) أو بإعادة توجيه.
webhook_unreachableتعذّر الوصول إلى نقطة النهاية لديك (DNS أو الاتصال أو انتهاء المهلة).
invalid_webhook_targetالرابط المخزّن ليس رابط Webhook صالحًا.
webhook_target_private_networkأشار الرابط إلى عنوان IP خاص أو محلي.
webhook_environment_invalidتعذّر تحديد بيئة إشعار Webhook.
webhook_signing_unavailableتعذّر توقيع إشعار Webhook.
webhook_egress_proxy_unreachableمشكلة تسليم مؤقتة من جانب Payniko.
webhook_delivery_failedأي خطأ آخر في التسليم.
POST/api/webhooks/deliveries/{id}/retryيتطلب Idempotency-Key

إعادة إرسال عملية تسليم فاشلة

KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
./payniko.sh POST '/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry' '' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const delivery = await api('POST', '/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry', undefined, key);
console.log(delivery);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$delivery = payniko_api('POST', '/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry', null, $key);
print_r($delivery);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
delivery = api("POST", "/api/webhooks/deliveries/0a6f3c9e-2b7d-4e15-9c84-6d1f2a3b5e70/retry", None, key)
print(delivery)

الاستجابة · 200

يُعيد عملية التسليم مع status: pending.

  • لا يمكن إعادة إرسال سوى عمليات التسليم في الحالة failed؛ وتُعيد أي عملية تسليم أخرى 409 9000025.
  • يُدرج إشعار Webhook نفسه في قائمة الانتظار مجددًا بالقيمة نفسها لكل من id وevent_id وبالمتن نفسه، وتبدأ سلسلة جديدة من المحاولات.
  • أرسل متنًا فارغًا أو {}.

الأخطاء المحتملة

البيئة التجريبية والبطاقات التجريبية

اختبر التكامل بالمفتاح التجريبي قبل الانتقال إلى البيئة الحية. وتوفر البيئة التجريبية واجهة API وإشعارات Webhook نفسها المتاحة في البيئة الحية، دون أموال حقيقية أو معالجة بنكية فعلية.

آلية عمل البيئة التجريبية

  • لا يوجد خيار للتبديل بين البيئات: تُنفَّذ الطلبات الموقّعة بالمفتاح التجريبي في البيئة التجريبية، والطلبات الموقّعة بالمفتاح الحي في البيئة الحية.
  • تعمل البطاقات أدناه على صفحة الدفع وفي نموذج الدفع الخاص بك على حد سواء؛ وتُرفض البطاقات الحقيقية. كما يقبل نموذج الدفع الخاص بك أي رقم بطاقة صالح آخر يحمل BIN 435509 أو 400000؛ وتتيح لك صفحة تجريبية حينئذٍ اختيار النجاح أو الفشل أو عدم الاستجابة.
  • شاشات 3-D Secure شاشات تجريبية، ويجب أن تتضمن أرقام البطاقات رقم تحقق صالحًا.
  • تُسلَّم إشعارات Webhook بوصفها طلبات HTTPS حقيقية إلى webhook_url الخاص بك، وتُوقَّع بالمفتاح السري للبيئة التجريبية.

البطاقات التجريبية

تتصرف البطاقات بالطريقة نفسها مع Hosted API وDirect API ما لم يذكر الجدول خلاف ذلك. استخدم أي تاريخ انتهاء مستقبلي وأي CVV من 3 أرقام.

رقم البطاقةالنتيجة
4000000000000010موافقة دون 3-D Secure. وفي Direct API، تُعيد استجابة الإنشاء الحالة succeeded مباشرةً.
4000000000000002 · 4355090000044242خطوة 3-D Secure تجريبية، تليها الحالة succeeded.
4000000000000051 · 4355090000040000مرفوضة قبل 3-D Secure (do_not_honor). Hosted API: قد تبقى الدفعة في requires_action نحو 5 دقائق وترسل payment.updated قبل أن تنتهي بـ failed. Direct API: تعيد استجابة الإنشاء failed مباشرة.
4000000000000093 · 4355090000005151خطوة 3-D Secure تجريبية، تليها الحالة failed مع insufficient_funds.
4000000000000044لا استجابة من البنك ولا 3-D Secure؛ وتبقى الدفعة في الحالة processing.
4355090000089999Hosted API: processing بدون خطوة 3-D Secure. Direct API: خطوة 3-D Secure تجريبية ثم processing.
  • مع أي بطاقة تجريبية، يُبقي المبلغ المنتهي بـ .77 (على سبيل المثال 100.77) الدفعة في الحالة processing (مراجعة يدوية).
  • تُغلق الدفعة التجريبية التي لا تتلقى أي استجابة في نهاية المطاف بالحالة failed مع service_unavailable.
  • قد تبقى الدفعة المنفذة ببطاقة تجريبية مرفوضة (على سبيل المثال 4000000000000051) في الحالة requires_action لبعض الوقت قبل وصولها إلى حالة نهائية. واستند في قراراتك إلى إشعار Webhook أو طلب GET، لا إلى رفض مؤقت.

أرقام BIN التجريبية

في البيئة التجريبية، يُعيد فحص BIN النتائج التالية:

BINالنتيجة
435509 · 400000200 مع payment_mode: direct.
540061200 مع payment_mode: hosted؛ ويُكمل العميل الدفع على صفحة الدفع في البيئة التجريبية.
979210409 مع الرمز 0055230.
أي ستة أرقام أخرى409 مع الرمز 0055233.

الاسترداد والسحب

في sandbox، تكتمل عمليات الاسترداد والسحب فورًا مقابل رصيد تجريبي. اختر النتيجة بترويسة X-Sandbox-Scenario، وهي ليست جزءًا من التوقيع. استخدم Idempotency-Key جديدًا لكل سيناريو: فالترويسة جزء من بصمة idempotency، والمفتاح نفسه مع سيناريو مختلف يُرفض بـ 409 9000010. القيمة غير المعروفة، أو الترويسة المرسلة مع مفتاح حي، تعيد 422 1000001؛ ونقطة نهاية الأدلة لا تقبل الترويسة. يُعاد السيناريو المطبّق في ترويسة الاستجابة X-Sandbox-Scenario.

قبل تطبيق السيناريو، يفرض sandbox القواعد نفسها المطبقة في البيئة الحية: الحد الأدنى والحد اليومي للسحب، وأسعار الصرف (503 3000001 عند عدم توفرها)، والتحقق من العنوان. يُقبل في sandbox أي عنوان TRC20 ذي مجموع تحقق صالح، بما في ذلك عنوان TRON الصفري، ولا يُجرى أي تحويل؛ ولا يمكن إعادة إنتاج 1000030 (عنوان لا يمكنه استلام المدفوعات) في sandbox.

نقطة النهايةالقيمة
POST /api/account/refundscompleted (الافتراضية) أو rejected
POST /api/account/withdrawalssent (الافتراضية) أو rejected
KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
SANDBOX_SCENARIO=rejected ./payniko.sh POST '/api/account/refunds' '{
  "payment_id": "LUVEI-PAYN-123456",
  "operation_type": "refund",
  "reason": "Test refund"
}' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const refund = await api('POST', '/api/account/refunds', {
  "payment_id": "LUVEI-PAYN-123456",
  "operation_type": "refund",
  "reason": "Test refund"
}, key, {'X-Sandbox-Scenario': 'rejected'});
console.log(refund);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$refund = payniko_api('POST', '/api/account/refunds', [
    "payment_id" => 'LUVEI-PAYN-123456',
    "operation_type" => 'refund',
    "reason" => 'Test refund',
], $key, ['X-Sandbox-Scenario: rejected']);
print_r($refund);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
refund = api("POST", "/api/account/refunds", {
    "payment_id": "LUVEI-PAYN-123456",
    "operation_type": "refund",
    "reason": "Test refund"
}, key, {"X-Sandbox-Scenario": "rejected"})
print(refund)

قيمة tx_hash في البيئة التجريبية قيمة محاكاة، ولا تشير إلى معاملة حقيقية على سلسلة الكتل.

POST/api/sandbox/disputesيتطلب Idempotency-Keyبالمفتاح التجريبي فقط

إنشاء نزاع تجريبي

يفتح نزاعًا على دفعة ناجحة في البيئة التجريبية لتتمكن من اختبار معالجتك للنزاعات وإشعارات Webhook. ويتاح بالمفتاح التجريبي فقط؛ ويتلقى المفتاح الحي 409 9000013.

الحقلالنوعإلزاميالوصف
payment_idstring · LUVEI-ABCD-123456إلزاميإحدى دفعاتك في البيئة التجريبية بالحالة succeeded.
reasonstringإلزاميسبب النزاع.
fraudunauthorizedproduct_not_deliveredduplicateamount_mismatchother
scenariostringاختياريالحالة الأولية. القيمة الافتراضية: open.
openwonlost
KEY="$(openssl rand -hex 16)"  # store with the order before sending; reuse it after network errors
./payniko.sh POST '/api/sandbox/disputes' '{
  "payment_id": "LUVEI-PAYN-123456",
  "reason": "product_not_delivered",
  "scenario": "open"
}' "$KEY"
import {api, randomUUID} from './payniko.mjs';

const key = randomUUID(); // Store this key with the order before sending; reuse it after network errors.
const dispute = await api('POST', '/api/sandbox/disputes', {
  "payment_id": "LUVEI-PAYN-123456",
  "reason": "product_not_delivered",
  "scenario": "open"
}, key);
console.log(dispute);
<?php
require __DIR__ . '/payniko.php';

$key = bin2hex(random_bytes(16)); // store with the order; reuse it after network errors
$dispute = payniko_api('POST', '/api/sandbox/disputes', [
    "payment_id" => 'LUVEI-PAYN-123456',
    "reason" => 'product_not_delivered',
    "scenario" => 'open',
], $key);
print_r($dispute);
from payniko import api
import uuid

key = str(uuid.uuid4())  # store with the order; reuse it after network errors
dispute = api("POST", "/api/sandbox/disputes", {
    "payment_id": "LUVEI-PAYN-123456",
    "reason": "product_not_delivered",
    "scenario": "open"
}, key)
print(dispute)

الاستجابة · 201

يُعيد كائن النزاع. ويقبل النزاع التجريبي في الحالة open الردود لمدة 7 أيام؛ أما won وlost فتكونان نهائيتين فورًا. ولا تحدث الحالة under_review في البيئة التجريبية، ولا تؤثر النزاعات التجريبية في رصيدك.

سجل الطلبات

تعرض صفحة سجل الطلبات أحدث طلبات API الخاصة بك مع الحالة ورمز الخطأ وcorrelation_id. ولا تُدرج الطلبات المرفوضة قبل تحديد المفتاح (مفتاح أو توقيع أو رمز تاجر غير صالح). وتُدرج دفعات البيئة التجريبية ضمن طلبات API التجريبية.

رموز الأخطاء

تُعاد الأخطاء بالصيغة application/problem+json. واستند في منطق تطبيقك إلى حالة HTTP والحقل code المكوّن من 7 أرقام؛ أما title وdetail فهما موجّهان للقارئ البشري.

صيغة استجابة الخطأ

الحقلالوصف
typeرابط نوع الخطأ: https://payniko.com/problems/{code}.
titleملخص موجز للخطأ.
statusرمز حالة HTTP.
detailشرح الخطأ في هذا الطلب.
codeرمز ثابت مكوّن من 7 أرقام؛ استخدمه في منطق تطبيقك.
correlation_idمطابق للترويسة X-Correlation-Id. أرفقه بطلبات الدعم.
errorsرسائل على مستوى الحقول لأخطاء التحقق من الصحة: { "field": ["message"] }.
integrationإرشادات التكامل: stage، reason، violations (location، path، expected، hint)، docs_url، retryable، وفي بعض الحالات retry_after.
metadataقد يظهر في أخطاء Direct API وأخطاء المصادقة وتجاوز حد معدل الطلبات، ويتضمن retry_class وdeveloper_detail لأغراض إعلامية. وتعامل معه بوصفه اختياريًا.
limitsنطاق المبلغ المسموح به مع 9000005. ومع 0055235، يكون الحقل المقابل amount_limit.
retry_afterعدد الثواني الواجب انتظارها؛ يظهر مع بعض أخطاء 503.

HTTP 422 · 1000001

{
  "type": "https://payniko.com/problems/1000001",
  "title": "Validation failed",
  "status": 422,
  "detail": "The submitted fields are not valid.",
  "code": "1000001",
  "correlation_id": "9b8c7d6e-5f40-4a3b-8c2d-1e0f9a8b7c6d",
  "errors": {
    "merchant_order_id": [
      "This field is required."
    ]
  },
  "integration": {
    "stage": "validation",
    "reason": "validation_failed",
    "violations": [
      {
        "location": "body",
        "path": "merchant_order_id",
        "expected": {
          "type": "string",
          "min_length": 1,
          "max_length": 64,
          "unique_within": "account_and_environment"
        },
        "hint": "Check this field against the documented format."
      }
    ],
    "docs_url": "/en/developers#errors",
    "retryable": false
  }
}

إرشادات إعادة المحاولة

retry_classالمعنى
no_retryصحّح الطلب قبل إعادة إرساله.
retry_same_requestأعِد إرسال الطلب المطابق بعد مهلة قصيرة (المفتاح نفسه والمتن نفسه).
retry_afterانتظر المدة المحددة في Retry-After، ثم أعِد الإرسال.
read_orderاستعلم عن الدفعة باستخدام GET قبل تحديد الإجراء التالي.
  • يوجد retry_class فقط في الاستجابات التي تحمل metadata (Direct API وأخطاء المصادقة وحدود المعدل). أما الأخطاء الأخرى فتحمل integration.retryable بدلًا من ذلك.
  • بعد انتهاء المهلة أو تلقي 5xx، لا تُنشئ دفعة جديدة مطلقًا؛ بل استعلم عن الدفعة أو أعِد إرسال الطلب بالمفتاح نفسه.
  • رفض البطاقة ليس خطأ HTTP: إذ تُعاد الدفعة مع 200 وstatus: failed وfailure.
  • قد تتغير قيمة developer_detail؛ فلا تبنِ منطق التكامل عليها.

جميع رموز الأخطاء

المصادقة والوصول

الرمزHTTPالمعنىالإجراء الموصى به
0055502401 / 403رُفض الوصول. 401: الترويسة X-Api-Key أو X-Merchant-Code مفقودة، أو إحدى الترويسات مكررة أو غير صحيحة الصيغة، أو المفتاح غير معروف أو مُلغى، أو رمز التاجر غير مطابق، أو أُرسلت X-Environment بقيمة لا تطابق المفتاح. 403: الحساب أو صلاحية الوصول إلى Direct API غير مفعّلة.تحقّق من الترويسات ومن مفتاح API. لا تُعِد المحاولة تلقائيًا.
0055504401الترويسة X-Timestamp أو X-Signature مفقودة أو غير صحيحة الصيغة، أو أن ساعة خادمك تختلف عن توقيت خوادم Payniko بأكثر من 300 ثانية.اضبط ساعة خادمك (NTP)، ثم وقّع الطلب من جديد بطابع زمني حالي.
0055505401التوقيع لا يطابق الطلب.احسب قيمة التجزئة على بايتات المتن التي ترسلها تمامًا، ووقّع المسار دون سلسلة الاستعلام، واستخدم المفتاح السري الصحيح.
9000002401انتهت صلاحية الطلب قبل معالجته.وقّع الطلب من جديد بطابع زمني حالي، ثم أعِد إرساله.
9000040403Hosted API غير مفعّل لحسابك.تواصل مع فريق الدعم.
1000004403استقبال المدفوعات الجديدة متوقف مؤقتًا.أعِد المحاولة لاحقًا.
9000004403حسابك مقيّد ولا يتيح تنفيذ هذه العملية.تواصل مع فريق الدعم.

منع التكرار والتعارضات

الرمزHTTPالمعنىالإجراء الموصى به
1000009422الترويسة Idempotency-Key مفقودة أو غير صالحة.أرسل مع كل طلب POST مفتاحًا يتكون من 8 إلى 128 حرفًا من A-Z a-z 0-9 _ -.
9000010409سبق استخدام Idempotency-Key هذا لطلب مختلف، أو أُعيد إرسال الطلب الموقّع نفسه بمفتاح مختلف.أعِد المحاولة بالمفتاح نفسه والمتن نفسه؛ ولا تستخدم مفتاحًا جديدًا إلا لعملية جديدة.
9000039409الطلب الأول بهذا الـ Idempotency-Key لا يزال قيد المعالجة (Hosted API).أعد إرسال الطلب نفسه بعد انتظار قصير.
9000007409توجد دفعة مسبقًا لقيمة merchant_order_id هذه (Hosted API).استعلم عن الدفعة القائمة أو استخدم مرجعًا جديدًا لطلب الشراء.
0055503409تعارض في Direct API: توجد دفعة مسبقًا لقيمة merchant_order_id هذه، أو استُخدم Idempotency-Key مع متن مختلف أو لدفعة عبر Hosted API، أو أن الطلب الأصلي لا يزال قيد التنفيذ.استعلم عن الدفعة باستخدام GET. ولا تُعِد إرسال سوى طلب مطابق تمامًا.

التحقق من صحة البيانات

الرمزHTTPالمعنىالإجراء الموصى به
1000001422الطلب غير صالح: حقل غير معروف، أو صيغة غير صالحة، أو سلسلة استعلام في طلب POST، أو معلمة استعلام غير مدعومة، أو متن ليس كائن JSON. ويتضمن errors الحقول المعنية.صحّح الحقول المذكورة.
1000020422صيغة المبلغ غير صالحة.أرسل المبلغ نصًا موجبًا بمنزلتين عشريتين ودون أصفار بادئة، على سبيل المثال "100.00".
1000021422العملة غير مدعومة.استخدم USD أو EUR أو AED أو TRY، أو احذف الحقل currency.
1000023422المبلغ كبير جدًا.أرسل مبلغًا أصغر.
9000008422قيمة webhook_url غير صالحة.استخدم رابط HTTPS عامًا على المنفذ 443 لا يتجاوز طوله 500 حرف.
9000038422قيمة success_url أو fail_url غير صالحة.استخدم رابط HTTPS لا يتجاوز طوله 2048 حرفًا. وتقبل البيئة التجريبية أيضًا http://localhost.

المبلغ والبطاقة وBIN

الرمزHTTPالمعنىالإجراء الموصى به
9000005422المبلغ خارج حدود حسابك (Hosted API). ويتضمن limits النطاق المسموح به.عدّل المبلغ.
0055235422المبلغ غير مقبول (Direct API وفحص BIN). ويتضمن amount_limit القيمتين min وmax عند توفرهما.عدّل المبلغ.
0055210422رقم BIN لا يتكون من ستة أرقام.أرسل الأرقام الستة الأولى من رقم البطاقة.
0055233409رقم BIN غير معروف.اطلب من العميل استخدام بطاقة أخرى. وهذا ليس رفضًا من البنك.
0055230409لا يمكن استخدام هذه البطاقة لهذه الدفعة.اطلب من العميل استخدام بطاقة أخرى. وهذا ليس رفضًا من البنك.
0055231409هذه البطاقة غير مقبولة لهذه الدفعة.اطلب من العميل استخدام بطاقة أخرى.
0055221409تم بلوغ أحد حدود هذه البطاقة.استخدم بطاقة أخرى أو أعِد المحاولة لاحقًا.
0055234409بيانات البطاقة غير صالحة: رقم البطاقة أو تاريخ الانتهاء أو CVV. يحدد metadata.developer_detail الحقل: card_number_invalid أو card_expiry_invalid أو card_cvv_invalid.اطلب من العميل إعادة إدخال بيانات البطاقة.
0055001409حسابك غير جاهز لهذه الدفعة. مع مفتاح sandbox، يعني هذا أيضًا أن محاكاة sandbox غير متاحة مؤقتًا (developer_detail: sandbox_unavailable)، وهو مكافئ لـ 9000013.تواصل مع فريق الدعم.
0055002403مدفوعات البطاقات غير مفعّلة لحسابك.تواصل مع فريق الدعم.
9000013409عملية البيئة التجريبية غير متاحة، على سبيل المثال عند استدعاء نقطة نهاية مخصصة للبيئة التجريبية بالمفتاح الحي.استخدم المفتاح التجريبي أو أعِد المحاولة لاحقًا.

غير موجود

الرمزHTTPالمعنىالإجراء الموصى به
9000009404الدفعة غير موجودة في هذا الحساب وهذه البيئة، أو لا يمكن استخدامها لهذه العملية.تحقّق من payment_id واستخدم مفتاحًا من البيئة نفسها.
1000002404غير موجود: المسار غير معروف، أو منتج API هذا غير مفعّل على المنصة. أما API المعطّل لحسابك فيعيد 403 بدلًا من ذلك (9000040 في Hosted API و0055502 في Direct API).تحقق من المسار؛ وتواصل مع الدعم إذا كان ينبغي أن يكون API متاحًا.
1000007405طريقة HTTP غير مسموح بها. وتتضمن الترويسة Allow الطرق الصالحة.استخدم الطريقة الموثّقة.

الحدود والتوفر

الرمزHTTPالمعنىالإجراء الموصى به
1000005429عدد الطلبات كبير جدًا، أو حظر مؤقت بعد تكرار إخفاق التوقيع.انتظر المدة المحددة في Retry-After، ثم أعِد المحاولة مع زيادة فترات الانتظار تدريجيًا.
1000011503الخدمة غير متاحة مؤقتًا.أعِد إرسال الطلب نفسه. واستعلم عن الموارد القائمة قبل إنشاء موارد جديدة.
1000013503هذه الميزة غير متاحة بعد.أعِد المحاولة بعد مرور retry_after ثانية.
1000014503تجري حاليًا صيانة مجدولة. ويتضمن until وnote التفاصيل.أعِد المحاولة بعد انقضاء المدة المحددة في Retry-After.
3000001503سعر الصرف غير متاح مؤقتًا.أعِد المحاولة لاحقًا.
1000006500حدث خطأ غير متوقع في الخادم.أعِد إرسال الطلب نفسه أو استعلم عن المورد.

عمليات الحساب

الرمزHTTPالمعنىالإجراء الموصى به
9000014409لهذه الدفعة عملية استرداد أو إبطال مفتوحة أو مكتملة، أو نزاع لم يُحسم لصالحك.استعلم عن عملية الاسترداد أو عن النزاع.
9000015404النزاع غير موجود.تحقّق من معرّف النزاع.
9000016409لم يعد النزاع يقبل الرد (أُغلق أو انقضى evidence_due_at).استعلم عن النزاع.
9000017422عنوان TRC20 غير صالح.تحقّق من العنوان مع صاحبه.
1000030422لا يمكن لهذا العنوان استلام عمليات السحب.استخدم عنوانًا آخر.
9000018422قيمة address_acknowledged ليست true.أكّد العنوان، ثم أرسل true.
9000019422مبلغ السحب غير صالح أو أقل من الحد الأدنى.عدّل المبلغ.
9000020409الرصيد المتاح غير كافٍ.راجِع الرصيد المتاح.
9000021409تم بلوغ حد السحب اليومي.أعِد المحاولة في اليوم التالي أو اطلب مبلغًا أصغر.
9000022409تجري حاليًا معالجة عملية سحب أخرى.انتظر، ثم أعِد إرسال الطلب نفسه.
9000023404عملية السحب غير موجودة.تحقّق من معرّف عملية السحب.
9000024404عملية تسليم إشعار Webhook غير موجودة.تحقّق من معرّف عملية التسليم.
9000025409لا يمكن إعادة إرسال سوى عمليات التسليم الفاشلة.راجِع حالة عملية التسليم.

البيئة التجريبية فقط

الرمزHTTPالمعنىالإجراء الموصى به
9000026503البيئة التجريبية غير متاحة لهذه العملية بعد.أعِد المحاولة بعد انقضاء المدة المحددة في Retry-After.
9000027409الرصيد التجريبي قيد الاستخدام من قِبل عملية أخرى.أعِد إرسال الطلب نفسه بعد مهلة قصيرة.
9000030409تعذّرت إضافة المبلغ إلى الرصيد التجريبي.أعِد المحاولة لاحقًا.
9000033409الرصيد التجريبي غير كافٍ لهذا الاسترداد. أما السحب في sandbox برصيد غير كافٍ فيعيد 9000020 بدلًا من ذلك.أنشئ دفعات تجريبية إضافية أولًا.

لغة الرسائل

يتبع title وdetail قيمة Accept-Language (en، tr، de، ar، es، el؛ وتُعامَل de-AT على أنها de). وتُعيد أي قيمة أخرى اللغة الإنجليزية. ويبيّن Content-Language اللغة المستخدمة.

قائمة التحقق قبل الانتقال إلى البيئة الحية

أكمل قائمة التحقق هذه قبل قبول مدفوعات حقيقية.

  • في البيئة التجريبية، انتهت دفعة بالحالة succeeded وأخرى بالحالة failed، وحُدّثت حالات طلبات الشراء لديك وفقًا لذلك.

  • تتحقق نقطة نهاية Webhook لديك من التوقيع على المتن الخام، وترفض الطوابع الزمنية القديمة، وتحفظ كل event_id مرة واحدة، وترسل استجابة 2xx دون تأخير.

  • لا تنفّذ طلبات الشراء إلا بعد أن يُظهر إشعار Webhook تم التحقق منه أو طلب GET الحالة succeeded، ولا تنفّذها مطلقًا بناءً على عودة المتصفح.

  • يحفظ كل طلب POST قيمة Idempotency-Key الخاصة به قبل الإرسال، ويُعيد استخدامها بعد أخطاء الشبكة.

  • تتعامل شيفرتك مع requires_action وprocessing و429 و5xx دون إنشاء دفعات مكررة.

  • يُحفظ المفتاح الحي ومفتاحه السري في مخزن الأسرار لديك فقط. واستبدال المفتاح التجريبي بالمفتاح الحي هو التغيير الوحيد في الشيفرة.

  • تشير webhook_url وsuccess_url وfail_url إلى عناوين HTTPS الخاصة ببيئة الإنتاج لديك.

  • تسجّل X-Correlation-Id لكل طلب.

حتى إذا كانت المفاتيح جاهزة، تحقق من التكامل بهذه القائمة قبل قبول دفعات حقيقية. إكمال الاختبار ليس شرطًا لإنشاء المفاتيح.