أداة الدفع

أداة الدفع

وجّه شخصًا إلى صفحة آمنة من Atli ليدفع أو يضيف بطاقة، عندما لا يستطيع تطبيقك عرض نموذج Stripe بنفسه.

متى تستخدمها

  • مساعدو الذكاء الاصطناعي وعملاء MCP، الذين يتحدثون مع الناس في محادثة وليس لديهم شاشة خاصة لوضع نموذج البطاقة عليها.

  • الدفع خارج المسار المعتاد، مثل بوت في Telegram أو WhatsApp، أو الإكمال على جهاز آخر أو في وقت لاحق.

  • أي عميل آخر لا يستطيع تحميل SDK الخاص بـ Stripe، مثل الطرفية أو تطبيق صوتي.

PCI DSS
نموذج صفحة الدفع، مع البطاقة وApple Pay

setup_intent

يحفظ الشخص بطاقة في حسابه دون أن يدفع الآن، ليتمكن عميلك من الشراء بها لاحقًا.

  1. ابدأ إضافة بطاقة

    يردّ POST /user/payment-methods/setup-intent بالحقول الثلاثة إلى جانب client_secret.

    لبطاقة مجموعة، استخدم POST /groups/:groupId/cards/setup وPOST /groups/:groupId/cards/confirm بالطريقة نفسها. تسمّي ردودهما المعرّف setup_intent_id.

    curl -X POST https://api.heyatli.com/user/payment-methods/setup-intent \
      -H 'Authorization: Bearer YOUR_TOKEN'
    
    {
      "id": "seti_…",
      "client_secret": "seti_…_secret_…",
      "sealed_secret": "eyJhbGciOiJFQ0RILUVTIi…",
      "key_id": "QvWE2gHWYExKj2Yc…",
      "seal_signature": "c2lnbmF0dXJl…",
      "options": "{\"lang\":\"en\",\"message\":\"Plan for Australia\"}",
      "options_signature": "b3B0aW9ucw…"
    }
  2. أرسل إلى الشخص رابطًا

    ضع القيم الثلاث في رابط إلى صفحة الدفع، مع ضبط type على setup_intent. يرى الشخص نموذج Stripe فقط، دون مبلغ.

    const link = new URL('https://heyatli.com/pay');
    link.search = new URLSearchParams({
      type: 'setup_intent',
      sealed_secret: response.sealed_secret,
      key_id: response.key_id,
      seal_signature: response.seal_signature,
      return_id: response.id,
      // Only when the response has them, and always together. options goes in
      // base64url-encoded (Buffer in Node; TextEncoder and btoa in a browser).
      ...(response.options && {
        options: Buffer.from(response.options).toString('base64url'),
        options_signature: response.options_signature,
      }),
    }).toString();
  3. احفظ البطاقة

    يحفظ POST /user/payment-methods/confirm البطاقة في الحساب عندما ينتهي الشخص. بعد ذلك يعرضها GET /user/payment-methods في القائمة.

    curl https://api.heyatli.com/user/payment-methods/confirm \
      -H 'Authorization: Bearer YOUR_TOKEN' \
      -H 'Content-Type: application/json' \
      -d '{ "setup_intent_id": "seti_…" }'

ما الذي يوضع في الرابط

المعاملمطلوبوظيفته
typeنعم

payment_intent أو setup_intent. يجب أن يطابق نوع السر الذي جرى ختمه.

sealed_secretنعم

القيمة من الاستجابة، كما وصلت تمامًا.

key_idنعم

القيمة من الاستجابة. تحدد المفتاح الذي خُتم السر له.

seal_signatureنعم

القيمة من الاستجابة. تفحصها الصفحة وترفض أي رابط تكون فيه مفقودة أو غير مطابقة.

optionsلا

قيمة options من الاستجابة مُرمَّزة بـ base64url. تحمل حاليًا رسالة تعرضها الصفحة فوق النموذج، بلغة طلبك. لا تصل إلا عندما يكون لدى واجهة البرمجة ما تعرضه.

options_signatureلا

القيمة من الاستجابة دون تغيير، مع options. إذا لم تتطابق، تتجاهل الصفحة الخيارات وتعمل كالمعتاد.

langلا

لغة الصفحة، وهي إحدى اللغات الخمس عشرة التي يستخدمها هذا الموقع. وبدونها تُستخدم اللغة التي اختارها الشخص على heyatli.com، ثم لغة المتصفح.

themeلا

dark أو light. افتراضيًا، حسب المتصفح.

brandلا

يُخفي none شعار Atli وشارتها. افتراضيًا، يظهران.

redirect_success_urlلا

إلى أين يُرسَل الشخص بعد ذلك. يجب أن يكون https على heyatli.com أو أحد نطاقاتها الفرعية.

redirect_cancel_urlلا

إلى أين يُرسَل الشخص بعد ذلك. يجب أن يكون https على heyatli.com أو أحد نطاقاتها الفرعية.

redirect_failure_urlلا

إلى أين يُرسَل الشخص بعد ذلك. يجب أن يكون https على heyatli.com أو أحد نطاقاتها الفرعية.

return_idلا

مرجعك الخاص، يُضاف إلى إعادة التوجيه. أحرف وأرقام و_ و-، حتى 128 حرفًا.

كيف تتعامل مع الرابط

  • مرّر القيم دون تغيير. يتولى URLSearchParams الترميز.

  • مرِّر خيارات الصفحة أيضًا. إذا تضمنت الاستجابة options وoptions_signature، فأضفهما معًا: options مُرمَّزة بـ base64url، وoptions_signature كما وصلت. لا تغيّر JSON قبل ترميزه.

  • ابقَ في بيئة واحدة. قيم Sandbox تُفتح فقط على staging.heyatli.com/pay، وقيم Production فقط على heyatli.com/pay.

  • تعامل معه كما تتعامل مع كلمة مرور. أرسله فقط إلى صاحب الحساب، ولا تدعه يظهر في السجلات.

  • طلب واحد في كل مرة. يلغي الشراء الجديد الطلب المعلّق السابق ورابطه، لذا لا تخزّن الروابط.

  • إذا كانت الحقول الثلاثة مفقودة، أعد الاستدعاء. لا تضع أبدًا client_secret عاديًا في رابط.

  • إعادة التوجيه لا تثبت شيئًا. لا تثق إلا بالتحقق في الخطوة الأخيرة.

كيف يعمل التشفير والتوقيع

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

صفحة الدفع
https://heyatli.com/pay
مفاتيح التشفير
https://heyatli.com/api/keys
مفاتيح التوقيع
https://api.heyatli.com/keys

التشفير

sealed_secret هو client_secret في صيغة JWE مضغوطة، وهي صيغة آمنة أصلًا داخل الروابط. وهو مشفّر بالمفتاح العام X25519 للموقع، لذا لا يستطيع قراءته إلا خادم الموقع. وkey_id هو id لذلك المفتاح، ويطابق kid في JWE.

alg
ECDH-ES
enc
A256GCM
kid
key_id
Key
JWK: kty OKP, crv X25519, x = public_key

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

import { CompactEncrypt, importJWK } from 'jose';

const { keys: [key] } = await (await fetch('https://heyatli.com/api/keys')).json();

const publicKey = await importJWK(
  { kty: 'OKP', crv: 'X25519', x: key.public_key },
  'ECDH-ES'
);
const sealedSecret = await new CompactEncrypt(new TextEncoder().encode(clientSecret))
  .setProtectedHeader({ alg: 'ECDH-ES', enc: 'A256GCM', kid: key.id })
  .encrypt(publicKey);

التوقيع

بما أن أي شخص يستطيع التشفير، فإن القيمة المختومة لا تُظهر من أنشأها. لذلك توقّعها واجهة API بمفتاح خاص Ed25519 لا يملكه غيرها. الرسالة الموقّعة هي بايتات UTF-8 للأسطر الثلاثة التالية، موصولة بسطر جديد، دون سطر جديد في النهاية:

atli-seal-v1
<key_id>
<sealed_secret>

تكوّن واجهة API التوقيع بهذه الطريقة، بمفتاح خاص لا يغادرها أبدًا:

// What the API does. Only it holds the private key.
const message = new TextEncoder().encode(
  `atli-seal-v1\n${keyId}\n${sealedSecret}`
);

const signature = await crypto.subtle.sign('Ed25519', privateKey, message);
const sealSignature = Buffer.from(signature).toString('base64url');

seal_signature هو التوقيع الخام ذو 64 بايت بصيغة base64url دون حشو، أي 86 حرفًا. توقّع كل بيئة بمفتاحها الخاص، وقد تحتوي القائمة على مفتاحين أثناء التدوير.

تفحص صفحة الدفع هذا قبل أن تفتح أي شيء، وترفض الرابط الذي يكون فيه مفقودًا أو غير مطابق. وللتحقق بنفسك مما أعاده الخادم، أعد بناء تلك الرسالة وتحقق من التوقيع باستخدام مفاتيح التوقيع:

// In Node. sealedSecret, keyId and sealSignature come from the API response.
const { keys } = await (await fetch('https://api.heyatli.com/keys')).json();

const base64url = (value) => Uint8Array.from(Buffer.from(value, 'base64url'));
const message = new TextEncoder().encode(
  `atli-seal-v1\n${keyId}\n${sealedSecret}`
);

// The list can hold two keys during a rotation: one match is enough.
const checks = keys.map(async (key) =>
  crypto.subtle.verify(
    'Ed25519',
    await crypto.subtle.importKey('raw', base64url(key.public_key), 'Ed25519', false, ['verify']),
    base64url(sealSignature),
    message
  )
);
const valid = (await Promise.all(checks)).some(Boolean);

تغيير sealed_secret أو key_id يُبطل التوقيع. فهو لا يشمل type ولا عمليات إعادة التوجيه ولا المعاملات الأخرى، ولا يثبت أن من يفتح الرابط يملك الحساب. يظل الرابط الموقّع صالحًا ما دام Stripe يقبل السر.

خيارات الصفحة

تُرجع واجهة البرمجة options نصَّ JSON. يحتوي على message، وهو نص عادي حتى 300 حرف، وlang، وهي لغة الرسالة. قد تُضاف مفاتيح أخرى، وتتجاهل الصفحة ما لا تعرفه منها. تتبع الرسالة لغة طلبك: X-Locale أو Accept-Language أو ?lang=، كما في كل واجهة البرمجة. وفي الرابط تكون بترميز base64url، فلا تحتاج إلى تهريب وتبقى قصيرة بأي لغة. الخيارات إضافية: إذا غابت أو لم تجتز التحقق، تقبل الصفحة الدفع بدونها.

// response.options, as the API returns it
{"lang":"en","message":"Plan for Australia"}

يُنشأ options_signature بالمفتاح نفسه على هذه الأسطر الأربعة، حيث <options> هي JSON تمامًا كما أرجعتها واجهة البرمجة. تفك الصفحة ترميز القيمة من الرابط وتتحقق منها. ولأن السر المختوم جزء منه، لا تجتاز الخيارات المنقولة إلى رابط آخر التحقق:

atli-options-v1
<key_id>
<sealed_secret>
<options>