للمطورين

Direct API (v4)

اجمع بيانات البطاقة في نموذج الدفع الخاص بك وأرسلها من خادمك إلى Manuell. يؤكد العميل الدفع عبر 3-D Secure وتستلم النتيجة عبر webhook.

  • عنوان URL الأساسيhttps://www.manuellsolutions.com/api/v4
  • إصدار الواجهةv4

ابدأ بنظرة عامة

البيئة

اختبر دون تحريك أموال حقيقية.

مدفوعات حقيقية. يلزم مفتاح الإنتاج.

نظرة عامة

يتحدث خادمك مع Manuell مباشرة: يسأل عن المسار الذي تحتاجه البطاقة، وينشئ الطلب ببيانات البطاقة، ثم يتابعه حتى يُدفع أو يفشل.

عنوان URL الأساسي
https://www.manuellsolutions.com/api/v4
العملة
الليرة التركية (TRY) فقط. دفعة واحدة لكل طلب، دون أقساط.
المبالغ
سلاسل نصية بخانتين عشريتين، مثل "1500.00".
البيئة
sandbox أو live في الترويسة X-CEK-Environment. يجب أن تطابق بيئة مفتاحك.
الأخطاء
application/problem+json (RFC 7807) مع code من 7 أرقام.
حد المعدل
300 طلبًا في الدقيقة لكل مفتاح (و600 في الدقيقة لكل عنوان IP قبل المصادقة). عند التجاوز: 429 مع Retry-After.
التكرار
لا يوجد Idempotency-Key. يحمي merchant_order_id من تكرار الطلبات.
  1. استعلم عن BIN

    أرسل أول 6 أرقام من البطاقة والمبلغ إلى GET /api/v4/bins/{bin}. يخبرك payment_mode هل تجمع البطاقة بنفسك (direct) أم توجّه العميل إلى صفحة مستضافة (hosted).

  2. أنشئ الطلب

    أرسل POST /api/v4/orders مع البطاقة (direct) أو مع BIN فقط (hosted). يعود الطلب بالحالة requires_action أو requires_hosted أو pending أو failed.

  3. أكمل وأكّد

    وجّه العميل إلى redirect_url (3-D Secure) أو hosted_url. تصل النتيجة النهائية عبر webhook؛ وإن تأخرت فاقرأ الطلب عبر GET /api/v4/orders/{merchantOrderId}.

الإعداد في اللوحة

  • أنشئ مفتاح API ومفتاحًا سريًا لكل بيئة من “API Key والتكامل”. احتفظ بالمفتاح السري على خادمك.
  • أضف عناوين IP العامة لخوادمك إلى قائمة IP المسموح بها للبيئة. ما دامت القائمة فارغة يُرفض كل استدعاء بـ 403 والرمز 0055501.
  • حدّد عنوان webhook بصيغة HTTPS. يجب أن يكون مضيفه عامًا.
  • حدّد عنواني العودة للنجاح والفشل (HTTPS في الإنتاج). وبدونهما يبقى العميل على شاشة النتيجة بعد الدفع.
  • رمز التاجر هو MRC- يليه رقم حسابك المكوّن من 6 أرقام، مثل MRC-123456.

المصادقة

يحمل كل استدعاء ترويسات X-CEK-*. التوقيع هو HMAC-SHA256 لنص يجمع الطابع الزمني والطريقة والمسار وتجزئة الجسم.

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

الترويسةالوصف
X-CEK-Api-Keyمفتاح API الخاص بالبيئة.
X-CEK-Environmentsandbox أو live. يجب أن تطابق بيئة المفتاح.
X-CEK-Merchant-Codeرمز التاجر، مثل MRC-123456. إلزامي في sandbox؛ وفي الإنتاج إن أرسلته فيجب أن يطابق حسابك.
X-CEK-Timestampوقت Unix الحالي بالثواني (10 أرقام). يُرفض أي فرق يتجاوز 300 ثانية.
X-CEK-Signature64 خانة hex بأحرف صغيرة، تُحسب كما هو موضح أدناه.
Content-Typeapplication/json في طلبات POST.

كيفية توقيع الطلب

  1. خذ الجسم تمامًا كما سترسله واحسب sha256(body) بصيغة hex بأحرف صغيرة. في الطلب بلا جسم استخدم تجزئة النص الفارغ.

  2. اجمع الطابع الزمني والطريقة بأحرف كبيرة والمسار وتجزئة الجسم مفصولةً بأسطر جديدة (\n). المسار هو المسار الكامل دون المضيف وسلسلة الاستعلام، مثل /api/v4/orders.

  3. احسب HMAC-SHA256 لهذا النص باستخدام مفتاح API السري مفتاحًا، واكتبه بـ 64 خانة hex بأحرف صغيرة.

  4. أرسله بوصفه X-CEK-Signature مع بقية الترويسات.

نص التوقيع القانوني

string_to_sign = X-CEK-Timestamp + "\n" + METHOD + "\n" + path + "\n" + hex(sha256(body))
X-CEK-Signature = hex(HMAC-SHA256(key = API_SECRET, message = string_to_sign))  // 64 lowercase hex characters

وقّع البايتات نفسها التي ترسلها: حوّل JSON إلى نص مرة واحدة واستخدم النص نفسه للتجزئة (hash) ولجسم الطلب.

مثال (ببيانات وهمية)

جميع القيم وهمية: المفتاح السري sk_test_EXAMPLE والطابع الزمني 1790000000. استخدم القيم الموجودة في لوحتك.

جسم الطلب

{"merchant_order_id":"ORDER-1001","amount":"1500.00","bin":"540061"}

sha256(body)

3bb9ee51caacfb41bd7a8774f12ff861f97b1fd8cf416a00b5fa92618e8390e7

string_to_sign

1790000000
POST
/api/v4/orders
3bb9ee51caacfb41bd7a8774f12ff861f97b1fd8cf416a00b5fa92618e8390e7

X-CEK-Signature

f721cbcccb6ed8a249ed0e331133686fe5f9dfb227856bbd1b6ff9e0b2a8f348

أمثلة برمجية

يستعلم كل مثال عن BIN وينشئ طلبًا في sandbox. تُقرأ بيانات الاعتماد من متغيرات البيئة؛ لا تضع المفتاح السري في الشيفرة. يستخدم كل تشغيل merchant_order_id جديدًا.

#!/usr/bin/env bash
# export MERCHANT_API_KEY=... MERCHANT_API_SECRET=... MERCHANT_CODE=MRC-123456   (sandbox key)
set -euo pipefail

BASE_URL="https://www.manuellsolutions.com"

signed() {  # signed METHOD PATH [BODY]
  local method="$1" path="$2" body="${3:-}" timestamp sha string_to_sign signature
  timestamp="$(date +%s)"
  sha="$(printf '%s' "$body" | openssl dgst -sha256 -hex | awk '{print $NF}')"
  string_to_sign="$(printf '%s\n%s\n%s\n%s' "$timestamp" "$method" "${path%%\?*}" "$sha")"
  # the secret travels in the environment, not on the command line
  signature="$(printf '%s' "$string_to_sign" | python3 -c 'import hashlib,hmac,os,sys; print(hmac.new(os.environ["MERCHANT_API_SECRET"].encode(), sys.stdin.buffer.read(), hashlib.sha256).hexdigest())')"
  local args=(--silent --show-error --request "$method" "$BASE_URL$path"
    --header "X-CEK-Api-Key: $MERCHANT_API_KEY"
    --header "X-CEK-Environment: sandbox"
    --header "X-CEK-Merchant-Code: $MERCHANT_CODE"
    --header "X-CEK-Timestamp: $timestamp"
    --header "X-CEK-Signature: $signature")
  if [ -n "$body" ]; then args+=(--header "Content-Type: application/json" --data-binary "$body"); fi
  curl "${args[@]}"
  echo
}

# 1) BIN lookup: no body, so the hash is sha256("")
signed GET "/api/v4/bins/435509?amount=1500.00"

# 2) create an order; a new merchant_order_id for every order
ORDER_ID="ORDER-$(date +%s)-$(openssl rand -hex 4)"
signed POST "/api/v4/orders" "{\"merchant_order_id\":\"$ORDER_ID\",\"amount\":\"1500.00\",\"card\":{\"holder_name\":\"TEST CUSTOMER\",\"number\":\"4355090000044242\",\"expiry_month\":\"12\",\"expiry_year\":\"30\",\"cvv\":\"123\"}}"
<?php
$config = [
    'base'   => 'https://www.manuellsolutions.com',
    'key'    => getenv('MERCHANT_API_KEY'),
    'secret' => getenv('MERCHANT_API_SECRET'),
    'code'   => getenv('MERCHANT_CODE'),
    'env'    => 'sandbox',
];

function signedRequest(array $config, string $method, string $path, ?array $payload = null): array
{
    $method = strtoupper($method);
    $signingPath = explode('?', $path, 2)[0];
    // serialise once: the same bytes are hashed and sent
    $body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $timestamp = (string) time();
    $stringToSign = implode("\n", [$timestamp, $method, $signingPath, hash('sha256', $body)]);
    $signature = hash_hmac('sha256', $stringToSign, $config['secret']);

    $headers = [
        'X-CEK-Api-Key: ' . $config['key'],
        'X-CEK-Environment: ' . $config['env'],
        'X-CEK-Merchant-Code: ' . $config['code'],
        'X-CEK-Timestamp: ' . $timestamp,
        'X-CEK-Signature: ' . $signature,
    ];
    if ($body !== '') {
        $headers[] = 'Content-Type: application/json';
    }

    $ch = curl_init($config['base'] . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 35, // an order can take up to 30 seconds
    ]);
    if ($body !== '') {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($response === false) {
        throw new RuntimeException('No response; repeat the same request or read the order with GET.');
    }

    return [$status, json_decode((string) $response, true)];
}

// 1) BIN lookup: no body, so the hash is sha256("")
[$status, $bin] = signedRequest($config, 'GET', '/api/v4/bins/435509?amount=1500.00');
echo $status, ' ', $bin['data']['payment_mode'] ?? '', PHP_EOL;

// 2) create an order; a new merchant_order_id for every order
[$status, $order] = signedRequest($config, 'POST', '/api/v4/orders', [
    'merchant_order_id' => 'ORDER-' . time() . '-' . bin2hex(random_bytes(4)),
    'amount' => '1500.00',
    'card' => ['holder_name' => 'TEST CUSTOMER', 'number' => '4355090000044242', 'expiry_month' => '12', 'expiry_year' => '30', 'cvv' => '123'],
]);
echo $status, ' ', $order['data']['status'] ?? '', PHP_EOL;
const crypto = require("node:crypto");

const BASE_URL = "https://www.manuellsolutions.com";
const { MERCHANT_API_KEY, MERCHANT_API_SECRET, MERCHANT_CODE } = process.env;

async function signedRequest(method, path, payload) {
  method = method.toUpperCase();
  const signingPath = path.split("?", 1)[0];
  // serialise once: the same bytes are hashed and sent
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const timestamp = String(Math.floor(Date.now() / 1000));
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const stringToSign = [timestamp, method, signingPath, bodyHash].join("\n");
  const signature = crypto.createHmac("sha256", MERCHANT_API_SECRET).update(stringToSign).digest("hex");

  const headers = {
    "X-CEK-Api-Key": MERCHANT_API_KEY,
    "X-CEK-Environment": "sandbox",
    "X-CEK-Merchant-Code": MERCHANT_CODE,
    "X-CEK-Timestamp": timestamp,
    "X-CEK-Signature": signature,
  };
  if (body !== "") headers["Content-Type"] = "application/json";

  // an order can take up to 30 seconds
  const res = await fetch(BASE_URL + path, { method, headers, body: body === "" ? undefined : body, signal: AbortSignal.timeout(35000) });
  return { status: res.status, body: JSON.parse(await res.text()) };
}

(async () => {
  // 1) BIN lookup: no body, so the hash is sha256("")
  const bin = await signedRequest("GET", "/api/v4/bins/435509?amount=1500.00");
  console.log(bin.status, bin.body.data?.payment_mode);

  // 2) create an order; a new merchant_order_id for every order
  const order = await signedRequest("POST", "/api/v4/orders", {
    merchant_order_id: `ORDER-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`,
    amount: "1500.00",
    card: { holder_name: "TEST CUSTOMER", number: "4355090000044242", expiry_month: "12", expiry_year: "30", cvv: "123" },
  });
  console.log(order.status, order.body.data?.status);
})();
import hashlib
import hmac
import json
import os
import time
import urllib.error
import urllib.request
import uuid

BASE_URL = "https://www.manuellsolutions.com"
API_KEY = os.environ["MERCHANT_API_KEY"]
API_SECRET = os.environ["MERCHANT_API_SECRET"]
MERCHANT_CODE = os.environ["MERCHANT_CODE"]


def signed_request(method, path, payload=None):
    method = method.upper()
    signing_path = path.split("?", 1)[0]
    # serialise once: the same bytes are hashed and sent
    body = json.dumps(payload, separators=(",", ":")) if payload is not None else ""
    timestamp = str(int(time.time()))
    string_to_sign = "\n".join([timestamp, method, signing_path, hashlib.sha256(body.encode()).hexdigest()])
    signature = hmac.new(API_SECRET.encode(), string_to_sign.encode(), hashlib.sha256).hexdigest()

    headers = {
        "X-CEK-Api-Key": API_KEY,
        "X-CEK-Environment": "sandbox",
        "X-CEK-Merchant-Code": MERCHANT_CODE,
        "X-CEK-Timestamp": timestamp,
        "X-CEK-Signature": signature,
    }
    if body:
        headers["Content-Type"] = "application/json"

    request = urllib.request.Request(BASE_URL + path, data=body.encode() or None, headers=headers, method=method)
    try:
        with urllib.request.urlopen(request, timeout=35) as response:  # an order can take up to 30 seconds
            return response.status, json.loads(response.read())
    except urllib.error.HTTPError as error:  # problem+json body
        return error.code, json.loads(error.read())


# 1) BIN lookup: no body, so the hash is sha256("")
status, bin_info = signed_request("GET", "/api/v4/bins/435509?amount=1500.00")
print(status, bin_info.get("data", {}).get("payment_mode"))

# 2) create an order; a new merchant_order_id for every order
status, order = signed_request("POST", "/api/v4/orders", {
    "merchant_order_id": f"ORDER-{int(time.time())}-{uuid.uuid4().hex[:8]}",
    "amount": "1500.00",
    "card": {"holder_name": "TEST CUSTOMER", "number": "4355090000044242", "expiry_month": "12", "expiry_year": "30", "cvv": "123"},
})
print(status, order.get("data", {}).get("status"))

الاستعلام عن BIN

GET/api/v4/bins/{bin}

استعلم عن BIN البطاقة

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

المعاملات

الحقلالنوعمطلوبالوصف
binstring · pathمطلوبأول ست خانات من رقم البطاقة.
amountstring · queryمطلوبمبلغ الطلب بالليرة التركية بخانتين عشريتين. تُقيَّم الأهلية على هذا المبلغ.

الاستجابة · 200

الحقلالنوعالوصف
binstringBIN الذي استعلمت عنه.
bank_namestring | nullالبنك المُصدر، أو null إن لم يكن معروفًا.
networkstring | nullشبكة البطاقة: visa أو mastercard أو amex أو troy أو null.
card_typestring | nullcredit أو debit أو prepaid أو null.
card_programstring | nullاسم برنامج البطاقة أو null.
countrystring | nullبلد الإصدار (ISO alpha-2) أو null.
domesticbooleantrue للبطاقة الصادرة في تركيا.
payment_modestringdirect: اجمع البطاقة بنفسك. hosted: استخدم الصفحة المستضافة.

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

{
  "data": {
    "bin": "435509",
    "bank_name": "Example Bank",
    "network": "visa",
    "card_type": "credit",
    "card_program": null,
    "country": "TR",
    "domestic": true,
    "payment_mode": "direct"
  }
}
  • مع direct اجمع كامل بيانات البطاقة وأنشئ الطلب باستخدام card.
  • مع hosted أنشئ الطلب بـ bin فقط ووجّه العميل إلى hosted_url المُعاد.

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

إنشاء طلب

POST/api/v4/orders

أنشئ طلبًا

نقطة نهاية واحدة تنشئ الطلب في الوضعين. يحدد الجسم الوضع: أرسل card لطلب direct أو bin فقط لطلب hosted. إرسال الاثنين معًا يُرجع 422.

طلب direct (مع البطاقة)

الحقلالنوعمطلوبالوصف
merchant_order_idstringمطلوبمرجع طلبك: من 1 إلى 64 حرفًا من A-Z a-z 0-9 . _ -، وفريد لكل حساب وبيئة.
amountstringمطلوبالمبلغ بالليرة التركية بخانتين عشريتين، مثل "1500.00".
card.holder_namestringمطلوبالاسم على البطاقة، من 2 إلى 120 حرفًا.
card.numberstringمطلوبرقم البطاقة، من 13 إلى 19 رقمًا، أرقام فقط.
card.expiry_monthstringمطلوبشهر الانتهاء، من "01" إلى "12".
card.expiry_yearstringمطلوبسنة الانتهاء بخانتين، مثل "30".
card.cvvstringمطلوبرمز الأمان، 3 أو 4 أرقام.

جسم الطلب · مثال

{
  "merchant_order_id": "ORDER-1001",
  "amount": "1500.00",
  "card": {
    "holder_name": "TEST CUSTOMER",
    "number": "4355090000044242",
    "expiry_month": "12",
    "expiry_year": "30",
    "cvv": "123"
  }
}

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

طلب hosted (BIN فقط)

الحقلالنوعمطلوبالوصف
merchant_order_idstringمطلوبمرجع طلبك: من 1 إلى 64 حرفًا من A-Z a-z 0-9 . _ -، وفريد لكل حساب وبيئة.
amountstringمطلوبالمبلغ بالليرة التركية بخانتين عشريتين، مثل "1500.00".
binstringمطلوبأول ست خانات من رقم البطاقة.

جسم الطلب · مثال

{
  "merchant_order_id": "ORDER-1002",
  "amount": "1500.00",
  "bin": "540061"
}

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

القواعد

  • يجب أن يكون merchant_order_id فريدًا لكل حساب وبيئة. وهو الحماية الوحيدة من التكرار.
  • تكرار الطلب بالقيمة نفسها لـ merchant_order_id والمحتوى نفسه لا يبدأ دفعة جديدة ويُرجع الطلب بحالته الحالية. أما المحتوى المختلف فيُرجع 409 بالرمز 0055503.
  • أي حقل غير موجود في المخطط يُرجع 422 بالرمز 1000001.
  • عند الاستجابة 4xx لم يُسحب أي مبلغ. استخدم merchant_order_id جديدًا للمحاولة التالية. والطلب failed نهائي أيضًا.
  • بعد استجابة 5xx أو انتهاء المهلة قد تكون الدفعة بدأت فعلًا. لا تنشئ طلبًا جديدًا: كرّر الطلب نفسه كما هو أو اقرأ الطلب عبر GET. اترك 30 ثانية على الأقل قبل انتهاء المهلة.
  • إذا كان BIN في تلك اللحظة من نوع hosted فإن الطلب المتضمن card يُرجع أيضًا requires_hosted ولا تُستخدم بيانات البطاقة.

كائن الطلب

تُرجع استدعاءات الإنشاء والقراءة الطلب داخل data.

الحقلالنوعالوصف
order_codestringرقم الطلب، 9 أرقام كنص. وهو القيمة التي تصل في عنوان العودة.
merchant_order_idstringقيمة merchant_order_id الخاصة بك.
statusstringrequires_action أو requires_hosted أو pending أو paid أو failed.
amountstringمبلغ الطلب بخانتين عشريتين.
currencystringدائمًا TRY.
redirect_urlstring | nullعنوان 3-D Secure. يكون معبّأً ما دامت الحالة requires_action.
hosted_urlstring | nullعنوان الصفحة المستضافة. يكون معبّأً ما دامت الحالة requires_hosted.
hosted_expires_atstring | nullالوقت الذي يتوقف فيه hosted_url عن العمل (ISO 8601، UTC).
failure_reason_codestring | nullرمز سبب الرفض للطلب failed؛ انظر أسباب الرفض. قد يكون null إذا لم تُجرَّ محاولة دفع.
failure_messagestring | nullنص قصير للعميل يشرح السبب. يكون دائمًا بالتركية؛ ابنِ منطقك على الرمز بدلًا منه.
cardobject | nullملخص البطاقة (holder_name وbin وlast_four وnetwork وprogram وissuer_bank_name). يتوفر فورًا في direct، وفي hosted بعد معرفة النتيجة.
created_atstringوقت الإنشاء (ISO 8601، +00:00).
completed_atstring | nullوقت الاكتمال، أو null ما دام الطلب مفتوحًا.

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

{
  "data": {
    "order_code": "123456789",
    "merchant_order_id": "ORDER-1001",
    "status": "requires_action",
    "amount": "1500.00",
    "currency": "TRY",
    "redirect_url": "https://bank.example.com/3ds/123456789",
    "hosted_url": null,
    "hosted_expires_at": null,
    "failure_reason_code": null,
    "failure_message": null,
    "card": {
      "holder_name": "TEST CUSTOMER",
      "bin": "435509",
      "last_four": "4242",
      "network": "visa",
      "program": null,
      "issuer_bank_name": "Example Bank"
    },
    "created_at": "2026-10-13T10:15:02+00:00",
    "completed_at": null
  }
}

requires_hosted

{
  "data": {
    "order_code": "123456789",
    "merchant_order_id": "ORDER-1001",
    "status": "requires_hosted",
    "amount": "1500.00",
    "currency": "TRY",
    "redirect_url": null,
    "hosted_url": "https://pay.example.com/h/123456789",
    "hosted_expires_at": "2026-10-13T10:30:02+00:00",
    "failure_reason_code": null,
    "failure_message": null,
    "card": null,
    "created_at": "2026-10-13T10:15:02+00:00",
    "completed_at": null
  }
}

الحالات والمُهل

الحقلنهائية؟الوصف
requires_actionلاوجّه العميل إلى redirect_url لإتمام 3-D Secure.
requires_hostedلاوجّه العميل إلى hosted_url.
pendingلالم يرد البنك بعد، أو غادر العميل الشاشة. انتظر webhook.
paidنعمتم تحصيل الدفعة.
failedنعملم تتم الدفعة. يشرح failure_reason_code السبب.

أمام العميل 10 دقائق لإتمام 3-D Secure. تعمل الصفحة المستضافة حتى hosted_expires_at. والدفعة التي لا يرد عليها البنك تصبح paid أو failed خلال 24 ساعة، والدفعة المنتهية تصبح failed.

نادرًا ما يتحول طلب failed إلى paid لاحقًا. اعتبر paid هو الحقيقة.

قراءة طلب

GET/api/v4/orders/{merchantOrderId}

اقرأ طلبًا

تُرجع الكائن نفسه كما في الإنشاء، دون جسم طلب. يُعاد redirect_url حتى تنتهي مهلة 3-D Secure، وhosted_url حتى hosted_expires_at.

المعاملات

الحقلالنوعمطلوبالوصف
merchantOrderIdstring · pathمطلوبقيمة merchant_order_id التي استخدمتها عند إنشاء الطلب.

الـ webhook هو القناة الأساسية. وإن تأخر فاقرأ الطلب مرة كل 30 ثانية على الأكثر.

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

3-D Secure والصفحة المستضافة

بعد إنشاء الطلب يُنهي العميل الدفع في صفحة ليست لك. وجّهه إليها بإعادة توجيه عادية للصفحة بأكملها.

3-D Secure (`requires_action`)

  1. أعد توجيه متصفح العميل إلى redirect_url.

  2. يؤكد العميل الدفع لدى بنكه.

  3. عندما تصبح الدفعة نهائية يُوجَّه العميل إلى عنوان العودة الخاص بك ويصلك إشعار webhook.

الصفحة المستضافة (`requires_hosted`)

  1. وجّه العميل إلى hosted_url. تعمل إعادة التوجيه الكاملة للصفحة دائمًا؛ أما عرضها داخل إطار نافذة منبثقة فيعتمد على إذن الصفحة، لذا احتفظ بإعادة التوجيه كخيار احتياطي.

  2. يُدخل العميل البطاقة ويُكمل 3-D Secure في تلك الصفحة. لا تغلق المحاولة الفاشلة الطلب؛ يمكن للعميل المحاولة مجددًا هناك.

  3. إذا أغلق العميل الصفحة فإن hosted_url نفسه يفتحها من جديد حتى hosted_expires_at.

  4. عند انتهاء الدفع تُوجَّه الصفحة بأكملها إلى عنوان العودة الخاص بك. أكّد النتيجة عبر webhook.

عنوان العودة

عندما يصبح الطلب paid أو failed يُوجَّه العميل إلى عنوان العودة المناسب الذي حدّدته في اللوحة. وإذا كان عنوان webhook مضبوطًا فتُضاف إلى العنوان معاملات الاستعلام الموقّعة التالية.

الحقلالنوعالوصف
order_codestringرقم الطلب المكوّن من 9 أرقام. لا يتضمن العنوان merchant_order_id؛ طابق الطلب بهذه القيمة.
statusstringpaid أو failed.
failure_codestringرمز السبب، فقط للطلب failed الذي له سبب. وإلا فالمعامل غائب.
tsintegerوقت Unix بالثواني الذي بُني فيه العنوان.
key_versionintegerإصدار مفتاحك السري. يزداد في كل مرة تجدّد فيها المفتاح السري.
sigstringالتوقيع، 64 خانة hex بأحرف صغيرة.

نص التوقيع القانوني

sig = hex(HMAC-SHA256(key = API_SECRET, message = ts + "." + order_code + "." + status + "." + failure_code))
  • تحقق من sig قبل أن تثق بالمعاملات. إذا غاب failure_code فضع نصًا فارغًا مكانه؛ وتبقى النقطة.
  • ارفض أي ts أقدم من 10 دقائق.
  • العودة تخبر فقط بمكان العميل. تصبح النتيجة نهائية عندما يصل webhook أو يؤكدها GET /api/v4/orders/{merchantOrderId}.
  • إذا لم يكن عنوان webhook مضبوطًا يُفتح العنوان دون هذه المعاملات.

مثال

https://shop.example.com/payment/success?order_code=123456789&status=failed&failure_code=insufficient_funds&ts=1790000000&key_version=1&sig=a6d0ee5e1774b836d04836d4046fff37e719904d9fea69e28a558e619743c4c2

Webhook

تُسلَّم النتائج بطلبات POST موقّعة بصيغة JSON إلى عنوان webhook بصيغة HTTPS الخاص بالبيئة.

الحدثمتى يُرسل
checkout.order.succeededأصبح الطلب paid.
checkout.order.failedأصبح الطلب failed.

الترويسة

الترويسةالوصف
X-CEK-Timestampوقت Unix بالثواني. ارفض أي فرق يتجاوز 300 ثانية.
X-CEK-Signatureالتوقيع: hex لـ HMAC-SHA256 على الطابع الزمني ثم نقطة ثم الجسم الخام، بمفتاح API السري مفتاحًا.
X-CEK-Event-Idمعرّف UUID للحدث. هو نفسه event_id في الجسم ولا يتغير عند إعادة المحاولة.
X-CEK-Environmentsandbox أو live.

تحقق من كل إشعار

  1. اقرأ الجسم الخام قبل تحليل JSON.

  2. احسب التوقيع على X-CEK-Timestamp ثم نقطة ثم الجسم الخام، وقارنه بـ X-CEK-Signature بمقارنة زمنها ثابت.

  3. ارفض الطلب إذا اختلف الطابع الزمني عن ساعتك بأكثر من 300 ثانية.

  4. أزل التكرار باستخدام X-CEK-Event-Id. يتم التسليم مرة واحدة على الأقل وقد يكون خارج الترتيب. إذا رأيت checkout.order.succeeded لطلب ما فتجاهل أي checkout.order.failed له.

  5. أجب بأي 2xx خلال 10 ثوانٍ (3 ثوانٍ للاتصال). تُعدّ إعادة التوجيه (3xx) و4xx و5xx وانتهاء المهلة إخفاقًا وتُعاد المحاولة.

أمثلة على التحقق من التوقيع

<?php
// Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>".
function isValidDirectWebhook(string $rawBody, string $timestamp, string $signature, string $secret): bool
{
    if (preg_match('/^[0-9]{10}$/D', $timestamp) !== 1 || preg_match('/^[0-9a-f]{64}$/D', $signature) !== 1
        || abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    return hash_equals(hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret), $signature);
}

$rawBody = file_get_contents('php://input');
if (!isValidDirectWebhook($rawBody, $_SERVER['HTTP_X_CEK_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_CEK_SIGNATURE'] ?? '', getenv('MERCHANT_API_SECRET'))) {
    http_response_code(401);
    exit;
}
// Deduplicate with X-CEK-Event-Id, save the event, then answer 2xx.
http_response_code(204);
const crypto = require("node:crypto");

// Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>".
// rawBody must be the untouched request bytes (Buffer), not re-serialised JSON.
function isValidDirectWebhook(rawBody, timestamp, signature, secret) {
  if (!/^[0-9]{10}$/.test(timestamp) || !/^[0-9a-f]{64}$/.test(signature)
      || Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(timestamp + "." + rawBody.toString("utf8")).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Then deduplicate with X-CEK-Event-Id, save the event and answer 2xx.
module.exports = { isValidDirectWebhook };
import hashlib
import hmac
import re
import time


def is_valid_direct_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    """Verify a notification: the signature covers "<X-CEK-Timestamp>.<raw body>"."""
    if (not re.fullmatch(r"[0-9]{10}", timestamp) or not re.fullmatch(r"[0-9a-f]{64}", signature)
            or abs(int(time.time()) - int(timestamp)) > 300):
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


# Then deduplicate with X-CEK-Event-Id, save the event and answer 2xx.

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

تُعاد عملية التسليم الفاشلة بعد فترات الانتظار أدناه، محسوبة من الإخفاق السابق، بحد أقصى 11 محاولة.

المحاولةالانتظار قبلها
1—
260 s (1 min)
3300 s (5 min)
4900 s (15 min)
51800 s (30 min)
63600 s (1 h)
710800 s (3 h)
821600 s (6 h)
943200 s (12 h)
1086400 s (24 h)
11172800 s (48 h)

الجسم

يتضمن الجسم schema_version (3) وevent_id وevent_type وكائن order وكائن merchant وملخص البطاقة في payment_details.card. ويضيف الحدث failed الحقول failure_code وfailure_reason_code وfailure_message. تجاهل الحقول التي لا تستخدمها؛ فقد تُضاف حقول جديدة.

جسم الطلب · مثال

{
  "schema_version": 3,
  "event_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6",
  "event_type": "checkout.order.failed",
  "order": {
    "code": "123456789",
    "merchant_order_id": "ORDER-1001",
    "status": "failed",
    "amount": "1500.00",
    "currency": "TRY"
  },
  "merchant": {
    "code": "MRC-123456"
  },
  "payment_details": {
    "card": {
      "holder_name": "TEST CUSTOMER",
      "bin": "435509",
      "last_four": "4242",
      "network": "visa",
      "program": null,
      "issuer_bank_code": null,
      "issuer_bank_name": "Example Bank",
      "installment_count": 1,
      "three_d": true
    }
  },
  "failure_code": "payment_declined",
  "failure_reason_code": "insufficient_funds",
  "failure_message": "Kart bakiyeniz veya limitiniz bu ödeme için yeterli değil."
}

أسباب الرفض

failure_reason_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.
security_declinedأوقف فحص أمني الدفعة. أخبر العميل فقط أن البنك لم يوافق عليها.
payment_not_permittedلا يمكن استخدام هذه البطاقة لهذه الدفعة.
amount_not_allowedالمبلغ خارج النطاق الذي تقبله هذه البطاقة.
currency_not_supportedالعملة غير مدعومة لهذه الدفعة.
payment_details_rejectedرُفضت بيانات الدفع.
duplicate_paymentيبدو أن الدفعة نفسها قد حُصِّلت من قبل.
cancelled_by_customerألغى العميل قبل اكتمال الدفع.
payment_timed_outانتهت مهلة الدفع.
bank_unavailableتعذّر الوصول إلى البنك الآن. أعد المحاولة بعد قليل.
bank_requested_retryطلب البنك إعادة الدفع.
provider_unavailableتعذّر إكمال الدفعة الآن. أعد المحاولة بعد قليل.

قد تتسع القائمة. تعامل مع أي رمز لا تعرفه كما تتعامل مع payment_declined. في webhook يُبلَّغ عن security_declined في failure_code بوصفه payment_declined.

رموز الأخطاء

تستخدم الأخطاء صيغة application/problem+json. ابنِ منطقك على code؛ أما title وdetail فللبشر.

الحقلالنوعالوصف
type, title, status, detailstring · integerحقول المشكلة القياسية.
instancestringمسار الطلب.
codestringرمز الخطأ المكوّن من 7 أرقام من الجدول أدناه.
correlation_idstringاذكره عند التواصل مع الدعم. يصل أيضًا في X-Correlation-Id.
metadataobjectdeveloper_detail وretry_class وaudit_event: معلومات إضافية لتصحيح الأخطاء.
amount_limitobjectفقط مع 0055235: القيمتان min وmax للمبلغ المسموح، أو null عند حد الحجم.
errorsobjectفقط مع 1000001: الحقول التي فشلت في التحقق.

application/problem+json

{
  "type": "https://www.manuellsolutions.com/problems/0055233",
  "title": "BIN not recognised",
  "status": 409,
  "detail": "No card range matches this BIN.",
  "instance": "/api/v4/bins/999999",
  "code": "0055233",
  "correlation_id": "3BAEB108-41A8-40AE-81EF-0A82AA1132D6",
  "metadata": {
    "developer_detail": "bin_not_found",
    "retry_class": "no_retry",
    "audit_event": "bin_lookup_rejected"
  }
}

تحمل استجابات الأخطاء أيضًا X-Correlation-Id وX-RateLimit-Limit وX-RateLimit-Remaining وContent-Language؛ ويضيف 429 الترويسة Retry-After.

تُجرى الفحوص بهذا الترتيب: المخطط (1000001) ثم المبلغ (0055235) ثم BIN (0055233، 0055230) ثم البطاقة (0055234).

الرمزHTTPالمعنى
0055501403قائمة IP المسموح بها فارغة أو أن الطلب يأتي من عنوان غير مدرج فيها.
0055502401 / 403مفتاح أو بيئة أو رمز تاجر خاطئ، أو حساب غير نشط، أو وصول Direct API غير مفتوح.
0055504401الطابع الزمني ليس من 10 أرقام أو يبعد أكثر من 300 ثانية، أو التوقيع ليس من 64 خانة hex بأحرف صغيرة.
0055505401تعذّر التحقق من التوقيع.
1000001422الجسم لا يطابق المخطط: حقل ناقص أو خاطئ أو غير معروف، أو card مع bin. يسرد errors الحقول.
1000005429تم تجاوز حد المعدل. انتظر المدة في Retry-After.
0055001409الحساب غير جاهز بعد لقبول المدفوعات؛ تنقصه خطوة إعداد.
0055002403المدفوعات بالبطاقة متوقفة للحساب.
0055235422المبلغ غير صالح أو خارج الحدود (amount_limit معبّأ)، أو بُلغ حد حجم يومي أو أسبوعي أو شهري (amount_limit يساوي null).
0055230409لا توجد قناة دفع متاحة لهذا BIN.
0055233409تعذّر التعرف على BIN.
0055210422bin ليس ستة أرقام بالضبط.
0055234409رقم البطاقة أو تاريخ الانتهاء غير صالح.
0055221409بلغت هذه البطاقة حد المحاولات أو المبلغ اليومي.
0055231409لا يمكن الدفع بهذه البطاقة في الوقت الحالي.
0055503409استُخدم merchant_order_id نفسه بمحتوى مختلف، أو أن الطلب الأول لا يزال قيد المعالجة.
0055402404لا يوجد طلب بهذا merchant_order_id.
0055404409نقطة نهاية نتيجة sandbox فقط: الطلب ليس في حالة pending ولا requires_hosted.

Sandbox

تستخدم sandbox نقاط النهاية نفسها بمفتاح sandbox والترويسة X-CEK-Environment: sandbox. لا تتحرك أموال حقيقية.

  • X-CEK-Merchant-Code إلزامي في sandbox. وتُستخدم عناوين webhook والعودة الخاصة ببيئة sandbox.
  • استخدم أي تاريخ انتهاء في المستقبل وأي رمز CVV من 3 أرقام.
  • تحتسب حدود الحجم طلبات sandbox فقط.
BINHTTPرمز النتيجة
435509200direct
540061200hosted
9792104090055230
أي BIN آخر4090055233
رقم البطاقةالسلوك
4355090000044242requires_action. وافق في شاشة 3-D Secure التجريبية ليصبح الطلب paid.
4355090000005151requires_action. بعد 3-D Secure يصبح الطلب failed مع insufficient_funds.
4355090000089999requires_action. بعد 3-D Secure يبقى الطلب pending؛ أغلقه عبر نقطة نهاية نتيجة sandbox.
4355090000040000يصبح failed فورًا مع payment_declined قبل 3-D Secure.
أي بطاقة صالحة أخرىrequires_action. في شاشة 3-D Secure التجريبية تختار النتيجة: النجاح paid والفشل failed (payment_declined) وانتهاء المهلة يُبقي pending.

رقم بطاقة غير صالح (Luhn) أو تاريخ انتهاء في الماضي يُرجع 409 بالرمز 0055234. يستخدم طلب hosted الـ BIN 540061؛ وفي صفحة الاختبار تختار النجاح (paid) أو الفشل (failed مع insufficient_funds). وما دام الطلب في حالة requires_hosted يمكنك أيضًا إغلاقه عبر نقطة نهاية نتيجة sandbox.

POST/api/v4/sandbox/orders/{merchantOrderId}/result

حدّد نتيجة طلب

يغلق طلب sandbox في حالة pending أو، في طلب hosted، ما زال ينتظر صفحة hosted الخاصة به (requires_hosted). يعمل webhook وعنوان العودة كما في الإنتاج.

الحقلالنوعمطلوبالوصف
resultstringمطلوبpaid أو failed (الطلب الفاشل يحصل على السبب payment_declined).

جسم الطلب · مثال

{
  "result": "paid"
}

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

تحدّث مع مدير حسابك بشأن طرفية الإنتاج والاختبار الشامل الأخير. تواصل معنا