Trang Chủ Ứng dụng & Tích hợp

Ứng dụng & Tích hợp

Bùi Lê Chí Bảo
By Bùi Lê Chí Bảo
2 bài viết

Tích hợp ứng dụng khác với GraphQL API

Selfomy cung cấp GraphQL API để bạn có thể tích hợp vào các hệ thống của bên thứ ba để thực hiện đọc và cập nhật dữ liệu trên Selfomy One. Chẳng hạn: đồng bộ thông tin học viên mới từ phần mềm quản lý khách hàng (CRM) sang Selfomy One để tạo học viên và ghi danh học viên vào lớp. Ngoài ra, bạn có thể gửi hướng dẫn cho các mô hình AI như ChatGPT, Claude, Gemini,... và yêu cầu AI thực hiện các tác vụ bạn mong muốn. Bạn nên gửi hướng dẫn này cho đội ngũ kỹ thuật/lập trình của công ty. Nếu công ty bạn không có đội ngũ kỹ thuật, chúng tôi cung cấp dịch vụ tích hợp hệ thống với chi phí hợp lý. Vui lòng liên hệ chúng tôi để được tư vấn. Lấy Personal Access Token Bạn cần lấy Personal Access Token (PAT) để có thể thực hiện bất kỳ thao tác nào trên GraphQL API. Xin hãy lưu ý, PAT được tạo sẽ gắn liền với tài khoản đang đăng nhập, đồng nghĩa với việc nếu tài khoản của bạn có quyền gì thì khi truy cập thông qua GraphQL API cũng sẽ có quyền đó. Hiện tại PAT có thời hạn sử dụng vĩnh viễn, vì vậy vui lòng cẩn trọng và không tiết lộ PAT với bất kỳ ai. Chúng tôi sẽ bổ sung phương thức để xóa PAT trong thời gian tới. Để lấy PAT, truy cập trên trình duyệt vào địa chỉ: https://one.selfomy.com/api/user/create-token Bạn sẽ thấy phản hồi có định dạng như sau (tích Pretty-print nếu có để dễ đọc hơn): {"access_token":"9852|gnlqweuiPIgqyop0PiXkRz2xz7tgqwr5","token_type":"Bearer","user":{"id":98915,"uuid":"203a0dd1-b7ga-414f-bb7b-agdd1c96238e","selfomy_id":null,"name":"Test User","email":"[email protected]","phone":null,"address":null,"locale":"vi","email_verified_at":"2025-03-24T16:42:09.000000Z","two_factor_confirmed_at":null,"profile_photo_path":null,"is_view_all_branch":0,"deleted_at":null,"created_at":"2023-02-12T16:58:56.000000Z","updated_at":"2025-05-26T08:25:11.000000Z","current_branch_id":62,"current_company_id":98712,"external_profile_photo_url":null,"company_id":null,"username":null,"profile_photo_url":"https:\/\/ui-avatars.com\/api\/?name=T+U&color=7F9CF5&background=EBF4FF"}} Hãy lấy giá trị của trường “access_token” và lưu lại (không lấy hai dấu ngoặc kép). Tương tác với hệ thống bằng mô hình AI Tính năng này cho phép bạn sử dụng các mô hình AI như OpenClaw, Codex, Claude Code, Gemini Antigravity,... để tương tác với hệ thống bằng những câu lệnh như: "Hãy cho tôi 5 lượt nộp bài gần nhất của học sinh". Không hỗ trợ giao diện chat thông thường của ChatGPT, Claude, Gemini,... LƯU Ý: Các mô hình AI mà bạn sử dụng có thể phạm sai lầm và gây mất mát hoặc thay đổi dữ liệu theo hướng không mong muốn, vui lòng yêu cầu AI lên kế hoạch từng bước và kiểm tra từng bước trước khi thực hiện. Để bắt đầu, trước tiên hãy ra lệnh cho AI như sau: Bạn là một trợ lý giúp tương tác ứng dụng LMS quản lý học tập cho trung tâm dạy học thông qua GraphQL API. Đây là GraphQL endpoint: https://one.selfomy.com/graphql, hãy đọc docs rồi nhận các truy vấn của tôi và thực hiện các truy vấn với API để đạt được yêu cầu của tôi. Trong một số trường hợp, bạn sẽ cần thực hiện nhiều truy vấn khác nhau để thu thập đủ các dữ liệu đầu vào cần thiết, hãy suy luận từng bước một. Đối với các thao tác yêu cầu có thay đổi dữ liệu hoặc xoá dữ liệu, vui lòng lên kế hoạch và hỏi tôi để để tôi kiểm duyệt từng bước một, chỉ thực hiện khi đã được tôi đồng ý. Nếu câu lệnh của tôi không thể thực hiện, vui lòng cho tôi biết. Từ đó, bạn có thể ra lệnh cho AI thực hiện các thao tác mong muốn. Sử dụng GraphQL API GraphQL Client Bạn có thể sử dụng bất kỳ GraphQL client nào để khai thác GraphQL API. Selfomy có cung cấp sẵn một GraphQL Client để bạn sử dụng tại đây. Ngoài ra, Altair GraphQL Client là một trong số các client miễn phí có phiên bản web, MacOS lẫn Windows. Hướng dẫn dưới đây sẽ dựa trên phiên bản web có trên Selfomy này, tuy nhiên các GraphQL Client khác cũng có giao diện tương tự. Đăng nhập Nếu bạn dùng client trên Selfomy: Tại tab Headers ở bên dưới khung nhập truy vấn, nhập nội dung như sau: { "Authorization": "Bearer <Nhập mã Personal Access Token của bạn>" } Sau đó bạn đã có thể bắt đầu truy vấn, thử với truy vấn đơn giản như sau giúp lấy thông tin người dùng hiện tại: query { me { uuid name email profilePhotoUrl } } Nếu bạn thấy thông tin người dùng được trả về nghĩa là bạn đã đăng nhập thành công Nếu bạn dùng client của bên thứ ba: Đầu tiên nhập endpoint là: https://one.selfomy.com/graphql Trong trường hợp bạn không thấy tab Sau đó chọn tab Auth và điền: - Auth type: Bearer Token - Bearer token: Chính là PAT (Personal Access Token) bạn đã tạo trước đó Sau đó bạn đã có thể bắt đầu truy vấn, thử với truy vấn đơn giản như sau giúp lấy thông tin người dùng hiện tại: query { me { uuid name email profilePhotoUrl } } Nếu bạn thấy thông tin người dùng được trả về nghĩa là bạn đã đăng nhập thành công Xem tài liệu Để xem các Query, Mutation, Type có sẵn trên hệ thống, hãy tìm tabs Docs hay Documentation có trên Client của bạn.

Cập nhật lần cuối: Apr 28, 2026

Nhận các cập nhật từ hệ thống với Webhooks

Webhook giúp hệ thống của bạn (ví dụ phần mềm quản trị ERP, CRM,...) phản ứng tức thì khi có sự kiện quan trọng xảy ra trên Selfomy: học viên mới được tạo, học viên đăng ký lớp, đơn hàng được thanh toán. Khi sự kiện xảy ra, Selfomy sẽ gửi một POST request JSON đến URL bạn cấu hình. Vị trí truy cập: Ảnh đại diện → Thông tin công ty → Webhooks. Bắt đầu nhanh 1. Truy cập /company/webhooks (sidebar → Webhooks). 2. Bấm Add endpoint (Thêm endpoint). 3. Điền thông tin: - Name (Tên) — nhãn để bạn dễ phân biệt (ví dụ: "Production", "Staging"). - Endpoint URL — URL HTTPS mà Selfomy sẽ POST tới. - Signing secret (Khóa ký) (tùy chọn) — dùng để ký request bằng HMAC-SHA256 nhằm xác thực nguồn gốc. Bấm Regenerate để tạo khóa ngẫu nhiên mạnh. - Notification email (Email nhận cảnh báo) (tùy chọn) — nơi nhận email khi webhook bị tự động tắt. Để trống để dùng email mặc định của công ty. - Events (Sự kiện) — chọn các sự kiện muốn nhận. Bắt buộc chọn ít nhất 1. - Active (Hoạt động) — bật/tắt. - Bấm Save (Lưu). Mỗi công ty được cấu hình tối đa 5 endpoint. Sự kiện - new_student_created — học viên mới được tạo. - new_student_enrolled — học viên được đăng ký vào lớp. - order_created — đơn hàng được thanh toán (lần thanh toán đầu tiên được ghi nhận). - order_updated — đơn hàng đã thanh toán một phần nhận thêm khoản thanh toán mới. - quiz_attempt_submitted — học viên nộp bài quiz (status chuyển thành submitted/grading/graded). Chỉ phát một lần cho mỗi lần nộp; chấm lại không phát lại. - quiz_group_attempt_submitted — bài làm trong phòng thi ảo vừa được nộp được nộp (tức đã làm xong đủ các bài thi). - assignment_group_attempt_submitted — học viên nộp bài tập. - material_attempt_submitted — học viên hoàn thành một học liệu. - quiz_mocktest_created — phòng thi ảo vừa được tạo. - quiz_attempt_graded — bài tập dạng đề thi được chấm xong. - quiz_group_attempt_graded — bài làm trong phòng thi ảo được chấm xong. - assignment_group_attempt_graded — bài tập được chấm xong. Cấu trúc payload Mọi payload là JSON với khung chung: { "type": "<mã_sự_kiện>", "data": { /* dữ liệu riêng cho từng sự kiện */ } } new_student_created { "type": "new_student_created", "data": { "student": { "uuid", "internalId", "fullName", "firstName", "lastName", "avatarUrl", "dateOfBirth", "school", "phone", "address", "email", "parentName", "parentPhone", "notes", "createdAt", "updatedAt" }, "branch": { "uuid", "name", "email", "phone", "address", "taxCode", "createdAt", "updatedAt" } } } new_student_enrolled { "type": "new_student_enrolled", "data": { "classroomEnrollment": { "uuid", "startAt", "endAt", "customTuitionFees", "createdAt", "updatedAt" }, "classroom": { "uuid", "title", "timeSlotName", "isActive", "startAt", "endAt", "customPrice", "createdAt", "updatedAt" }, "student": { /* same shape as new_student_created.student */ }, "branch": { /* same shape as new_student_created.branch */ } } } order_created / order_updated { "type": "order_created", "data": { "order": { "uuid", "internalId", "paymentMethod", "status", "amount", "tax", "discount", "totalDiscount", "totalAmount", "prepaidAmount", "paidAmount", "remainingAmount", "total", "createdAt", "updatedAt" }, "orderItems": [ { "description", "isTopup", "amount", "tax", "discount", "createdAt", "updatedAt" } ], "student": { /* same shape as new_student_created.student */ } | null, "branch": { /* same shape as new_student_created.branch */ } } } quiz_attempt_submitted { "type": "quiz_attempt_submitted", "data": { "quizAttempt": { "uuid", "attempt", "status", "startAt", "endAt", "sumGrades", "totalQuestions", "correctAnswers", "overallScore", "overallFeedback", "createdAt", "updatedAt" }, "quiz": { "uuid", "title", "description", "timeLimit", "numberOfRetakes", "passingScore", "createdAt", "updatedAt" }, "student": { /* cùng shape với new_student_created.student */ } | null, "branch": { /* cùng shape với new_student_created.branch */ } | null } } quiz_group_attempt_submitted { "type": "quiz_group_attempt_submitted", "data": { "quizGroupAttempt": { "uuid", "status", "startAt", "endAt", "overallScore", "feedback", "createdAt", "updatedAt" }, "quizMocktest": { "uuid", "title", "description", "startAt", "endAt", "examMode", "gradingMode", "createdAt", "updatedAt" } | null, "student": { /* ... */ } | null, "branch": { /* ... */ } | null } } assignment_group_attempt_submitted { "type": "assignment_group_attempt_submitted", "data": { "assignmentGroupAttempt": { "uuid", "status", "startAt", "endAt", "overallScore", "gradedAt", "createdAt", "updatedAt" }, "assignmentGroup": { "uuid", "title", "description", "timeLimit", "displayFormat", "createdAt", "updatedAt" } | null, "student": { /* ... */ } | null, "branch": { /* ... */ } | null } } material_attempt_submitted { "type": "material_attempt_submitted", "data": { "materialAttempt": { "uuid", "status", "endAt", "isLate", "createdAt", "updatedAt" }, "material": { "uuid", "title", "content", "externalImageUrl", "createdAt", "updatedAt" } | null, "student": { /* ... */ } | null, "branch": { /* ... */ } | null } } student và branch có thể là null trong các event *_attempt_submitted khi attempt được thực hiện bởi người dùng ẩn danh/MOOC không gắn học viên. material.content được cắt còn 500 ký tự để giữ payload nhỏ gọn; gọi API nếu cần đầy đủ. quiz_attempt_graded / quiz_group_attempt_graded / assignment_group_attempt_graded Payload giống hệt event *_submitted tương ứng. Chỉ khác giá trị type. Lưu ý: khi chấm lại (Graded → Grading → Graded) sẽ phát sinh event thêm lần nữa. Nếu bạn dùng event để gửi điểm cho học viên, có thể so sánh trường gradedAt để tránh gửi hai lần hoặc chấp nhận cùng một lần chấm có thể đến hai lần. Đối với các dạng bài chấm tự động như Listening/Reading có thể gửi cả event *_submitted và *_graded cho cùng một bài làm trong thời gian ngắn. quiz_mocktest_created { "type": "quiz_mocktest_created", "data": { "quizMocktest": { "uuid", "title", "description", "startAt", "endAt", "examMode", "gradingMode", "createdAt", "updatedAt" } } } Header của request Mỗi request gửi đi đều kèm các header sau: - Content-Type — luôn là application/json. - X-Selfomy-Event — mã sự kiện (ví dụ: order_created). - X-Selfomy-Timestamp — thời điểm gửi, định dạng ISO 8601. - X-Selfomy-Signature — chữ ký HMAC-SHA256 của body (hex). Chỉ gửi khi đã đặt signing secret. - X-Selfomy-Signature-Algo — hiện tại luôn là sha256. Chỉ gửi khi có signing secret. Xác thực chữ ký Nếu bạn đặt signing secret, hệ thống nhận webhook nên từ chối mọi request có chữ ký không khớp. Chữ ký được tính trên body thô của request dùng secret của bạn. Ví dụ PHP $body = file_get_contents('php://input'); $expected = hash_hmac('sha256', $body, $SECRET_CUA_BAN); if (! hash_equals($expected, $_SERVER['HTTP_X_SELFOMY_SIGNATURE'] ?? '')) { http_response_code(401); exit; } Ví dụ Node.js import crypto from 'node:crypto'; app.post('/hooks', express.raw({ type: 'application/json' }), (req, res) => { const expected = crypto.createHmac('sha256', SECRET_CUA_BAN).update(req.body).digest('hex'); if (expected !== req.header('X-Selfomy-Signature')) return res.sendStatus(401); const payload = JSON.parse(req.body.toString()); // xử lý payload... res.sendStatus(200); }); Chống replay Nên từ chối request có X-Selfomy-Timestamp cũ hơn mốc cho phép (ví dụ: 5 phút) để chống tấn công replay. Tự động tắt sau nhiều lần thất bại liên tiếp Để bảo vệ cả receiver và worker gửi sự kiện, Selfomy sẽ tự động tắt webhook sau 5 lần gửi thất bại liên tiếp (mọi response không phải 2xx hoặc lỗi mạng đều tính là thất bại). Khi điều này xảy ra: - Trạng thái webhook chuyển sang Không hoạt động — không còn gửi sự kiện nào nữa. - Một email cảnh báo được gửi tới Email thông báo của webhook nếu có, ngược lại là email công ty. - Bảng "Lịch sử gửi gần đây" hiển thị các lần thất bại để bạn chuẩn đoán. Để bật lại: sửa receiver, mở webhook trong panel, và bấm Lưu để hệ thống gửi lại cho những lần sau. Test ping Để kiểm tra xem hệ thống có nhận được dữ liệu, hãy bấm nút Ping test. Khi bấm, hệ thống gửi: { "type": "ping", "data": { "sent_at": "<ISO 8601>", "source": "test" } } Test ping đi qua cùng quy trình ký + header như sự kiện thật, nên đây là cách tốt để xác nhận receiver hoạt động và chữ ký được xác thực đúng. Test ping cũng được tính vào số lần thất bại, nếu 5 test ping liên tiếp đều thất bại thì webhook sẽ tự động tắt. Lịch sử gửi gần đây Mở webhook để xem lịch sử gửi (chỉ lưu 30 ngày gần nhất). Mỗi dòng sẽ hiển thị: - Thời gian gửi - Loại sự kiện - Trạng thái thành công / thất bại - Xem payload — xem toàn bộ request + response JSON - Gửi lại — gửi lại payload đã lưu, dùng URL/secret/header hiện tại (không phải các giá trị tại thời điểm gửi gốc) Tính năng Gửi lại hữu ích khi receiver tạm thời gặp sự cố và bạn muốn gửi lại các sự kiện bị bỏ lỡ. Giới hạn kỹ thuật - Số endpoint tối đa mỗi công ty — 5. - Thời gian lưu lịch sử gửi — 30 ngày (tự động dọn). - Ngưỡng tự động tắt — 5 lần thất bại liên tiếp. - Thuật toán chữ ký — HMAC-SHA256 (chỉ khi đặt secret). Khuyến nghị cho receiver 1. Luôn xác thực chữ ký trước khi tin tưởng nội dung — bất kỳ ai cũng có thể POST đến URL công khai. 2. Phản hồi nhanh — trả 2xx trong vài giây. Việc nặng nên đẩy sang background job. 3. Idempotent — receiver có thể nhận cùng sự kiện 2 lần (ví dụ: sau khi Resend). Dùng UUID của entity + loại sự kiện làm khóa khử trùng. 4. Từ chối timestamp cũ — chống replay bằng cách so X-Selfomy-Timestamp với thời điểm hiện tại. 5. Ghi log đầy đủ — ít nhất là loại sự kiện, kết quả xác thực chữ ký, và mã HTTP bạn trả về. 6. Bắt buộc HTTPS — Selfomy không chấp nhận URL http://. Câu hỏi thường gặp Tại sao webhook của tôi bị tự động tắt? Có 5 lần gửi liên tiếp thất bại. Mở webhook để xem response lỗi, sửa receiver rồi kích hoạt lại webhook. Tôi có thể tạo webhook riêng cho staging và production không? Được — đó chính là mục đích của tính năng nhiều endpoint. Đặt tên rõ ràng và chọn sự kiện phù hợp cho từng endpoint. Receiver của tôi xử lý chậm thì sao? Bạn nên phản hồi trong vòng 15 giây. Nếu việc xử lý nặng, hãy nhận request, queue job, và trả 2xx ngay. Secret_key có nằm trong payload không? Không. Nó chỉ được dùng để tính X-Selfomy-Signature. Không bao giờ log, không bao giờ trả về. Một endpoint có thể nhận nhiều loại sự kiện không? Được — chọn nhiều sự kiện trên cùng một endpoint. Trường type cho biết bạn đang nhận sự kiện nào.

Cập nhật lần cuối: Apr 28, 2026