دليل التكامل

العقد الكامل والمحدَّث للتكامل — الوثيقة نفسها التي بُنيت عليها أولى عمليات النشر. وسمَا سكربت في الواجهة، وثلاث استدعاءات REST من خادمك.

آخر مزامنة: 11 أغسطس 2026 الدليل منشور بالإنجليزية؛ الترجمة العربية قريبًا.

Integration Guide (for the host app team)

This guide is language-agnostic. Your backend can be Node, Python, PHP, Ruby, Java, C#, Go, etc. — you only need to do two things:

A. Embed the chat widget and tell it who the logged-in student is (authenticated). B. Send us your students and behavior metrics over a simple HTTPS API.

You will receive these values from us:

ValueWhere it’s used
CHAT_HOST (e.g. https://abwaab.waselchat.com)Widget embed
GLOBAL_NAME (e.g. abwaabChat)Widget embed — your deployment’s window.<GLOBAL_NAME> settings object
INBOX_HMAC_KEY (secret)Computing the identity hash (backend only)
INGEST_BASE_URLData API
API_KEY (secret)Data API auth

Keep INBOX_HMAC_KEY and API_KEY on your server only. Never put them in frontend code, mobile bundles, or a public repo.


Ready-to-paste code for everything below lives in examples/ (host page + backend in TypeScript/PHP/Python), and npm run provision:integration renders it with your deployment’s real values filled in.

A. Embed + authenticate the widget

A.1 Add the widget (frontend)

Two ingredients, whatever your stack: a settings object on window.GLOBAL_NAME (who the student is), and our loader script (CHAT_HOST/embed.js), which must run after the settings exist. The loader creates an iframe on our origin — your CSS can’t affect the chat and the chat can’t leak into your page. There is no SDK and no build step.

In a single-page app (React, Vue, Svelte, … — the common case): load the script from a mount hook in a layout that survives navigation. examples/host-spa-react.tsx is a complete drop-in component; the core of it:

// In a layout that survives client-side navigation, when a student is signed in:
window.GLOBAL_NAME = { externalId, identifierHash /* from YOUR server — §A.2 */ };
const s = document.createElement("script");
s.src = "CHAT_HOST/embed.js";
s.defer = true;
document.body.appendChild(s);

The one rule behind this: a script tag must be created to execute. Markup a framework inserts during a client-side transition is added to the DOM but never runs — the widget then appears after a hard reload and not after a soft navigation, with no error anywhere. So use document.createElement (as above) or your framework’s script primitive (Next.js <Script src="…/embed.js" strategy="afterInteractive" />) — never raw tags in a template.

On a classic server-rendered page (full document load per navigation), the raw two-tag form works as-is — sample in examples/host-page.html:

<script>
  window.GLOBAL_NAME = {
    externalId: "STU-10432",        // your stable student id (same as §B)
    identifierHash: "9f2b…",        // computed on YOUR server — see A.2
    // Optional:
    // bottomOffset: 76,            // px to lift the launcher above your
    // bottomOffsetWide: 24,        //   fixed bottom chrome (narrow / ≥1024px)
    // side: "left",                // launcher side; default "right"
    // onUnread: function (n) {},   // unread-count callback for your own badge
  };
</script>
<script src="CHAT_HOST/embed.js" defer></script>

After a login without a page reload, re-identify with:

window.ApiName.identify({ externalId: "...", identifierHash: "..." });
// ApiName is the capitalized global we hand you, e.g. window.AbwaabChat

identify merges any settings keys, not just the identity pair — and that matters for onUnread: the loader reads the settings global once, when it first runs. A callback defined inline in the global works on a classic server-rendered page, but a framework host whose callback closes over component state should install it via the API instead:

window.ApiName.identify({ onUnread: (count) => setUnread(count) });

Logging out: there is no destroy(). The widget’s iframe is appended to document.body, outside your framework’s tree, so a client-side navigation to a signed-out page leaves the widget standing. End sessions with a full document load (e.g. a classic form POST answered with a redirect) — that is what removes the widget.

A.2 Compute the identity hash (backend)

The widget must know the student’s identity in a way that can’t be faked. That requires a signature (identifier_hash) computed on your backend with the secret INBOX_HMAC_KEY, then handed to your page for the logged-in user.

Backend: compute identifier_hash = HMAC_SHA256( identifier , INBOX_HMAC_KEY ) as a hex string, where identifier is your stable student ID (the same external_id you send us in section B — e.g. "STU-10432"). Expose it to your own frontend for the logged-in user (e.g. inject it server-side, or return it from an authed endpoint). Never expose INBOX_HMAC_KEY itself.

HMAC-SHA256 in common backends:

// Node.js
const crypto = require("crypto");
const identifier_hash = crypto
  .createHmac("sha256", INBOX_HMAC_KEY)
  .update(identifier)
  .digest("hex");
# Python
import hmac, hashlib
identifier_hash = hmac.new(
    INBOX_HMAC_KEY.encode(), identifier.encode(), hashlib.sha256
).hexdigest()
// PHP
$identifier_hash = hash_hmac('sha256', $identifier, $INBOX_HMAC_KEY);
# Ruby
identifier_hash = OpenSSL::HMAC.hexdigest("sha256", INBOX_HMAC_KEY, identifier)
// Java
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(INBOX_HMAC_KEY.getBytes("UTF-8"), "HmacSHA256"));
String identifier_hash = HexFormat.of().formatHex(mac.doFinal(identifier.getBytes("UTF-8")));
// C#
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(INBOX_HMAC_KEY));
var bytes = h.ComputeHash(Encoding.UTF8.GetBytes(identifier));
string identifier_hash = Convert.ToHexString(bytes).ToLowerInvariant();
// Go
m := hmac.New(sha256.New, []byte(INBOX_HMAC_KEY))
m.Write([]byte(identifier))
identifierHash := hex.EncodeToString(m.Sum(nil))

Expose the hash to your own frontend for the logged-in user (inject it server-side, or return it from an authed endpoint). Once the hash matches, the student is recognized and their conversation history is securely theirs — a wrong or missing hash gets no chat at all.

Students you haven’t registered yet: a valid hash is your server vouching for that student, so when the widget authenticates an external_id we have never seen, we create a minimal student automatically rather than show an empty box — they can message their mentor immediately. Their name and profile fill in the next time your backend calls POST /api/v1/students for them (§B.1 — the ordinary upsert, no special case), and per-course threads appear as your enrollment events arrive (§B.3). This is a safety net for pre-existing accounts and calls lost to an outage, not a replacement for §B — only your backend knows names and enrollments.

A.3 Domain / CORS

  • The widget is served from the CHAT_HOST we hand you (later, optionally a subdomain of your domain — the cutover is lossless because identity rides on external_id, not the host).
  • The data API (section B) is server-to-server, so CORS does not apply there.

B. Send us students + metrics (backend → our API)

Plain HTTPS POST with JSON. Auth header on every call: X-API-Key: API_KEY. Base URL: INGEST_BASE_URL.

B.1 Upsert a student — POST /api/v1/students

Call this when a student is created or their profile changes. external_id is your stable student ID and the key everything joins on.

curl -X POST "$INGEST_BASE_URL/api/v1/students" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "STU-10432",
    "name": "…",
    "email": "…",
    "phone": "…",
    "grade": "10",
    "learning_plan": "…",
    "enrolled_subjects": ["math", "english"],
    "custom": { "anything": "else" }
  }'

Response: { "id": "…", "chatwoot_contact_id": 501 }

Only external_id is required; send whatever else you have. Safe to call repeatedly (it upserts).

B.2 Send a metric / behavior event — POST /api/v1/metrics

Call this whenever something happens that a mentor might act on (a failed exam, a drop in activity, a missed session). We decide (via configured rules) whether it triggers an outreach.

curl -X POST "$INGEST_BASE_URL/api/v1/metrics" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "student_external_id": "STU-10432",
    "event_type": "exam_failed",
    "payload": { "subject": "math", "score": 41 },
    "idempotency_key": "exam-STU-10432-2026-07-20"
  }'

Response: { "id": "…", "triggered": true }

  • event_type is a short snake_case string — see the catalog below.
  • payload is free-form JSON — send whatever context is useful.
  • idempotency_key (recommended): a unique string per real event, so retries never double-send.

The event-type set is open by design. Any event_type you send is accepted and stored immediately — no schema change, no coordination, no deploy on either side. What an event does is decided by outreach rules, which are configuration rows an operator manages live in the console. So: emit generously, wire behavior later. Start from this catalog and extend freely (payload keys in parentheses are conventions the rules can match on):

Areaevent_typeTypical payload
Lifecyclesignup— (fires the welcome rule)
profile_updatedchanged fields
Enrollmentenrollment / enrollment_removedcourse_id, course, subject (§B.3)
Learningquiz_completedcourse_id, score (0–100)
exam_completed / exam_failedcourse_id, subject, score
lesson_completedcourse_id, lesson
assignment_submitted / assignment_missedcourse_id, assignment
Engagementsession_missed / session_attendedcourse_id, session
inactivitydays since last activity
streak_brokendays the streak lasted
Commercesubscription_started / subscription_expiring / subscription_expiredplan, days_left
payment_failedplan, reason

Rules match on event_type plus payload conditions (e.g. score < 50), so one event type can drive several behaviors — quiz_completed powers both the low-score help offer and the high-score congratulations.

B.3 Enrollment events (they also drive the chat)

Two event_types are special: besides matching trigger rules, they maintain the student’s enrollment state, which decides which per-course Q&A threads the widget offers.

Your course catalog is never mirrored into our configuration — create, rename, and delete courses in your dashboard as you always do. We learn each course from the enrollment events themselves:

  • course_id (required) — your stable course id; everything joins on it.
  • course — the display name; it titles the student’s Q&A thread (last write wins, so renames propagate with the next enrollment).
  • subject — routes the thread to the teacher team of the same name (the team list is the one stable thing we configure together — see “What we need from you”). Omitted or unmatched, the thread lands in the shared queue where any agent can pick it up, instead of nowhere.
# student enrolled in a course
curl -X POST "$INGEST_BASE_URL/api/v1/metrics" \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -d '{
    "student_external_id": "STU-10432",
    "event_type": "enrollment",
    "payload": { "course_id": "physics-m8", "course": "Physics — Month 8", "subject": "physics" },
    "idempotency_key": "enroll-STU-10432-physics-m8"
  }'

# and when they leave it
#   "event_type": "enrollment_removed", "payload": { "course_id": "physics-m8" }

B.4 Delivery & recovery (how robust do you want to be?)

Every call in §B is an upsert or carries an idempotency_key, so retrying is always safe — the same event never lands twice. That gives you a ladder; each rung is optional and adds resilience:

  1. Nothing beyond §A — every logged-in student still gets a mentor chat (see the §A.2 note on auto-created students). No names, no course threads.
  2. Fire-and-forget §B calls (most integrations): full experience; an outage on either side can drop events.
  3. Retry failed calls: when a POST fails or times out, store it and re-send later with the same idempotency_key. Turns an outage into a delay instead of a loss.
  4. Periodic re-sync: a scheduled job that re-upserts your students and re-sends current enrollments. Already-known facts are deduped silently; only genuinely missed ones land. Heals even gaps nobody noticed.

If you have pre-existing students at go-live, run a one-time backfill of §B.1 + §B.3 for the current roster — coordinate with us first so welcome-message rules are paused during the replay.


What we need from you to start

  1. Your backend language/framework (so we can hand you exact snippets if the generic ones above aren’t enough).
  2. Your subjects / teacher teams (the stable list — e.g. physics, chemistry…): each becomes an answering team, and enrollment subject values route to them. Courses themselves need no list — they’re learned from your enrollment events (§B.3).
  3. Any event_types beyond the §B.2 catalog you plan to emit (nothing blocks on this — new types are accepted the moment you send them).
  4. Confirmation you can (a) load the widget per §A.1 and (b) call the two POST endpoints from your backend.

Test checklist

  • Widget launcher appears in the host app.
  • A logged-in student is recognized (history persists across sessions); a wrong/blank hash gets no chat.
  • POST /api/v1/students returns a chatwoot_contact_id.
  • An enrollment event makes that course’s Q&A thread appear in the widget.
  • POST /api/v1/metrics with a trigger event produces a mentor message in the student’s widget.
  • An agent reply from the dashboard raises the host’s unread badge (onUnread).
  • Logging out (full document load) removes the widget.
  • Arabic (RTL) and mixed Arabic/English/math render correctly.

لديك أسئلة عن التكامل؟ احجز مكالمة تقنية