توثيق المطورينAPI v1
دليل الربط عبر واجهة Udeyou OTP
واجهة REST بصيغة JSON لإرسال رموز التحقق إلى بريد عملائك والتحقق منها من خادمك، مع توثيق كامل بالعربية وأمثلة مُختبرة بسبع لغات وأطر عمل.
https://udeyou.com/api/v1
- 1خادمك يطلب الرمز
- 2Udeyou تولّد الرمز وترسله
- 3يصل الرمز إلى بريد العميل
- 4خادمك يتحقق من الرمز
محتويات الصفحة
البداية
نظرة عامة
ترسل Udeyou رمز تحقق لمرة واحدة إلى بريد عميلك باسم متجرك، ثم تتحقق من الرمز الذي يُدخله. يتصل خادمك بالواجهة مباشرة، ولا يتعامل تطبيقك أو موقعك مع مفتاح API.
-
1
الإرسال
يرسل خادمك بريد العميل إلى
POST /otp/send، فتولّد Udeyou الرمز وترسله وتعيدotp_id. -
2
الاستلام
تصل إلى العميل رسالة باسم متجرك فيها الرمز ومدة صلاحيته، فيُدخله في تطبيقك أو موقعك.
-
3
التحقق
يرسل خادمك الرمز مع
otp_idإلىPOST /otp/verify، ويحصل علىverified: trueعند التطابق.
- الصيغة
- JSON في الطلب والرد، مع الترويسة
Content-Type: application/json. - قناة الإرسال
- البريد الإلكتروني وواتساب (تجريبي) متاحان الآن عبر معامل
channel، والرسائل النصية قريبًا. - بنية الرد
successمعdataعند النجاح، أوerrorيحتويcodeوmessageعند الفشل.- الرصيد
- رصيد واحد لكل رمز يُرسل بمفتاح حي، ويُعاد تلقائيًا إذا تعذّر الإرسال.
البداية
البدء السريع
ثلاث خطوات لإرسال أول رمز والتحقق منه باستخدام مفتاح تجريبي.
-
أنشئ حسابًا
سجّل في Udeyou لتبدأ تجربة مجانية لمدة شهرين: 2,000 رسالة في الشهر الأول ومثلها في الشهر الثاني للإرسال الفعلي. إنشاء حساب
-
أنشئ مفتاحًا تجريبيًا
من قسم المفاتيح في لوحة التحكم أنشئ مفتاحًا يبدأ بـ
udy_test_. يظهر المفتاح مرة واحدة فقط، فاحفظه في متغير البيئةUDEYOU_API_KEYعلى خادمك. -
أرسل أول رمز
المفتاح التجريبي لا يرسل بريدًا ولا يخصم رصيدًا، ويعيد الرمز في الرد لتكمل التحقق مباشرة.
curl -sS --max-time 20 -X POST https://udeyou.com/api/v1/otp/send \ -H "Authorization: Bearer $UDEYOU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"to": "user@example.com", "lang": "ar"}'{ "success": true, "data": { "otp_id": "otp_b8042658f0f2959de5df7d1d", "to": "user@example.com", "status": "sent", "mode": "test", "expires_in": 300, "expires_at": "2026-10-09T09:16:17Z", "credits_remaining": 100, "test_mode": true, "code": "625056" } }
عند الانتقال إلى الإنتاج استبدل المفتاح التجريبي بمفتاح حي يبدأ بـ udy_live_، دون أي تغيير آخر في الطلبات.
البداية
المصادقة
أنشئ مفتاحًا من لوحة التحكم وأرسله مع كل طلب بإحدى الترويستين التاليتين. إذا أرسلت الاثنتين تُعتمد Authorization. فحص الحالة /health هو الطلب الوحيد الذي لا يحتاج مفتاحًا.
| الطريقة | الترويسة |
|---|---|
| الأساسية | Authorization: Bearer <API_KEY> |
| البديلة | X-API-Key: <API_KEY> |
أنواع المفاتيح
المفتاح الحي
udy_live_- يرسل الرسالة فعليًا إلى بريد العميل.
- يخصم رصيدًا واحدًا لكل رمز يُرسل، ويعيده إذا تعذّر الإرسال.
- للاستخدام في الإنتاج.
المفتاح التجريبي
udy_test_- لا يرسل بريدًا ولا يخصم رصيدًا، ويعمل حتى برصيد صفر.
- يعيد الرمز في الرد ضمن الحقل
codeمعtest_mode: true. - للتطوير والاختبار الآلي.
يرى كل مفتاح رموز نوعه فقط، فالرمز المرسل بمفتاح تجريبي لا يُتحقق منه بمفتاح حي. أما حدود الإرسال فمشتركة بين النوعين على مستوى الحساب. والمفتاح المفقود أو غير الصحيح أو الملغى يعيد 401 invalid_api_key، والحساب الموقوف يعيد 403 account_suspended.
أبقِ المفتاح على الخادم
لا تضع المفتاح في كود المتصفح أو تطبيق الجوال. يتصل تطبيقك بخادمك، وخادمك وحده يتصل بـ Udeyou. نحفظ المفاتيح كبصمة SHA-256 ولا تظهر إلا مرة واحدة عند إنشائها، ويمكنك إلغاء أي مفتاح من لوحة التحكم فيتوقف قبوله فورًا.
مرجع الواجهة
إرسال رمز
ينشئ رمزًا جديدًا ويرسله إلى بريد المستلم، ويعيد otp_id الذي تستخدمه في التحقق. إرسال رمز جديد إلى البريد نفسه يُلغي الرموز السابقة له.
حقول الطلب
tostringإلزاميبريد المستلم (بحد أقصى 190 حرفًا) عند الإرسال البريدي، أو رقم الجوال بصيغة E.164 بدون + (مثل 9665xxxxxxxx) عند الإرسال عبر واتساب.
channelstringاختياريemail (الافتراضي) أو whatsapp. قناة التسليم — واتساب متاحة حاليًا في وضع تجريبي وتتطلب رقم مستلم مسجلًا في قائمة الأرقام التجريبية.
sender_namestringاختيارياسم المرسل الظاهر في صندوق الوارد، من 2 إلى 60 حرفًا. الافتراضي اسم الحساب. تُستبدل فواصل الأسطر وأحرف التحكم فيه بمسافة.
langstringاختياريar (الافتراضي) أو en. يحدد لغة الرسالة الافتراضية وسطر التذييل.
code_lengthintegerاختياريعدد خانات الرمز المولَّد، من 4 إلى 10. الافتراضي 6.
expires_inintegerاختياريمدة الصلاحية بالثواني، من 60 إلى 3600. الافتراضي 300 (5 دقائق).
codestringاختياريرمز تحدده بنفسك من 4 إلى 10 أحرف أو أرقام. إذا لم ترسله تولّد Udeyou رمزًا رقميًا.
templatestringاختيارينص الرسالة، نص عادي حتى 1000 حرف ويجب أن يحتوي {{code}}. يمكنك استخدام {{sender_name}} و{{minutes}}. لا يُفسَّر HTML داخل القالب.
subjectstringاختياريعنوان الرسالة حتى 120 حرفًا، ويقبل المتغيرات نفسها بما فيها {{code}}.
reply_tostringاختياريبريد دعم متجرك الذي يستقبل ردود العميل على الرسالة.
أرسل code_length وexpires_in كأرقام في JSON، فالقيمة النصية مثل "6" تُرفض بالخطأ 422. ويُضاف إلى نهاية كل رسالة سطر يوضح أنها أُرسلت نيابةً عن sender_name عبر Udeyou OTP.
مثال
curl -sS --max-time 20 -X POST https://udeyou.com/api/v1/otp/send \
-H "Authorization: Bearer $UDEYOU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "user@example.com", "lang": "ar"}'
# إرسال OTP عبر واتساب (تجريبي) — to = رقم الجوال بصيغة E.164 بدون +
curl -sS --max-time 20 -X POST https://udeyou.com/api/v1/otp/send -H "Authorization: Bearer $UDEYOU_API_KEY" -H "Content-Type: application/json" -d '{"to": "9665XXXXXXXX", "channel": "whatsapp", "lang": "ar"}'{
"success": true,
"data": {
"otp_id": "otp_61b7bfa182c0d13b33a9349f",
"to": "user@example.com",
"channel": "email",
"status": "sent",
"mode": "live",
"expires_in": 300,
"expires_at": "2026-10-09T09:17:23Z",
"credits_remaining": 99
}
}حقول الرد
otp_idstringمعرّف الرمز. احفظه في جلسة المستخدم على خادمك لاستخدامه في التحقق.
tostringبريد المستلم بأحرف صغيرة.
statusstringالقيمة sent في الرد الناجح.
modestringlive أو test حسب نوع المفتاح.
expires_inintegerمدة الصلاحية بالثواني.
expires_atstringوقت انتهاء الصلاحية بتوقيت UTC وبصيغة ISO 8601.
credits_remainingintegerالرصيد المتبقي بعد الإرسال.
test_mode, codeboolean, stringيظهران مع المفتاح التجريبي فقط. الرمز نص وقد يبدأ بصفر، فلا تحوّله إلى رقم.
تخصيص الرسالة
يمكنك تحديد اسم المرسل والعنوان والنص وطول الرمز ومدة الصلاحية في الطلب نفسه.
curl -sS --max-time 20 -X POST https://udeyou.com/api/v1/otp/send \
-H "Authorization: Bearer $UDEYOU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"sender_name": "My Store",
"lang": "en",
"code_length": 8,
"expires_in": 600,
"subject": "Login code",
"template": "Your {{sender_name}} code is {{code}}. Valid for {{minutes}} minutes.",
"reply_to": "help@example.com"
}'
curl -sS --max-time 20 -X POST https://udeyou.com/api/v1/otp/send \
-H "Authorization: Bearer $UDEYOU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"sender_name": "متجر الورد",
"template": "رمز الدخول إلى متجر الورد: {{code}} (صالح {{minutes}} دقائق)"
}'
مرجع الواجهة
التحقق من الرمز
يتحقق من الرمز الذي أدخله العميل. عند التطابق يُستهلك الرمز ولا يُقبل مرة أخرى.
حقول الطلب
codestringإلزاميالرمز الذي أدخله العميل، من 4 إلى 10 أحرف أو أرقام.
otp_idstringموصى بهالمعرّف المعاد من طلب الإرسال.
tostringبديلبريد المستلم بدلًا من otp_id، ويُتحقق حينها من آخر رمز أُرسل إليه.
مثال
curl -sS --max-time 20 -X POST https://udeyou.com/api/v1/otp/verify \
-H "Authorization: Bearer $UDEYOU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"otp_id": "otp_61b7bfa182c0d13b33a9349f", "code": "716699"}'
{
"success": true,
"data": {
"verified": true,
"otp_id": "otp_61b7bfa182c0d13b33a9349f",
"to": "user@example.com",
"verified_at": "2026-10-09T09:12:23Z"
}
}
حقول الرد
verifiedbooleanالقيمة true عند تطابق الرمز.
otp_idstringمعرّف الرمز الذي تم التحقق منه.
tostringالبريد الذي تم التحقق منه. اعتمد عليه في خادمك بدلًا من بريد يرسله التطبيق.
verified_atstringوقت التحقق بتوقيت UTC وبصيغة ISO 8601.
عند إدخال رمز خاطئ
تُحتسب محاولة مع كل رمز خاطئ، ويعيد الحقل attempts_remaining عدد المحاولات المتبقية.
{
"success": false,
"error": {
"code": "invalid_code",
"message": "The code is incorrect.",
"attempts_remaining": 4
}
}
لكل رمز 5 محاولات. بعد استنفادها يُرفض حتى الرمز الصحيح بالخطأ
429 too_many_attempts، ويلزم إرسال رمز جديد.الرمز يُستخدم مرة واحدة، وإعادة التحقق به تعيد
409 otp_already_used.بعد انتهاء الصلاحية، أو بعد إرسال رمز أحدث إلى البريد نفسه، يعيد الرمز القديم
410 otp_expired.يتحقق كل مفتاح من رموز نوعه فقط، وغير ذلك يعيد
404 otp_not_found.
مرجع الواجهة
الرصيد
يعيد اسم الحساب ونوع المفتاح المستخدم والرصيد الحالي. يعمل بأي من ترويستي المصادقة.
curl -sS --max-time 20 https://udeyou.com/api/v1/balance \
-H "Authorization: Bearer $UDEYOU_API_KEY"
curl -sS --max-time 20 https://udeyou.com/api/v1/balance \
-H "X-API-Key: $UDEYOU_API_KEY"
{
"success": true,
"data": {
"company": "متجر الورد",
"mode": "live",
"credits": 98
}
}
companystringاسم الحساب، وهو اسم المرسل الافتراضي.
modestringنوع المفتاح المستخدم في الطلب: live أو test.
creditsintegerالرصيد الحالي.
مرجع الواجهة
فحص الحالة
يتأكد من أن الواجهة تعمل. لا يحتاج مفتاحًا ولا يخصم رصيدًا، ويعيد status بدلًا من data.
curl -sS https://udeyou.com/api/v1/health
{
"success": true,
"status": "ok"
}
التشغيل
الحدود
قيم افتراضية على مستوى المنصة تحمي عملاءك وحسابك من الإرسال المتكرر. تُحتسب لكل حساب، ويتشاركها المفتاح الحي والتجريبي.
| الحد | القيمة الافتراضية | الخطأ | retry_after |
|---|---|---|---|
| طلبات الإرسال لكل حساب | 60 طلبًا في الدقيقة | rate_limited | 60 |
| الفاصل بين رمزين للبريد نفسه | 60 ثانية | resend_too_soon | 60 |
| الرموز للبريد نفسه | 5 رموز في الساعة | recipient_limit | 3600 |
| المحاولات الخاطئة لكل رمز | 5 محاولات | too_many_attempts | لا يوجد |
| صلاحية الرمز | 300 ثانية، وتُضبط من 60 إلى 3600 | otp_expired | لا يوجد |
- تدخل طلبات الإرسال وحدها في حد الدقيقة، أما طلبات التحقق والرصيد فلا.
- قيمة
retry_afterهي طول النافذة بالثواني، وليست الوقت المتبقي. انتظرها ثم أعد المحاولة.
{
"success": false,
"error": {
"code": "resend_too_soon",
"message": "Wait 60 seconds before sending another code to this address.",
"retry_after": 60
}
}
التشغيل
أكواد الأخطاء
تعيد الأخطاء حالة HTTP مناسبة مع كائن error. اعتمد في منطقك على error.code، أما message فنص توضيحي بالإنجليزية.
{
"success": false,
"error": {
"code": "validation_error",
"message": "Some fields are invalid.",
"fields": {
"to": "A valid recipient email is required.",
"code_length": "code_length must be an integer between 4 and 10."
}
}
}
| HTTP | الكود | الطلب | المعنى | حقول إضافية |
|---|---|---|---|---|
| 400 | invalid_json |
الإرسال، التحقق | جسم الطلب ليس كائن JSON. | |
| 400 | invalid_code |
التحقق | الرمز غير صحيح، وتُحتسب محاولة. | attempts_remaining |
| 401 | invalid_api_key |
كل الطلبات عدا /health |
المفتاح مفقود أو بصيغة خاطئة أو غير معروف أو ملغى. | |
| 402 | insufficient_credits |
الإرسال بمفتاح حي | الرصيد صفر. | |
| 403 | account_suspended |
كل الطلبات عدا /health |
الحساب موقوف. | |
| 404 | otp_not_found |
التحقق | لا يوجد رمز مطابق لـ otp_id أو to ضمن نوع المفتاح. |
|
| 404 | not_found |
أي مسار | المسار غير موجود. | |
| 405 | method_not_allowed |
أي مسار | طريقة HTTP غير مدعومة لهذا المسار. | |
| 409 | otp_already_used |
التحقق | الرمز استُخدم مسبقًا. | |
| 410 | otp_expired |
التحقق | انتهت الصلاحية، أو أُرسل رمز أحدث إلى البريد نفسه. | |
| 422 | validation_error |
الإرسال | حقول غير صالحة. | fields |
| 422 | validation_error |
التحقق | الحقل code غير صالح، أو لم يُرسل otp_id ولا to. |
|
| 429 | rate_limited |
الإرسال | تجاوز 60 طلب إرسال في الدقيقة. | retry_after |
| 429 | resend_too_soon |
الإرسال | أقل من 60 ثانية منذ آخر رمز لهذا البريد. | retry_after |
| 429 | recipient_limit |
الإرسال | 5 رموز لهذا البريد خلال الساعة الأخيرة. | retry_after |
| 429 | too_many_attempts |
التحقق | استُنفدت المحاولات (5)، ويلزم طلب رمز جديد. | |
| 500 | server_error |
أي مسار | خطأ غير متوقع في الخادم. | |
| 502 | delivery_failed |
الإرسال بمفتاح حي | تعذّر الإرسال عبر خادم البريد، وأُعيد الرصيد. |
الربط
أمثلة الربط: سبع لغات وأطر عمل
أمثلة كاملة شُغّلت فعليًا على الواجهة بمفتاح تجريبي ومفتاح حي: ترسل رمزًا ثم تتحقق منه، وتعرض كود الخطأ وتفاصيله عند الفشل. تقرأ المفتاح من متغير البيئة UDEYOU_API_KEY على الخادم، ويمكن تغيير الرابط الأساسي عبر UDEYOU_BASE_URL. مع المفتاح التجريبي يكتمل التحقق تلقائيًا لأن الرمز يعود في الرد، ومع المفتاح الحي يطلب المثال إدخال الرمز.
لكل لغة دليل كامل قابل للتنزيل بجانب مثالها، والنسخة الإنجليزية هي المرجع الأساسي.
#!/usr/bin/env bash
# Send a verification code by email.
# Usage: UDEYOU_API_KEY=udy_test_... ./send.sh user@example.com
BASE_URL="${UDEYOU_BASE_URL:-https://udeyou.com/api/v1}"
TO="${1:-user@example.com}"
curl -sS --max-time 20 -X POST "$BASE_URL/otp/send" \
-H "Authorization: Bearer $UDEYOU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "'"$TO"'", "lang": "ar"}'
echo
#!/usr/bin/env bash
# Verify the code the user entered.
# Usage: UDEYOU_API_KEY=udy_test_... ./verify.sh otp_xxxxxxxx 123456
BASE_URL="${UDEYOU_BASE_URL:-https://udeyou.com/api/v1}"
curl -sS --max-time 20 -X POST "$BASE_URL/otp/verify" \
-H "Authorization: Bearer $UDEYOU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"otp_id": "'"$1"'", "code": "'"$2"'"}'
echo
#!/usr/bin/env bash
# Current credit balance for the account that owns the key.
BASE_URL="${UDEYOU_BASE_URL:-https://udeyou.com/api/v1}"
curl -sS --max-time 20 "$BASE_URL/balance" \
-H "Authorization: Bearer $UDEYOU_API_KEY"
echo
<?php
// Udeyou OTP - PHP 8.1+ (ext-curl, ext-json)
// Usage: UDEYOU_API_KEY=udy_test_... php udeyou.php user@example.com
declare(strict_types=1);
final class UdeyouException extends RuntimeException
{
public function __construct(
public readonly int $status,
public readonly string $errorCode,
string $message,
public readonly array $details = [], // retry_after, attempts_remaining, fields
) {
parent::__construct($message);
}
}
function udeyou(string $method, string $path, ?array $body = null): array
{
$baseUrl = rtrim(getenv('UDEYOU_BASE_URL') ?: 'https://udeyou.com/api/v1', '/');
$ch = curl_init($baseUrl . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('UDEYOU_API_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
}
$raw = curl_exec($ch);
if ($raw === false) {
throw new UdeyouException(0, 'network_error', curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$json = json_decode($raw, true);
if ($status >= 400 || !is_array($json) || ($json['success'] ?? false) !== true) {
$error = is_array($json['error'] ?? null) ? $json['error'] : [];
throw new UdeyouException(
$status,
$error['code'] ?? 'http_' . $status,
$error['message'] ?? 'Unexpected response',
array_diff_key($error, ['code' => 1, 'message' => 1]),
);
}
return $json['data'];
}
// 1) Send the code
$to = $argv[1] ?? 'user@example.com';
try {
$sent = udeyou('POST', '/otp/send', ['to' => $to, 'lang' => 'ar']);
echo "Sent {$sent['otp_id']} to {$sent['to']} (expires in {$sent['expires_in']} s)\n";
// 2) Verify the code the user typed. Test keys (udy_test_) return it in the response.
$code = $sent['code'] ?? null;
if ($code === null) {
echo 'Code: ';
$code = trim((string) fgets(STDIN));
}
$result = udeyou('POST', '/otp/verify', ['otp_id' => $sent['otp_id'], 'code' => $code]);
echo "Verified {$result['to']} at {$result['verified_at']}\n";
} catch (UdeyouException $e) {
fwrite(STDERR, "Error {$e->status} {$e->errorCode}: {$e->getMessage()}\n");
if ($e->details) {
fwrite(STDERR, json_encode($e->details, JSON_UNESCAPED_SLASHES) . "\n");
}
exit(1);
}
// config/services.php: add this entry to the returned array
'udeyou' => [
'key' => env('UDEYOU_API_KEY'), // .env: UDEYOU_API_KEY=udy_test_...
'url' => env('UDEYOU_BASE_URL', 'https://udeyou.com/api/v1'),
],
<?php
namespace App\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
// Udeyou OTP client for Laravel 10+ (HTTP client). The key stays in .env on the server.
class Udeyou
{
/** POST /otp/send. data: otp_id, to, expires_in, expires_at, credits_remaining (+ code with a test key). */
public function send(string $to, array $options = []): Response
{
return $this->http()->post('/otp/send', ['to' => $to] + $options + ['lang' => 'ar']);
}
/** POST /otp/verify. data: verified, otp_id, to, verified_at. */
public function verify(string $otpId, string $code): Response
{
return $this->http()->post('/otp/verify', ['otp_id' => $otpId, 'code' => $code]);
}
/** GET /balance. data: company, mode, credits. */
public function balance(): Response
{
return $this->http()->get('/balance');
}
private function http(): PendingRequest
{
return Http::baseUrl(config('services.udeyou.url'))
->withToken(config('services.udeyou.key'))
->acceptJson()
->timeout(20);
}
}
<?php
namespace App\Http\Controllers;
use App\Services\Udeyou;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class OtpController extends Controller
{
public function __construct(private Udeyou $udeyou) {}
// POST /otp/send (email)
public function send(Request $request): RedirectResponse
{
$data = $request->validate(['email' => ['required', 'email', 'max:190']]);
try {
$res = $this->udeyou->send($data['email']);
} catch (ConnectionException) {
return back()->withInput()->withErrors(['email' => 'The service is unavailable. Try again later.']);
}
if ($res->failed()) {
return back()->withInput()->withErrors(['email' => $this->message($res)]);
}
// Keep otp_id in the server session, never in a hidden form field.
$request->session()->put('otp_id', $res->json('data.otp_id'));
return back()->with('status', 'We sent a code to your email.');
}
// POST /otp/verify (code)
public function verify(Request $request): RedirectResponse
{
$data = $request->validate(['code' => ['required', 'string', 'regex:/^[0-9A-Za-z]{4,10}$/']]);
$otpId = $request->session()->get('otp_id');
if (! $otpId) {
return back()->withErrors(['code' => 'Request a code first.']);
}
try {
$res = $this->udeyou->verify($otpId, $data['code']);
} catch (ConnectionException) {
return back()->withErrors(['code' => 'The service is unavailable. Try again later.']);
}
if ($res->failed()) {
return back()->withErrors(['code' => $this->message($res)]);
}
$request->session()->forget('otp_id');
// data.to is the verified address: sign the user in or mark the email as verified here.
$request->session()->put('verified_email', $res->json('data.to'));
return redirect()->intended('/');
}
private function message(Response $res): string
{
return match ($res->json('error.code')) {
'invalid_code' => 'Incorrect code. Attempts left: '.$res->json('error.attempts_remaining'),
'resend_too_soon', 'recipient_limit', 'rate_limited' => 'Please wait '.$res->json('error.retry_after').' seconds and try again.',
'otp_expired', 'otp_already_used', 'otp_not_found', 'too_many_attempts' => 'This code is no longer valid. Request a new one.',
'validation_error' => 'Check the email address and try again.',
// 401, 402, 403 and 5xx: log $res->json('error') and show a generic message.
default => 'The service is unavailable. Try again later.',
};
}
}
// routes/web.php (Route is already imported at the top of this file)
use App\Http\Controllers\OtpController;
Route::view('/otp', 'otp');
Route::post('/otp/send', [OtpController::class, 'send'])->middleware('throttle:5,1');
Route::post('/otp/verify', [OtpController::class, 'verify'])->middleware('throttle:10,1');
{{-- resources/views/otp.blade.php --}}
@if (session('status'))
<p>{{ session('status') }}</p>
@endif
<form method="POST" action="/otp/send">
@csrf
<input type="email" name="email" value="{{ old('email') }}" autocomplete="email" required>
<button type="submit">Send code</button>
@error('email') <p>{{ $message }}</p> @enderror
</form>
@if (session('otp_id'))
<form method="POST" action="/otp/verify">
@csrf
<input name="code" inputmode="numeric" autocomplete="one-time-code" required>
<button type="submit">Verify</button>
@error('code') <p>{{ $message }}</p> @enderror
</form>
@endif
// Udeyou OTP - Node.js 18+ (built-in fetch, no dependencies)
// Usage: UDEYOU_API_KEY=udy_test_... node udeyou.mjs user@example.com
import { createInterface } from 'node:readline/promises';
const BASE_URL = (process.env.UDEYOU_BASE_URL ?? 'https://udeyou.com/api/v1').replace(/\/+$/, '');
const API_KEY = process.env.UDEYOU_API_KEY;
class UdeyouError extends Error {
constructor(status, error = {}) {
super(error.message ?? `HTTP ${status}`);
this.status = status;
this.code = error.code ?? `http_${status}`;
this.retryAfter = error.retry_after; // 429 responses
this.attemptsRemaining = error.attempts_remaining; // invalid_code
this.fields = error.fields; // validation_error
}
}
async function udeyou(method, path, body) {
let res;
try {
res = await fetch(BASE_URL + path, {
method,
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(20_000),
});
} catch (err) {
// Timeout, DNS or connection failure
throw new UdeyouError(0, { code: 'network_error', message: err.message });
}
const json = await res.json().catch(() => ({}));
if (!res.ok || json.success !== true) throw new UdeyouError(res.status, json.error ?? {});
return json.data;
}
const sendOtp = (to, options = {}) => udeyou('POST', '/otp/send', { to, ...options });
const verifyOtp = (otpId, code) => udeyou('POST', '/otp/verify', { otp_id: otpId, code });
const getBalance = () => udeyou('GET', '/balance');
// 1) Send the code
const to = process.argv[2] ?? 'user@example.com';
try {
const sent = await sendOtp(to, { lang: 'ar' });
console.log(`Sent ${sent.otp_id} to ${sent.to} (expires in ${sent.expires_in} s)`);
// 2) Verify the code the user typed. Test keys (udy_test_) return it in the response.
let code = sent.code;
if (!code) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
code = (await rl.question('Code: ')).trim();
rl.close();
}
const result = await verifyOtp(sent.otp_id, code);
console.log(`Verified ${result.to} at ${result.verified_at}`);
} catch (err) {
if (!(err instanceof UdeyouError)) throw err;
console.error(`Error ${err.status} ${err.code}: ${err.message}`);
if (err.retryAfter) console.error(`Retry after ${err.retryAfter} s`);
if (err.attemptsRemaining !== undefined) console.error(`Attempts remaining: ${err.attemptsRemaining}`);
if (err.fields) console.error(err.fields);
process.exitCode = 1;
}
"""Udeyou OTP - Python 3.8+ with requests (pip install requests).
Usage: UDEYOU_API_KEY=udy_test_... python udeyou.py user@example.com
"""
import os
import sys
import requests
BASE_URL = os.environ.get("UDEYOU_BASE_URL", "https://udeyou.com/api/v1").rstrip("/")
API_KEY = os.environ.get("UDEYOU_API_KEY", "")
class UdeyouError(Exception):
def __init__(self, status, error):
super().__init__(error.get("message", f"HTTP {status}"))
self.status = status
self.code = error.get("code", f"http_{status}")
self.retry_after = error.get("retry_after") # 429 responses
self.attempts_remaining = error.get("attempts_remaining") # invalid_code
self.fields = error.get("fields") # validation_error
def udeyou(method, path, body=None):
try:
res = requests.request(
method,
BASE_URL + path,
json=body,
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=20,
)
except requests.RequestException as e: # timeout, DNS or connection failure
raise UdeyouError(0, {"code": "network_error", "message": str(e)}) from e
try:
payload = res.json()
except ValueError:
payload = {}
if not res.ok or payload.get("success") is not True:
raise UdeyouError(res.status_code, payload.get("error") or {})
return payload["data"]
def send_otp(to, **options):
return udeyou("POST", "/otp/send", {"to": to, **options})
def verify_otp(otp_id, code):
return udeyou("POST", "/otp/verify", {"otp_id": otp_id, "code": code})
def get_balance():
return udeyou("GET", "/balance")
if __name__ == "__main__":
to = sys.argv[1] if len(sys.argv) > 1 else "user@example.com"
try:
# 1) Send the code
sent = send_otp(to, lang="ar")
print(f"Sent {sent['otp_id']} to {sent['to']} (expires in {sent['expires_in']} s)")
# 2) Verify the code the user typed. Test keys (udy_test_) return it in the response.
code = sent.get("code") or input("Code: ").strip()
result = verify_otp(sent["otp_id"], code)
print(f"Verified {result['to']} at {result['verified_at']}")
except UdeyouError as e:
print(f"Error {e.status} {e.code}: {e}", file=sys.stderr)
if e.retry_after:
print(f"Retry after {e.retry_after} s", file=sys.stderr)
if e.attempts_remaining is not None:
print(f"Attempts remaining: {e.attempts_remaining}", file=sys.stderr)
if e.fields:
print(e.fields, file=sys.stderr)
sys.exit(1)
// Udeyou OTP - Go 1.18+ (standard library only)
// Usage: UDEYOU_API_KEY=udy_test_... go run udeyou.go user@example.com
package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
var (
baseURL = strings.TrimRight(envOr("UDEYOU_BASE_URL", "https://udeyou.com/api/v1"), "/")
apiKey = os.Getenv("UDEYOU_API_KEY")
client = &http.Client{Timeout: 20 * time.Second}
)
// APIError is the "error" object of a failed response.
type APIError struct {
Status int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
RetryAfter int `json:"retry_after,omitempty"` // 429 responses
AttemptsRemaining *int `json:"attempts_remaining,omitempty"` // invalid_code
Fields map[string]string `json:"fields,omitempty"` // validation_error
}
func (e *APIError) Error() string {
return fmt.Sprintf("%d %v: %v", e.Status, e.Code, e.Message)
}
type SendResult struct {
OtpID string `json:"otp_id"`
To string `json:"to"`
Status string `json:"status"`
Mode string `json:"mode"`
ExpiresIn int `json:"expires_in"`
ExpiresAt string `json:"expires_at"`
CreditsRemaining int `json:"credits_remaining"`
TestMode bool `json:"test_mode"` // test keys only
Code string `json:"code"` // test keys only
}
type VerifyResult struct {
Verified bool `json:"verified"`
OtpID string `json:"otp_id"`
To string `json:"to"`
VerifiedAt string `json:"verified_at"`
}
func udeyou(method, path string, body, out any) error {
var payload io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
payload = bytes.NewReader(b)
}
req, err := http.NewRequest(method, baseURL+path, payload)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var envelope struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
Error *APIError `json:"error"`
}
decodeErr := json.NewDecoder(res.Body).Decode(&envelope)
if decodeErr != nil || res.StatusCode >= 400 || !envelope.Success {
apiErr := envelope.Error
if apiErr == nil {
apiErr = &APIError{Code: fmt.Sprintf("http_%d", res.StatusCode), Message: "Unexpected response"}
}
apiErr.Status = res.StatusCode
return apiErr
}
return json.Unmarshal(envelope.Data, out)
}
func sendOtp(to string) (SendResult, error) {
var r SendResult
err := udeyou("POST", "/otp/send", map[string]any{"to": to, "lang": "ar"}, &r)
return r, err
}
func verifyOtp(otpID, code string) (VerifyResult, error) {
var r VerifyResult
err := udeyou("POST", "/otp/verify", map[string]string{"otp_id": otpID, "code": code}, &r)
return r, err
}
func main() {
to := "user@example.com"
if len(os.Args) > 1 {
to = os.Args[1]
}
// 1) Send the code
sent, err := sendOtp(to)
if err != nil {
fail(err)
}
fmt.Printf("Sent %v to %v (expires in %d s)\n", sent.OtpID, sent.To, sent.ExpiresIn)
// 2) Verify the code the user typed. Test keys (udy_test_) return it in the response.
code := sent.Code
if code == "" {
fmt.Print("Code: ")
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
code = strings.TrimSpace(line)
}
result, err := verifyOtp(sent.OtpID, code)
if err != nil {
fail(err)
}
fmt.Printf("Verified %v at %v\n", result.To, result.VerifiedAt)
}
func fail(err error) {
var apiErr *APIError
if errors.As(err, &apiErr) {
fmt.Fprintf(os.Stderr, "Error %v\n", apiErr)
if apiErr.RetryAfter > 0 {
fmt.Fprintf(os.Stderr, "Retry after %d s\n", apiErr.RetryAfter)
}
if apiErr.AttemptsRemaining != nil {
fmt.Fprintf(os.Stderr, "Attempts remaining: %d\n", *apiErr.AttemptsRemaining)
}
if len(apiErr.Fields) > 0 {
fmt.Fprintln(os.Stderr, apiErr.Fields)
}
} else {
fmt.Fprintln(os.Stderr, "Error:", err)
}
os.Exit(1)
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
name: otp_backend
description: Udeyou OTP client and a sample shelf backend for Flutter apps.
publish_to: none
environment:
sdk: ^3.0.0
dependencies:
http: ^1.2.0
shelf: ^1.4.0
shelf_router: ^1.1.0
dev_dependencies:
lints: ^5.0.0
// Udeyou OTP client for Dart servers (package:http).
// Keep the API key on the server. Never ship it inside a mobile or web app.
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
class UdeyouException implements Exception {
UdeyouException(this.statusCode, this.code, this.message, [this.details = const {}]);
final int statusCode;
final String code;
final String message;
/// Extra fields such as retry_after, attempts_remaining or fields.
final Map<String, dynamic> details;
int? get retryAfter => details['retry_after'] as int?;
int? get attemptsRemaining => details['attempts_remaining'] as int?;
String toString() => 'UdeyouException($statusCode $code: $message)';
}
class UdeyouClient {
UdeyouClient({
required this.apiKey,
this.baseUrl = 'https://udeyou.com/api/v1',
http.Client? httpClient,
}) : _http = httpClient ?? http.Client();
final String apiKey;
final String baseUrl;
final http.Client _http;
static const _timeout = Duration(seconds: 20);
/// Sends a code to [to]. Returns otp_id, expires_in, expires_at, credits_remaining
/// (and code when using a udy_test_ key).
Future<Map<String, dynamic>> sendOtp(
String to, {
String lang = 'ar',
String? senderName,
int? codeLength,
int? expiresIn,
}) {
return _request('POST', '/otp/send', {
'to': to,
'lang': lang,
if (senderName != null) 'sender_name': senderName,
if (codeLength != null) 'code_length': codeLength,
if (expiresIn != null) 'expires_in': expiresIn,
});
}
/// Verifies [code]. Returns verified, otp_id, to and verified_at.
Future<Map<String, dynamic>> verifyOtp({required String otpId, required String code}) {
return _request('POST', '/otp/verify', {'otp_id': otpId, 'code': code});
}
/// Returns company, mode and credits.
Future<Map<String, dynamic>> balance() => _request('GET', '/balance');
void close() => _http.close();
Future<Map<String, dynamic>> _request(String method, String path,
[Map<String, Object>? body]) async {
final uri = Uri.parse('$baseUrl$path');
final headers = {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
final http.Response res;
try {
res = await (method == 'GET'
? _http.get(uri, headers: headers)
: _http.post(uri, headers: headers, body: jsonEncode(body)))
.timeout(_timeout);
} on TimeoutException {
throw UdeyouException(0, 'timeout', 'No response within ${_timeout.inSeconds} s');
}
Map<String, dynamic> json;
try {
json = jsonDecode(res.body) as Map<String, dynamic>;
} on FormatException {
json = const {};
}
if (res.statusCode >= 400 || json['success'] != true) {
final error = Map<String, dynamic>.from(json['error'] as Map? ?? const {});
throw UdeyouException(
res.statusCode,
error.remove('code') as String? ?? 'http_${res.statusCode}',
error.remove('message') as String? ?? 'Unexpected response',
error,
);
}
return json['data'] as Map<String, dynamic>;
}
}
// Usage: UDEYOU_API_KEY=udy_test_... dart run bin/demo.dart user@example.com
import 'dart:io';
import 'package:otp_backend/udeyou_client.dart';
Future<void> main(List<String> args) async {
final env = Platform.environment;
final udeyou = UdeyouClient(
apiKey: env['UDEYOU_API_KEY'] ?? '',
baseUrl: env['UDEYOU_BASE_URL'] ?? 'https://udeyou.com/api/v1',
);
final to = args.isNotEmpty ? args.first : 'user@example.com';
try {
// 1) Send the code
final sent = await udeyou.sendOtp(to);
print('Sent ${sent['otp_id']} to ${sent['to']} (expires in ${sent['expires_in']} s)');
// 2) Verify the code the user typed. Test keys (udy_test_) return it in the response.
var code = sent['code'] as String?;
if (code == null) {
stdout.write('Code: ');
code = stdin.readLineSync()?.trim() ?? '';
}
final result = await udeyou.verifyOtp(otpId: sent['otp_id'] as String, code: code);
print('Verified ${result['to']} at ${result['verified_at']}');
} on UdeyouException catch (e) {
stderr.writeln('Error ${e.statusCode} ${e.code}: ${e.message}');
if (e.details.isNotEmpty) stderr.writeln(e.details);
exitCode = 1;
} finally {
udeyou.close();
}
}
// Your backend: the Flutter app calls these routes, and only this server holds the Udeyou key.
// Usage: UDEYOU_API_KEY=udy_live_... dart run bin/server.dart
import 'dart:convert';
import 'dart:io';
import 'package:otp_backend/udeyou_client.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
final udeyou = UdeyouClient(
apiKey: Platform.environment['UDEYOU_API_KEY']!,
baseUrl: Platform.environment['UDEYOU_BASE_URL'] ?? 'https://udeyou.com/api/v1',
);
Future<void> main() async {
final router = Router()
// {email} -> {otp_id, expires_in}
..post('/auth/otp/send', (Request req) async {
final body = await _readJson(req);
final sent = await udeyou.sendOtp('${body['email'] ?? ''}'.trim());
return _json(200, {'otp_id': sent['otp_id'], 'expires_in': sent['expires_in']});
})
// {otp_id, code} -> {verified}
..post('/auth/otp/verify', (Request req) async {
final body = await _readJson(req);
final result = await udeyou.verifyOtp(
otpId: '${body['otp_id'] ?? ''}',
code: '${body['code'] ?? ''}',
);
// result['to'] is the verified email: sign the user in or mark the address as verified here.
return _json(200, {'verified': result['verified'] == true});
});
final handler = const Pipeline().addMiddleware(_errors()).addHandler(router.call);
final server = await io.serve(
handler, InternetAddress.anyIPv4, int.parse(Platform.environment['PORT'] ?? '8081'));
print('Listening on http://${server.address.host}:${server.port}');
}
Future<Map<String, dynamic>> _readJson(Request req) async {
final decoded = jsonDecode(await req.readAsString());
if (decoded is Map<String, dynamic>) return decoded;
throw const FormatException('Expected a JSON object');
}
Response _json(int status, Map<String, Object?> body) =>
Response(status, body: jsonEncode(body), headers: {'Content-Type': 'application/json'});
Middleware _errors() => (inner) => (req) async {
try {
return await inner(req);
} on FormatException {
return _json(400, {'error': 'invalid_json', 'message': 'Send a JSON object.'});
} on UdeyouException catch (e) {
// A bad key, an empty balance or an outage is our problem: log it, do not expose it.
if (e.statusCode == 0 ||
e.statusCode >= 500 ||
const {401, 402, 403}.contains(e.statusCode)) {
stderr.writeln(e);
return _json(503, {'error': 'service_unavailable', 'message': 'Try again later.'});
}
return _json(e.statusCode, {
'error': e.code,
'message': e.message,
if (e.retryAfter != null) 'retry_after': e.retryAfter,
if (e.attemptsRemaining != null) 'attempts_remaining': e.attemptsRemaining,
if (e.details['fields'] != null) 'fields': e.details['fields'],
});
}
};
التطبيق يتصل بخادمك، لا بـ Udeyou
لا يحمل تطبيق Flutter مفتاح Udeyou. يرسل التطبيق الطلبات إلى خادمك عبر المسارين التاليين، وخادمك يتصل بـ Udeyou. يوفّر الملف في تبويب Dart هذين المسارين. ضع رابط خادمك بدل https://api.example.com في lib/main.dart.
| مسار خادمك | الجسم | الرد عند النجاح |
|---|---|---|
POST /auth/otp/send | {"email": "..."} | {"otp_id": "...", "expires_in": 300} |
POST /auth/otp/verify | {"otp_id": "...", "code": "..."} | {"verified": true} |
عند الخطأ يعيد خادمك حالة Udeyou نفسها مع error وmessage والحقول الإضافية، أو 503 service_unavailable إذا كانت المشكلة في المفتاح أو الرصيد أو الخدمة.
// Flutter app side: talks to YOUR backend only. The Udeyou key never ships in the app.
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
class OtpApiException implements Exception {
OtpApiException(
this.code,
this.message, {
this.retryAfter,
this.attemptsRemaining,
});
final String code;
final String message;
final int? retryAfter;
final int? attemptsRemaining;
String toString() => message;
}
class OtpApi {
OtpApi(this.baseUrl, {http.Client? client}) : _http = client ?? http.Client();
/// Your backend, for example https://api.example.com
final String baseUrl;
final http.Client _http;
Future<({String otpId, int expiresIn})> sendCode(String email) async {
final data = await _post('/auth/otp/send', {'email': email});
return (
otpId: data['otp_id'] as String,
expiresIn: data['expires_in'] as int,
);
}
Future<bool> verifyCode(String otpId, String code) async {
final data = await _post('/auth/otp/verify', {
'otp_id': otpId,
'code': code,
});
return data['verified'] == true;
}
Future<Map<String, dynamic>> _post(
String path,
Map<String, String> body,
) async {
final http.Response res;
try {
res = await _http
.post(
Uri.parse('$baseUrl$path'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
)
.timeout(const Duration(seconds: 20));
} on TimeoutException {
throw OtpApiException('timeout', 'The server took too long to respond.');
}
final Object? json;
try {
json = jsonDecode(res.body);
} on FormatException {
throw OtpApiException(
'bad_response',
'Unexpected server response (${res.statusCode}).',
);
}
final data = json is Map<String, dynamic> ? json : <String, dynamic>{};
if (res.statusCode != 200) {
throw OtpApiException(
data['error'] as String? ?? 'http_${res.statusCode}',
data['message'] as String? ?? 'Request failed (${res.statusCode}).',
retryAfter: data['retry_after'] as int?,
attemptsRemaining: data['attempts_remaining'] as int?,
);
}
return data;
}
}
import 'package:flutter/material.dart';
import 'otp_api.dart';
class OtpScreen extends StatefulWidget {
const OtpScreen({super.key, required this.api, required this.onVerified});
final OtpApi api;
final ValueChanged<String> onVerified; // receives the verified email
State<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends State<OtpScreen> {
final _email = TextEditingController();
final _code = TextEditingController();
String? _otpId;
bool _loading = false;
String? _error;
void dispose() {
_email.dispose();
_code.dispose();
super.dispose();
}
Future<void> _run(Future<void> Function() action) async {
setState(() {
_loading = true;
_error = null;
});
try {
await action();
} on OtpApiException catch (e) {
_error = switch (e.code) {
'invalid_code' =>
'Incorrect code. ${e.attemptsRemaining ?? 0} attempts left.',
'resend_too_soon' || 'recipient_limit' || 'rate_limited' =>
'Please wait ${e.retryAfter ?? 60} seconds and try again.',
'otp_expired' || 'too_many_attempts' =>
'This code is no longer valid. Request a new one.',
_ => e.message,
};
} catch (_) {
_error = 'Could not reach the server. Check your connection.';
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _send() => _run(() async {
final sent = await widget.api.sendCode(_email.text.trim());
_code.clear();
_otpId = sent.otpId;
});
Future<void> _verify() => _run(() async {
if (await widget.api.verifyCode(_otpId!, _code.text.trim())) {
widget.onVerified(_email.text.trim());
}
});
Widget build(BuildContext context) {
final codeSent = _otpId != null;
return Scaffold(
appBar: AppBar(title: const Text('Sign in')),
body: ListView(
padding: const EdgeInsets.all(24),
children: [
TextField(
controller: _email,
enabled: !_loading,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: _loading ? null : _send,
child: Text(codeSent ? 'Resend code' : 'Send code'),
),
if (codeSent) ...[
const SizedBox(height: 24),
TextField(
controller: _code,
enabled: !_loading,
keyboardType: TextInputType.number,
autofillHints: const [AutofillHints.oneTimeCode],
maxLength: 6,
textAlign: TextAlign.center,
decoration: const InputDecoration(
labelText: '6-digit code',
counterText: '',
),
onSubmitted: (_) => _verify(),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _loading ? null : _verify,
child: const Text('Verify'),
),
],
if (_loading)
const Padding(
padding: EdgeInsets.only(top: 16),
child: Center(child: CircularProgressIndicator()),
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'otp_api.dart';
import 'otp_screen.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) => OtpScreen(
api: OtpApi('https://api.example.com'),
onVerified: (email) =>
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Verified $email'))),
),
),
);
}
}
الربط
أفضل الممارسات
قائمة مراجعة قبل الإطلاق.
- احفظ المفتاح في متغير بيئة على الخادم، ولا تضعه في المتصفح أو تطبيق الجوال أو مستودع الكود.
- استخدم المفتاح التجريبي في التطوير والاختبار الآلي، والمفتاح الحي في الإنتاج فقط.
- احفظ
otp_idفي جلسة المستخدم على الخادم، وتحقق به بدلًا من البريد. - اعتمد
data.toمن رد التحقق بريدًا موثّقًا، لا البريد المرسل من التطبيق. - تعامل مع الرمز كنص، فقد يبدأ بصفر.
- اعرض للمستخدم المحاولات المتبقية من
attempts_remaining، وعطّل زر إعادة الإرسال 60 ثانية بعد كل إرسال. - اضبط مهلة لكل طلب، وتعامل مع أخطاء الشبكة دون عرض تفاصيلها للمستخدم.
- ألغِ من لوحة التحكم أي مفتاح يُشتبه في تسرّبه، ثم أنشئ مفتاحًا جديدًا.
- لا تسجّل الرموز أو المفاتيح في سجلات تطبيقك.
- عند
429انتظر مدةretry_afterبالثواني قبل إعادة المحاولة.
تحتاج مساعدة في الربط؟
راسل فريق الدعم من نموذج التواصل، وأرفق otp_id وكود الخطأ إن وُجد. نرد بالعربية خلال 24 ساعة عمل.