دليل التكامل
العقد الكامل والمحدَّث للتكامل — الوثيقة نفسها التي بُنيت عليها أولى عمليات النشر. وسمَا سكربت في الواجهة، وثلاث استدعاءات REST من خادمك.
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:
| Value | Where 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_URL | Data API |
API_KEY (secret) | Data API auth |
Keep
INBOX_HMAC_KEYandAPI_KEYon 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), andnpm run provision:integrationrenders 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_idwe 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 callsPOST /api/v1/studentsfor them (§B.1 — the ordinary upsert, no special case), and per-course threads appear as yourenrollmentevents 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_HOSTwe hand you (later, optionally a subdomain of your domain — the cutover is lossless because identity rides onexternal_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_typeis a short snake_case string — see the catalog below.payloadis 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):
| Area | event_type | Typical payload |
|---|---|---|
| Lifecycle | signup | — (fires the welcome rule) |
profile_updated | changed fields | |
| Enrollment | enrollment / enrollment_removed | course_id, course, subject (§B.3) |
| Learning | quiz_completed | course_id, score (0–100) |
exam_completed / exam_failed | course_id, subject, score | |
lesson_completed | course_id, lesson | |
assignment_submitted / assignment_missed | course_id, assignment | |
| Engagement | session_missed / session_attended | course_id, session |
inactivity | days since last activity | |
streak_broken | days the streak lasted | |
| Commerce | subscription_started / subscription_expiring / subscription_expired | plan, days_left |
payment_failed | plan, 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:
- 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.
- Fire-and-forget §B calls (most integrations): full experience; an outage on either side can drop events.
- 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. - 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
- Your backend language/framework (so we can hand you exact snippets if the generic ones above aren’t enough).
- Your subjects / teacher teams (the stable list — e.g. physics,
chemistry…): each becomes an answering team, and enrollment
subjectvalues route to them. Courses themselves need no list — they’re learned from your enrollment events (§B.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). - 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/studentsreturns achatwoot_contact_id. - An
enrollmentevent makes that course’s Q&A thread appear in the widget. -
POST /api/v1/metricswith 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.
لديك أسئلة عن التكامل؟ احجز مكالمة تقنية