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 focus.camp, đã 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 focus.camp. Khách không phải thanh toán lại lần hai.
Setup — 3 bước
- Vào
Settings → Tích hợp → API Keys, bấm + Tạo API key mới, tick ô Provision members. - 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ì focus.camp 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 focus.camp 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. focus.camp 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 focus.camp.
Key bị lộ thì sao?
Vào tab Tích hợp bấm Revoke — 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://focus.camp/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). |
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 Settings → Tích hợp. Đ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. |
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.
Ví dụ
curl -X POST https://focus.camp/api/integrations/member \
-H "Authorization: Bearer fc_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "khach@example.com",
"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://focus.camp/api/integrations/member \
-H "Authorization: Bearer fc_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "khach@example.com",
"name": "Nguyễn Văn A",
"externalOrderId": "LDP-12346",
"tierKey": "builder",
"billingPeriod": "yearly"
}'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
"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 đó
}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 | 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 scope Provision members. |
| 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. |
| 409 | community_inactive | Gói community hết hạn — đang read-only. Retry vô ích; gia hạn gói trước. |
| 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
