2 giờ sáng, một khách chốt đơn trên landing page của bạn. Bạn đang ngủ. Đến sáng mở mắt ra: khách đã có tài khoản AI Passport, đã nằm trong community, đã vào đúng challenge, và đã nhận email đăng nhập — không ai phải thao tác gì cả. Đó là việc webhook này làm.
Nó dành cho bạn nếu: bạn thu tiền ở nơi khác (landing page riêng, Ladipage, web bán hàng…) nhưng muốn giao sản phẩm trên AI Passport. Khách không phải thanh toán lại lần hai.
Setup — 3 bước
- Vào Cài đặt → Tích hợp → Bán ở web ngoài, bấm Tạo key (quyền Tạo thành viên đã được tick sẵn).
- Copy key (chỉ hiện 1 lần!) và đưa cho dev của bạn — kèm link trang này.
- Dev cài đặt: sau khi khách thanh toán thành công, hệ thống của bạn gọi webhook (chi tiết ở phần kỹ thuật bên dưới). Xong.
Từ đó mỗi đơn hàng chạy tự động: tạo tài khoản (nếu khách chưa có) → join community → vào challenge bạn chỉ định → gửi email magic link cho khách mới.
Câu hỏi thường gặp
Lỡ hệ thống gọi trùng 2 lần thì sao?
Không sao. Mỗi đơn hàng có một mã riêng (externalOrderId) — gọi lại cùng mã thì AI Passport nhận ra và bỏ qua, không tạo member trùng, không gửi email lần hai. Retry thoải mái.
Khách đã có tài khoản AI Passport rồi thì sao?
Hệ thống nhận ra qua email, dùng lại tài khoản cũ của khách — vẫn join community và vào challenge bình thường. Không gửi email magic link (vì khách đã đăng nhập được sẵn).
Khách có bị thu phí gì thêm không?
Không. AI Passport không thu tiền ở bước này — việc thu tiền đã xảy ra ở landing page của bạn rồi. Mặc định khách vào free (Explorer); nếu bạn truyền tierKey + billingPeriod, khách được cấp thẳng gói trả phí tương ứng (có thời hạn) mà vẫn không phải trả thêm gì trên AI Passport. Truyền courseSlug thì khách sở hữu luôn khoá học đó, truyền eventId thì khách có chỗ đã xác nhận ở sự kiện đó — đều không qua bước thanh toán nào trên AI Passport.
Key bị lộ thì sao?
Vào Cài đặt → Tích hợp → Bán ở web ngoài, bấm Thu hồi, key chết ngay lập tức. Tạo key mới đưa lại cho dev. Key này chỉ tạo được member free (không đụng được nội dung hay tiền), và bị giới hạn 60 lần gọi/phút, nên thiệt hại nếu lộ cũng được khoanh vùng.
Làm sao biết webhook đã chạy?
Member mới sẽ xuất hiện trong danh sách thành viên (và trong challenge nếu có chỉ định). Lịch sử provision được lưu lại để đối soát, kể cả khi key đã xoá.
Phần kỹ thuật — đưa cho dev
Endpoint
POST https://app.aipassport.vn/api/integrations/member Authorization: Bearer fc_live_<KEY> Content-Type: application/json
Key gắn với 1 community — member luôn được đưa vào community của key, không truyền communityId trong body.
Request body
| Field | Bắt buộc | Mô tả |
|---|---|---|
email | ✅ | Email khách hàng (≤ 200 ký tự). Trùng email = dùng lại tài khoản cũ. |
name | ✅ | Tên hiển thị (1–120 ký tự). Chỉ áp dụng khi tạo tài khoản mới. |
externalOrderId | ✅ | Mã đơn hàng phía bạn (1–200 ký tự). Khoá idempotent — gọi lại cùng id sẽ không tạo trùng. |
challengeSlug | — | Slug challenge muốn đưa khách vào (bỏ trống = chỉ join community). |
courseSlug | — | Slug khoá học muốn mở cho khách. Cấp quyền vĩnh viễn — không hết hạn theo gói. Khoá phải đã publish. |
eventId | — | Id sự kiện muốn giữ chỗ cho khách — đoạn cuối URL trang sự kiện (/events/<eventId>). Chỗ vào thẳng trạng thái đã xác nhận. Sự kiện phải đang mở và còn chỗ. Không đi cùng eventSeries. |
eventSeries | — | Mã chuỗi của sự kiện (ô “Mã chuỗi” trong form sự kiện). Khách được xếp vào eventCount buổi kế tiếp chưa kết thúc của chuỗi — buổi đang diễn ra vẫn tính, buổi đã qua thì bỏ. Dùng cho lớp định kỳ: bạn tạo sẵn các buổi cùng mã, webhook không phải đổi eventId mỗi tuần. |
eventCount | — | Số buổi muốn xếp theo chuỗi, 1–30, mặc định 1. Chuỗi còn ít buổi hơn thì xếp hết số còn lại và trả về đúng danh sách đã xếp (eventsBooked). |
eventWeekday | — | Chỉ xếp vào buổi rơi đúng thứ này theo giờ của community: 0 = Chủ nhật, 1 = Thứ 2 … 6 = Thứ 7. Buổi đúng thứ đó đã qua thì sang cùng thứ tuần sau. Chỉ dùng cùng eventSeries; bỏ trống = mọi thứ. |
productSlug | — | Slug món trong Marketplace muốn tặng kèm (đoạn cuối URL /marketplace/<slug>). Khách sở hữu vĩnh viễn, đơn 0đ. Món theo kỳ hạn (subscription) không tặng qua đây được. |
tierKey | — | Key gói trả phí muốn cấp (vd builder, pro). Bỏ trống = free Explorer. Xem key hợp lệ ngay trong Cài đặt → Thành viên & gói (dòng “Mã gói” của từng gói). Đi cùng billingPeriod. |
billingPeriod | — | Kỳ hạn: weekly · monthly · 3months · 6months · yearly. Thời hạn (số ngày) tự tính theo cấu hình community. Bắt buộc nếu có tierKey. |
refCode | — | Mã người giới thiệu (4–16 ký tự chữ/số) để ghi công hoa hồng cho đơn bán ở web ngoài. Sai định dạng thì bị bỏ qua, KHÔNG làm hỏng việc cấp tài khoản. |
amountVnd | — | Số tiền thực thu (VNĐ) để tính hoa hồng theo %. Nhận cả số lẫn chuỗi số (3000000 hoặc "3000000"). Bỏ trống nếu bạn trả thưởng cố định. |
Cấp gói trả phí: truyền tierKey + billingPeriod để khách vào thẳng gói (không chỉ free). Hệ thống tạo subscription ACTIVE với hạn = ngày hiện tại + số ngày của kỳ hạn. Nếu khách đang có gói ACTIVE khác, gói cũ được thay thế bằng gói mới (giống hệt luồng thanh toán trong app). Chỉ chấp nhận kỳ hạn đang được bật ở tab Thanh toán.
Mở khoá học: truyền courseSlug để khách sở hữu khoá đó — giống hệt mua lẻ trong app, chỉ khác là tiền đã thu ở landing page của bạn. Quyền này không neo vào gói: gói hết hạn thì khách vẫn vào học được khoá đã mua. Khách đã sở hữu khoá từ trước (mua thật trong app) thì đơn cũ được giữ nguyên, không bị ghi đè.
Giữ chỗ sự kiện: truyền eventId để khách có chỗ ở sự kiện đó — giống đặt vé trong app, chỉ khác là tiền đã thu ở landing page của bạn nên chỗ được xác nhận luôn. Số chỗ của sự kiện vẫn được tôn trọng: hết chỗ thì webhook trả lỗi event_full chứ không nhồi thêm (khách đã giữ chỗ từ trước thì vẫn qua). Sự kiện đặt số chỗ Không giới hạn thì không bao giờ hết chỗ. Khách đã có vé thì vé cũ được giữ nguyên.
Xếp vào buổi kế tiếp của một chuỗi: lớp định kỳ (mỗi tối một sự kiện riêng) thì đặt cùng một Mã chuỗi cho các buổi, rồi truyền eventSeries + eventCount thay cho eventId. Muốn khách tự chọn ngày (vd vé lẻ Thứ 6 hay Thứ 7) thì thêm eventWeekday: mỗi ngày một sản phẩm bên bạn, mỗi sản phẩm gửi một thứ cố định. Hệ thống lấy N buổi sớm nhất chưa kết thúc (buổi đang diễn ra vẫn tính), bỏ qua buổi đã kín chỗ, và trả về danh sách đã xếp để bạn viết vào email cho khách. Chuỗi không còn buổi nào thì trả event_series_exhausted — thêm buổi mới cùng mã rồi gọi lại cùng externalOrderId.
Tặng món Marketplace: truyền productSlug để khách sở hữu món đó (file, template, license…) như mua lẻ trong app, đơn 0đ. Khách đã mua món từ trước thì đơn cũ giữ nguyên; đơn đang chờ thanh toán của cùng món bị huỷ cùng mã QR để khách không trả tiền cho thứ vừa được tặng.
Các thứ này độc lập nhau và dùng chung được trong một đơn: challengeSlug (vào thử thách) · courseSlug (mở khoá học) · eventId hoặc eventSeries (giữ chỗ sự kiện) · productSlug (tặng món) · tierKey (cấp gói). Một combo bán ở ngoài chỉ cần một lần gọi.
Ví dụ
curl -X POST https://app.aipassport.vn/api/integrations/member \ -H "Authorization: Bearer fc_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Nguyễn Văn A", "challengeSlug": "ai-agent-challenge-21-day", "externalOrderId": "LDP-12345" }'
Ví dụ có cấp gói trả phí (Builder, 1 năm):
curl -X POST https://app.aipassport.vn/api/integrations/member \ -H "Authorization: Bearer fc_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Nguyễn Văn A", "externalOrderId": "LDP-12346", "tierKey": "builder", "billingPeriod": "yearly" }'
Ví dụ mở một khoá học bán lẻ:
curl -X POST https://app.aipassport.vn/api/integrations/member \ -H "Authorization: Bearer fc_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Nguyễn Văn A", "externalOrderId": "LDP-12347", "courseSlug": "lam-banh-co-ban" }'
Ví dụ giữ chỗ một sự kiện:
curl -X POST https://app.aipassport.vn/api/integrations/member \ -H "Authorization: Bearer fc_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Nguyễn Văn A", "externalOrderId": "LDP-12348", "eventId": "<eventId trên URL trang sự kiện>" }'
Ví dụ vé 2 buổi kế tiếp của một lớp hằng tuần, tặng kèm một món Marketplace:
curl -X POST https://app.aipassport.vn/api/integrations/member \ -H "Authorization: Bearer fc_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Nguyễn Văn A", "externalOrderId": "LDP-12349", "eventSeries": "weekend-ai-class", "eventCount": 2, "productSlug": "bo-skill-tang-kem" }'
Ví dụ vé lẻ khách chọn học Thứ 7 (gửi từ sản phẩm “vé Thứ 7” bên bạn):
curl -X POST https://app.aipassport.vn/api/integrations/member \ -H "Authorization: Bearer fc_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Nguyễn Văn A", "externalOrderId": "LDP-12350", "eventSeries": "weekend-ai-class", "eventCount": 1, "eventWeekday": 6 }'
Ghi công người giới thiệu (bán ở web ngoài)
Khi khách bấm link giới thiệu trên AI Passport, hệ thống đặt cookie fc_ref. Nhưng cookie đó không đi qua được biên miền, nên đơn thu tiền ở web khác phải tự mang mã về. Đường đi:
link giới thiệu → trang bán của bạn ?fc_ref=aB3dEf7h
↓ (giữ mã qua các bước: form, giỏ, thanh toán)
khách trả tiền
↓
POST /api/integrations/member { …, "refCode": "aB3dEf7h" }Nếu trang bán của bạn có nhiều bước (đặt lịch trước, thanh toán sau vài ngày), hãy lưu mã ngay ở bước đầu tiên và đọc lại lúc gửi webhook — query string và sessionStorage đều mất khi khách mở link thanh toán từ email.
curl -X POST https://app.aipassport.vn/api/integrations/member \ -H "Authorization: Bearer fc_live_..." \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "name": "Nguyễn Văn A", "challengeSlug": "ai-agent-challenge-21-day", "externalOrderId": "LDP-12345", "refCode": "aB3dEf7h", "amountVnd": 3000000 }'
Mặc định hoa hồng được ghi nhận ngay khi gọi webhook. Muốn chỉ trả khi người học hoàn thành thử thách (và tuỳ chọn: không được nộp trễ), bật ở Cài đặt của challenge → Hoa hồng giới thiệu. Khi đó khoản hoa hồng nằm ở trạng thái chờ điều kiện, tự mở khoá lúc người học hoàn thành, và bị huỷ nếu họ trễ quá mức cho phép hoặc rời thử thách.
Người giới thiệu lấy link ở đâu
Thành viên vào mục Affiliate trong menu bên trái của cộng đồng để lấy link. Ngoài link chung của cộng đồng, mỗi món có trang bán riêng sẽ có một link riêng trỏ thẳng tới trang đó. Món chưa có trang bán thì dùng chung link cộng đồng — vẫn ghi công, nhưng người bấm vào đáp xuống trang cộng đồng chứ không thấy đúng món.
Response thành công (200)
{ "ok": true, "externalOrderId": "LDP-12345", "userCreated": true, // false nếu email đã có tài khoản "challengeJoined": true, // false nếu không truyền challengeSlug "courseGranted": false, // true nếu có mở khoá học (courseSlug) "eventBooked": false, // true nếu có giữ chỗ sự kiện (eventId / eventSeries) "productGranted": false, // true nếu có tặng món (productSlug) "tierGranted": false, // true nếu có cấp gói trả phí (tierKey) "alreadyProcessed": false, // true nếu order này đã xử lý trước đó "referralAttributed": true, // true nếu refCode ghi công được cho ai đó "commissionRecorded": true, // true nếu đã sinh dòng hoa hồng "eventsBooked": [ // các buổi đã xếp, theo thứ tự thời gian (rỗng nếu không có) { "id": "…", "shortCode": "bq93ns5s", "title": "…", "startsAt": "2026-10-09T12:30:00.000Z", "endsAt": "…" } ], "eventsBookedSummary": "Thứ Sáu, 09/10/2026 · 19:30" // mỗi buổi một dòng, giờ Việt Nam — dán thẳng vào email }
Gửi refCode mà thấy referralAttributed: false nghĩa là mã không khớp link nào, hoặc người mua đã được ghi công cho người giới thiệu khác từ trước (luật first-touch: người mang khách về đầu tiên giữ công), hoặc họ tự giới thiệu chính mình.
Lỗi
| HTTP | error | Nguyên nhân / xử lý |
|---|---|---|
| 400 | invalid_payload | Body sai schema. Sửa request, đừng retry nguyên trạng. |
| 400 | challenge_not_found | challengeSlug không tồn tại trong community của key. |
| 400 | course_not_found | courseSlug không tồn tại trong community của key. |
| 400 | course_not_published | Khoá học chưa publish — khách vào sẽ thấy 404. Publish khoá rồi gọi lại. |
| 400 | event_not_found | eventId không tồn tại trong community của key. |
| 400 | product_not_found | productSlug không tồn tại trong community của key. |
| 400 | product_is_subscription | Món theo kỳ hạn — quyền cấp qua webhook là vĩnh viễn, không có chỗ ghi hạn. |
| 400 | tier_not_found | tierKey không có trong cấu hình gói của community. |
| 400 | tier_is_free | tierKey trỏ tới gói free (Explorer) — không cần cấp qua đây, bỏ trống tierKey là đủ. |
| 400 | billing_period_not_available | billingPeriod không nằm trong các kỳ hạn đang bật của community. |
| 401 | unauthorized | Thiếu / sai key, key đã revoke hoặc hết hạn. |
| 403 | insufficient_scope | Key chưa tick quyền Tạo thành viên. |
| 403 | challenge_not_allowed_for_this_key | Key bị giới hạn theo challenge, còn challengeSlug trỏ tới challenge ngoài danh sách cho phép của key. |
| 403 | course_not_allowed_for_this_key | Key đang bị giới hạn theo challenge — key hẹp không mở được khoá học. Dùng key không giới hạn cho đơn có courseSlug. |
| 403 | event_not_allowed_for_this_key | Key đang bị giới hạn theo challenge — key hẹp không giữ chỗ sự kiện được. Dùng key không giới hạn cho đơn có eventId / eventSeries. |
| 403 | product_not_allowed_for_this_key | Key đang bị giới hạn theo challenge — key hẹp không tặng món được. Dùng key không giới hạn cho đơn có productSlug. |
| 409 | community_inactive | Gói community hết hạn — đang read-only. Retry vô ích; gia hạn gói trước. |
| 409 | event_closed | Sự kiện đã huỷ hoặc đã kết thúc — không giữ chỗ được. |
| 409 | event_full | Sự kiện đã đủ chỗ. Tăng số chỗ của sự kiện rồi gọi lại đúng đơn đó (cùng externalOrderId). Với eventSeries: mọi buổi còn mở đều đã kín. |
| 409 | event_series_exhausted | Chuỗi eventSeries không còn buổi nào chưa kết thúc. Tạo thêm buổi cùng Mã chuỗi rồi gọi lại đúng đơn đó. |
| 429 | rate_limited | Quá 60 request/phút (theo IP và theo key). Đợi theo Retry-After rồi gọi lại. |
| 500 | internal | Lỗi hệ thống. Retry cùng externalOrderId là an toàn (idempotent). |
Xem thêm: MCP API · Hướng dẫn vận hành community