ضَع الحصّةَ في منصّتك — بالطريقة التي تناسبك

من رابطٍ تُرسله في واتساب إلى واجهةٍ برمجيّةٍ كاملة. ابدأ بالأسهل، وانتقل متى احتجت — فالغرفةُ نفسُها والأدواتُ نفسُها في كلّ الطرق، وكلُّ ميزةٍ مفتاحٌ تضبطه مؤسّستك.

٢

تضمين في أيّ صفحة

Wix · Squarespace · Google Sites · Blogger · كتلةُ HTML في Moodle · أيُّ موقعٍ يقبل كود HTML.

خذ رابطَ الطلّاب من الطريقة ١ وضعه في إطار. هذا كلُّ شيء — الحصّةُ داخل صفحتك بهويّة مؤسّستك.

<iframe src="رابط الطلّاب"
  allow="camera; microphone; display-capture; fullscreen; autoplay; screen-wake-lock"
  allowfullscreen style="width:100%;height:80vh;border:0;border-radius:12px"></iframe>
سطرُ allow ضروريّ: بدونه يمنع المتصفّحُ الميكروفونَ والكاميرا داخل الإطار. وبعضُ منشئي المواقع يضع الكود في إطارٍ ثانٍ يحذف هذا السطر — إن لم يعمل الميكروفون فضع رابطًا يفتح الحصّةَ في صفحتها بدل الإطار.
٣

ووردبريس

أعضاءُ موقعك يدخلون بحساباتهم — بأسمائهم، والمحرّرون والمديرون معلّمون.
  1. نزّل الإضافة وارفعها: الإضافات ← أضف جديدًا ← ارفع إضافة.
  2. الإعدادات ← حَلْقة: الصق سرّ مؤسّستك (يُحفظ في خادمك ولا يظهر في أيّ صفحة).
  3. ضع في أيّ صفحة أو درس:
[halqa room="halaqa-fajr"]

// والزوّارُ بلا حساب يدخلون أيضًا (يكتبون أسماءهم):
[halqa room="halaqa-fajr" guests="yes"]
نزّل إضافة ووردبريس ↓

من المعلّم؟

من يملك صلاحيّة «تحرير مقالات الآخرين» أو halqa_teach. ولتحديده بنفسك (مع LearnDash أو Tutor LMS مثلًا):

add_filter('halqa_role', function ($role, $user) {
  return in_array('tutor_instructor', $user->roles) ? 'host' : $role;
}, 10, 2);
٤

من خادمك — الواجهة البرمجيّة

لمنصّةٍ لها حساباتُها: طلّابك بأسمائهم، ومعلّموك معلّمون، ومشرفوك وأولياءُ الأمور بأدوارهم.

نداءان من خادمك — والسرُّ لا يغادره أبدًا: (١) غرفةٌ للحصّة بمعرّفها عندك، (٢) رمزُ دخولٍ لهذا المستخدم بدوره. ثمّ اعرض الغرفةَ في إطارٍ أو صفحة. ونفسُ المعرّف يُعيد الغرفةَ نفسَها، فلا تُخزّن شيئًا.

const H = { Authorization: `Bearer ${process.env.HALQA_SECRET}`, "Content-Type": "application/json" };
const post = (p, b) => fetch("https://api.halqa.live" + p, { method: "POST", headers: H, body: JSON.stringify(b) }).then((r) => r.json());

export async function halqaUrl({ lessonId, classId, user, role }) {
  const room = await post("/v1/rooms", { externalId: `lesson-${lessonId}`, classKey: `class-${classId}` });
  const { token } = await post("/v1/tokens", { roomId: room.roomId, identity: user.id, name: user.name, role });
  return `https://api.halqa.live/room#t=${token}`;   // role: host · cohost · participant · supervisor · observer
}
function halqa_post($path, $body) {
  $ch = curl_init("https://api.halqa.live" . $path);
  curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("HALQA_SECRET"), "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode($body)]);
  return json_decode(curl_exec($ch), true);
}
$room  = halqa_post("/v1/rooms",  ["externalId" => "lesson-$lessonId", "classKey" => "class-$classId"]);
$token = halqa_post("/v1/tokens", ["roomId" => $room["roomId"], "identity" => $userId,
                                   "name" => $userName, "role" => $isTeacher ? "host" : "participant"])["token"];
$url = "https://api.halqa.live/room#t=" . $token;
import os, requests
H = {"Authorization": f"Bearer {os.environ['HALQA_SECRET']}"}
def post(p, b): return requests.post("https://api.halqa.live" + p, json=b, headers=H, timeout=10).json()

def halqa_url(lesson_id, class_id, user, role):
    room = post("/v1/rooms", {"externalId": f"lesson-{lesson_id}", "classKey": f"class-{class_id}"})
    tok = post("/v1/tokens", {"roomId": room["roomId"], "identity": str(user.id), "name": user.name, "role": role})
    return "https://api.halqa.live/room#t=" + tok["token"]
curl -X POST https://api.halqa.live/v1/rooms -H "Authorization: Bearer $HALQA_SECRET" \
  -H "Content-Type: application/json" -d '{"externalId":"lesson-42","classKey":"class-7"}'
# ← { "roomId": "cm…" }
curl -X POST https://api.halqa.live/v1/tokens -H "Authorization: Bearer $HALQA_SECRET" \
  -H "Content-Type: application/json" -d '{"roomId":"cm…","identity":"u-9","name":"سارة","role":"participant"}'
# ← { "token": "…" }   ثمّ:  https://api.halqa.live/room#t=<token>

داخل إطارٍ في صفحتك

أضف &exit= برابط صفحتك للعودة بعد الخروج، واستمع لحدث الخروج:

addEventListener("message", (e) => {
  if (e.origin !== "https://api.halqa.live") return;
  if (e.data?.type === "halqa:left")  location.href = "/my-lessons";
  if (e.data?.type === "halqa:time")  draw(e.data.endsAt, e.data.serverNow); // عدّادك وزرّ التمديد
  if (e.data?.type === "halqa:ended") done(e.data.reason);        // scheduled | host | admin
});

الحصّةُ المحجوزة: بدايةٌ ونهايةٌ وتمديد

إن كنتَ تبيع الحصّةَ كتلةَ دقائق، أرسِل موعدَها عند الإنشاء. والموعدُ اختياريّ — غرفةٌ بلا موعدٍ تعمل كما كانت: يفتحها المعلّم ويغلقها.

POST /v1/rooms  { "externalId": "lesson-42", "startsAt": "2026-11-02T17:00:00Z", "endsAt": "2026-11-02T17:45:00Z" }
// ← { roomId, startsAt, endsAt, closesAt, serverNow }
ما يحدثمتى
يُفتح الباب ويُصدَر الرمزقبل startsAt بعشر دقائق — وقبلها يُرَدّ النداءُ بـ425
سطرٌ هادئ «بقيت خمس دقائق» داخل الغرفةقبل النهاية بخمس
تنتهي الحصّةُ للجميع وتُغلق الغرفةعند closesAt = endsAt + دقائق السماح
الرمزُ يموتعند closesAt نفسِه — فرابطٌ تسرَّب يموت مع غرفته

النداءُ ثانيةً بنفس externalId يُعيد الغرفةَ نفسَها، وبنهايةٍ أبعد يُحرّك النهاية. أو صراحةً: POST /v1/rooms/{roomId}/extend {"minutes":15} — يُعيد endsAt الجديد. والقرارُ كلُّه عندك: نحن لا نُسعّر ولا نُحصّل ولا نَعُدّ بالثانية.

دقائقُ السماح إعدادُ مؤسّسةٍ (افتراضًا خمس، لا تُحسب على أحد) — اطلب تغييرها لمؤسّستك.

خطّافاتُ الحضور

اضبط لنا webhookUrl وwebhookSecret لمؤسّستك، فتصلك ثلاثةُ أحداث: room.started وparticipant.joined وparticipant.left وroom.ended (ومعه reason: scheduled · host · admin) وreport.ready وrecording.ready. ومع كلٍّ منها externalId وclassKey، ومع أحداث المشارك metadata. تُستعمَل لغياب المعلّم وردّ المال وسجلِّ الحضور عند النزاع — لا للمحاسبة بالثانية (تلك في /v1/usage).

// التحقّقُ من التوقيع — رُدَّ ما مضى عليه خمس دقائق
const body = await req.text();
const ts = req.headers.get("X-Halqa-Timestamp");
const want = "sha256=" + crypto.createHmac("sha256", SECRET).update(ts + "." + body).digest("hex");
if (want !== req.headers.get("X-Halqa-Signature")) return new Response("bad sig", { status: 401 });

ثلاثُ محاولاتٍ عند الفشل، ومهلةُ خمسِ ثوانٍ لكلٍّ منها — وخادمٌ نائمٌ عندك لا يُوقف صفًّا يدرس.

بياناتُك عن الداخل — تعود كما هي

أرسِل مع الرمز metadata (أيُّ كائنٍ حتّى ٢٠٠٠ حرف)، فتعود كما هي في GET /v1/reports وفي كلّ خطّافٍ عن صاحبها. بها تربط الحضورَ بسجلّاتك بلا مطابقةٍ بالأسماء (و«محمد» ثلاثةٌ في الصفّ الواحد). ولا نُفسّرها ولا نبحث فيها — كـclassKey سواءً بسواء.

POST /v1/tokens { "roomId": "…", "identity": "u-9", "role": "participant",
  "metadata": { "studentId": "s-9931", "book": "qiraa-3", "unit": 7 } }
⚠️ metadata تسكن في الرمز الموقَّع ويقرؤها متصفّحُ صاحبه — فضَع معرّفاتٍ لا أسرارًا.

الرمزُ ينتهي والحصّةُ لم تنته

عمرُ الرمز ساعة. فإن طالت حصّتُك، تطلب الغرفةُ منك رمزًا جديدًا قبل انتهائه بخمس دقائق، وترُدّ أنت برمزٍ جديد — فلا ينقطع أحد:

if (e.data?.type === "halqa:token-expiring") {
  const { token } = await post("/v1/tokens", { roomId: e.data.roomId, identity: e.data.identity, role });
  frame.contentWindow.postMessage({ type: "halqa:token", token }, "https://api.halqa.live");
}

المقبسُ القائم يصمد بعد انتهاء الرمز، لكنّ أوّلَ انقطاعٍ بعده لا عودةَ منه — وهنا يقع الضرر.

مَن يُضمّن غرفَك

نضع frame-ancestors على صفحة الغرفة، فلا يُضمّنها إلّا نطاقاتُك التي سجّلناها لك. أعطِنا نطاقاتِك (https://x.com https://*.x.com) قبل الإطلاق، وإلّا ظهرت الغرفةُ فارغةً داخل صفحتك. ومررّ ?t=<publicKey> في عنوان الإطار لتُقصَر القائمةُ على نطاقاتك وحدَها.

نطاقُ التطوير عندك (http://localhost:3000) نسجّله لك، لكنّه يعمل بـ?t=<publicKey> وحدَه: صفحةُ الغرفة ساكنةٌ تُخدَم بقائمةٍ واحدةٍ لكلّ عملائنا، ولو دخلها جهازُ مبرمجٍ عندك لصار يُضمّن غرفَ غيرك. فضَع ?t= في إطارك دائمًا.

ما يبقى بعد الحصّة

النداءما يُعيده
GET /v1/recordings?externalId=lesson-42تسجيلاتُ الحصّة (إن أتحتَ التسجيل)
POST /v1/recordings/:id/linkرابطُ مشاهدةٍ قصيرُ العمر لطالبٍ أو وليّ أمر
GET /v1/reports?externalId=lesson-42تقريرُ الحصّة: الحضور، ومن سمّع وأخطاؤه، والواجب، والاختبارات
GET /v1/students/u-9?classKey=class-7دفترُ تسميع الطالب في صفّه: آخرُ تسميع، وخطؤه المعتاد، وواجبه — لملفّ الطالب عندك
POST /v1/console-linkلوحةُ منصّتك للقراءة؛ و{"manage":true} لرابط إدارةٍ يُنشئ روابط الدعوة أيضًا
GET /v1/usageالاستهلاكُ اليوميّ للفوترة
classKey معرّفُ «الصفّ» عندك (حلقةٌ، أو معلّمٌ وطالب) — به تتبع ملاحظاتُ المعلّم ودفترُ التسميع الصفَّ من حصّةٍ إلى حصّة. اختياريّ، وأنت تقرّر معناه.
٥

نظامُك الخاصّ — طلبٌ خاصّ

تطبيقُ جوّال، نظامٌ داخليّ للمدرسة، منصّةٌ بُنيت خصّيصًا، أو أيُّ شيءٍ لا تجده أعلاه.

أخبرنا بما تستعمل وكيف يدخل طلّابك اليوم، ونقترح عليك أقصرَ طريق — غالبًا يكفي واحدٌ من الأربعة أعلاه بتعديلٍ صغير، وإلّا بنيناه معك. ولا نَعِدُ بما لم نبنِه: نقول لك ما يعمل اليوم وما يحتاج وقتًا.

اطلب دمجًا خاصًّا

أيُّ طريقٍ لك؟

إن كنت…ابدأ بـالطلّاب يدخلون بـ
معلّمًا أو مركزًا صغيرًا بلا موقع١ · روابطاسمٍ يكتبونه
لك موقعٌ على Wix أو Google Sites٢ · تضميناسمٍ يكتبونه
لك موقعُ ووردبريس بعضويّات٣ · الإضافةحساباتهم في موقعك
منصّةً بحساباتٍ وأدوار (أولياءُ أمورٍ ومشرفون)٤ · الواجهة البرمجيّةحساباتهم وأدوارهم
مدرسةً أو جامعةً على Moodle أو Canvas٢ · كتلةُ HTML في المقرّراسمٍ يكتبونه
لك تطبيقٌ أو نظامٌ لا تجده هنا٥ · طلبٌ خاصّما نتّفق عليه

وفي كلّ الطرق: هويّةُ مؤسّستك (الشعارُ والألوان)، وكلُّ أداةٍ مفتاحٌ تُشعله أو تطفئه. تواصل معنا للحصول على سرّ مؤسّستك.