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

setup_intent
يحفظ الشخص بطاقة في حسابه دون أن يدفع الآن، ليتمكن عميلك من الشراء بها لاحقًا.
ابدأ إضافة بطاقة
يردّ
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…" }أرسل إلى الشخص رابطًا
ضع القيم الثلاث في رابط إلى صفحة الدفع، مع ضبط
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();احفظ البطاقة
يحفظ
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 | نعم |
|
| sealed_secret | نعم | القيمة من الاستجابة، كما وصلت تمامًا. |
| key_id | نعم | القيمة من الاستجابة. تحدد المفتاح الذي خُتم السر له. |
| seal_signature | نعم | القيمة من الاستجابة. تفحصها الصفحة وترفض أي رابط تكون فيه مفقودة أو غير مطابقة. |
| options | لا | قيمة |
| options_signature | لا | القيمة من الاستجابة دون تغيير، مع |
| lang | لا | لغة الصفحة، وهي إحدى اللغات الخمس عشرة التي يستخدمها هذا الموقع. وبدونها تُستخدم اللغة التي اختارها الشخص على heyatli.com، ثم لغة المتصفح. |
| theme | لا |
|
| brand | لا | يُخفي |
| redirect_success_url | لا | إلى أين يُرسَل الشخص بعد ذلك. يجب أن يكون |
| redirect_cancel_url | لا | إلى أين يُرسَل الشخص بعد ذلك. يجب أن يكون |
| redirect_failure_url | لا | إلى أين يُرسَل الشخص بعد ذلك. يجب أن يكون |
| return_id | لا | مرجعك الخاص، يُضاف إلى إعادة التوجيه. أحرف وأرقام و |
كيف تتعامل مع الرابط
مرّر القيم دون تغيير. يتولى
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>