# API Xưởng mã thiết bị API cho phép khách tích hợp Xưởng mã thiết bị vào chương trình riêng. Tài khoản tạo qua API xuất hiện trên dashboard của chính người dùng đó và sử dụng chung hạn mức đang được quản trị viên cấp. - Địa chỉ website: `https://workshop.example.com` - API gốc: `https://workshop.example.com/api/v1/workshop` - Tài liệu trên web: `/api-docs` - OpenAPI JSON: `/api/v1/openapi.json` - Mẫu Python: `/api-docs/examples/workshop_api.py` ## 1. Tạo API Key Đăng nhập website, mở **màn hình API KEY**, nhập tên, chọn quyền và thời hạn rồi tạo key. Sao chép ngay: key đầy đủ chỉ hiển thị một lần. Mỗi người dùng có tối đa **10 key đang hoạt động**. Giao diện cho chọn **30, 90, 365 ngày hoặc không hết hạn**. | Quyền | Sử dụng | |---|---| | `workshop:read` | Xem hạn mức, danh sách tài khoản và tiến trình | | `workshop:write` | Thêm/đăng nhập tài khoản, gửi mã, nhập OTP, bật/tạm dừng/xóa tài khoản | | `workshop:code` | Lấy mã 6 số và được xem `device_api` trong phản hồi tài khoản | Tích hợp toàn bộ luồng cần cả ba quyền. Đổi link lấy mã cần cả `workshop:write` và `workshop:code`. Key chỉ có quyền đọc sẽ nhận `device_api: null` và `device_path: null`. Gửi key trong header của mọi request API: ```http Authorization: Bearer xid_YOUR_API_KEY ``` Key không được nhận qua query URL hoặc phiên đăng nhập web. Dù key thuộc super admin, API này vẫn chỉ truy cập tài khoản thuộc chính chủ key. Key không truy cập quản trị, backup hoặc tạo key khác. Từ màn hình API KEY có thể thu hồi từng key. Key hết hạn, bị thu hồi, hoặc key cũ sau khi user bị khóa/đổi mật khẩu sẽ không xác thực được nữa; mở khóa user không làm sống lại key cũ. **Thu hồi API Key không đổi các link `device_api` đã cấp.** Muốn vô hiệu link cũ, dùng chức năng đổi link trên dashboard hoặc endpoint `rotate-link` bên dưới. Người có link `device_api` có thể lấy mã mà không cần API Key; bảo quản link như thông tin bí mật. ## 2. Quy ước request và giới hạn - Dùng HTTPS. Request có body gửi JSON với `Content-Type: application/json`. - Các thao tác không có trường dữ liệu, kể cả DELETE, có thể gửi `{}`. - Giới hạn **120 request/phút cho mỗi người dùng và mỗi key**. Nhiều key cùng user vẫn chia sẻ giới hạn của user. Giới hạn hiện tại được tính trong tiến trình dịch vụ; khởi động lại dịch vụ sẽ bắt đầu cửa sổ mới. - Khi nhận HTTP `429` hoặc `503 code_queue_timeout`, chờ tối thiểu số giây trong `Retry-After`. Gửi SMS còn có khoảng chờ riêng theo `resend_available_at`. - Theo dõi job khoảng **3 giây/lần**; khi chạy nhiều job, điều phối tổng lượt gọi để không vượt hạn mức. - Không theo redirect một cách tự động với header Authorization. Gọi đúng origin API đã cấu hình. - API chỉ trả trạng thái cần thiết, không trả log xử lý nội bộ. ### Ngôn ngữ phản hồi Gửi `Accept-Language: vi`, `en` hoặc `zh-CN` để chọn tiếng Việt, tiếng Anh hoặc tiếng Trung giản thể cho thông báo. Có thể dùng `X-XID-Language` với các giá trị tương tự để ưu tiên ngôn ngữ của request. Trên trình duyệt, ngôn ngữ đã chọn được lưu trong cookie và được ưu tiên hơn `Accept-Language`; `X-XID-Language` được ưu tiên hơn cookie. Nếu không chọn, API mặc định tiếng Việt. Tên trường, đường dẫn, HTTP status, mã `error`, giá trị `status`, ID, URL và dữ liệu người dùng giữ nguyên; không dùng chuỗi thông báo đã dịch để điều khiển chương trình. ### Chống tạo trùng với Idempotency-Key Ba endpoint tạo tài khoản, đăng nhập lại và gửi batch **bắt buộc** header `Idempotency-Key`: ```http Idempotency-Key: 3ecf9a18-b767-4d82-9aae-539ab946b4cf ``` Dùng UUID mới cho mỗi thao tác mới. Key dài 8–128 ký tự; chấp nhận chữ ASCII, số, `.`, `_`, `:`, `-`. Nếu mất mạng sau khi gửi, có thể gửi lại **cùng key, cùng endpoint và cùng nội dung JSON** để lấy lại tham chiếu tài khoản/job mà không tạo thêm tác vụ. Header `Idempotency-Replayed: true` cho biết đây là lần gọi lại. Phản hồi khi replay thể hiện trạng thái hiện tại của tài khoản/job. - Bản ghi chống trùng được giữ **7 ngày**. Hết thời gian này, gửi lại key cũ có thể tạo thao tác mới; hãy kiểm tra tài khoản/job trước. - Cùng key nhưng nội dung khác trả `409 idempotency_conflict`. - Nếu tác vụ chưa được xác nhận hoặc bị gián đoạn, API trả `request_in_progress`/`request_incomplete`, có thể kèm `account_id`, `job_id` hoặc `batch_id`. Kiểm tra các tham chiếu đó và danh sách tài khoản trước khi tạo thao tác mới. - Nếu đã tạo account nhưng chưa khởi chạy login, lỗi có thể kèm `account_id`. Sửa nguyên nhân, dùng endpoint đăng nhập lại cho account đó với **Idempotency-Key mới**. - Tác vụ đã ghi nhận thất bại sẽ phát lại cùng kết quả lỗi khi dùng lại key. Đừng đổi key để lặp tạo tài khoản một cách tự động. - Mỗi user lưu tối đa 10.000 bản ghi trong khoảng giữ này; vượt giới hạn trả `429 request_history_full`. ## 3. Luồng đăng nhập và lấy mã ### Bước 1: kiểm tra quyền và hạn mức ```http GET /api/v1/workshop/me ``` Ví dụ phản hồi: ```json { "user": {"id": "USER_ID", "username": "customer"}, "quota": { "can_add_accounts": true, "max_accounts": 10, "account_count": 2, "remaining": 8 }, "key": { "id": "KEY_ID", "name": "Ứng dụng của tôi", "scopes": ["workshop:read", "workshop:write", "workshop:code"] } } ``` ### Bước 2: thêm tài khoản và bắt đầu đăng nhập ```http POST /api/v1/workshop/accounts Authorization: Bearer xid_YOUR_API_KEY Content-Type: application/json Idempotency-Key: YOUR_UNIQUE_OPERATION_UUID ``` ```json { "apple_id": "your-account@example.com", "password": "YOUR_APPLE_ID_PASSWORD", "label": "Tài khoản của tôi", "note": "Nhóm khách hàng A" } ``` Nhận HTTP `202` với hai đối tượng `account` và `job`. Lưu `account.id` và `job.id` vào hệ thống của bạn. `device_api` thường chưa có ngay vì job còn đang chạy. `note` tùy chọn, tối đa 4.000 ký tự và có thể nhiều dòng. Có thể gửi thêm `folder_id` là ID thư mục thuộc chính chủ API key (tạo bằng `POST /folders` với `{"name":"Khách hàng A"}`). Bỏ trường này hoặc gửi `null` để chưa phân loại. Các đối tượng tài khoản trả thêm `note`, `folder_id`, `folder_name` và `hidden`. #### Tên và loại thiết bị (chỉ khi thêm tài khoản mới) `device_name` là tên tùy chọn dùng được cho **Mac, iPhone và iPad**. Để trống để dùng tên mặc định của loại thiết bị; tên tùy chỉnh được lưu và giữ nguyên khi đăng nhập lại hoặc khôi phục tài khoản. `GET /api/v1/workshop/profiles` (quyền `workshop:read`) trả danh sách loại thiết bị hiện có. Gửi `profile_id` lấy từ danh sách này khi tạo iPhone/iPad, kèm `device_name` nếu muốn. Không gửi mẫu Mac hoặc phiên bản macOS tùy chỉnh cho hồ sơ iPhone/iPad. Nếu không gửi `profile_id`, API dùng hồ sơ Mac mặc định. Các tùy chọn mẫu Mac và macOS dưới đây chỉ áp dụng cho hồ sơ Mac: Ba trường tùy chọn `device_model`, `device_os` và `device_name` được lưu cùng tài khoản: ```json { "apple_id": "your-account@example.com", "password": "YOUR_APPLE_ID_PASSWORD", "device_model": "macbook-air-13-m5", "device_os": "tahoe-26.5", "device_name": "MacBook của tôi" } ``` - Bỏ ba trường, hoặc gửi `device_model: "default"`, `device_os: "default"` và `device_name: ""`, để dùng đúng cấu hình mặc định hiện tại. Mẫu `default` chỉ kết hợp được với macOS `default`. - `GET /api/v1/workshop/device-profiles` (quyền `workshop:read`) trả `{ "default": "default", "profiles": [...], "default_os": "default", "operating_systems": [...] }`. Mỗi mẫu có `id`, `label`, `product_type`, `os_version`, `os_build`, `experimental`, `default_os`, `supported_os`. Client chỉ hiển thị OS trong `supported_os`; khi chuyển mẫu và lựa chọn cũ không phù hợp, chọn `default_os` của mẫu đó. - Danh mục tạo mới theo thứ tự: `default`, `mac-mini-m6`, `mac-studio-m5-max`, `macbook-air-13-m5`, `macbook-air-15-m5`, `macbook-pro-14-m5`, `macbook-air-13-m4`, `macbook-air-15-m4`, `mac-studio-m4-max`, `macbook-pro-14-m4`, `macbook-pro-14-m4-pro`, `macbook-pro-14-m4-max`, `macbook-pro-16-m4-pro`, `macbook-pro-16-m4-max`, `mac-studio-m1-max`. Mặc định ở đầu, tiếp theo là mẫu mới đến cũ. MacBook Pro M5 là mẫu năm 2025; các mẫu MacBook Pro M4/M4 Pro/M4 Max là năm 2024. - Các mẫu khác từng có trong danh mục bị từ chối khi tạo tài khoản mới. Tài khoản cũ vẫn đăng nhập lại, chạy lại trong lô và khôi phục backup bằng mẫu/macOS đã lưu; không đổi danh tính hoặc liên kết lấy mã. - Phiên bản macOS: `default` = **13.1 / 22C65** cho các mẫu cũ, `sonoma-14.4.1` = **14.4.1 / 23E224**, `sequoia-15.5` = **15.5 / 24F74**, `tahoe-26.5` = **26.5 / 25F71**, `macos-27.0.1` = **27.0.1 / 26A434**. Không nhận model/version/build tùy ý, hoặc cặp model/OS ngoài `supported_os`. - Bỏ `device_os` hoặc gửi `default` sẽ lấy `default_os` của mẫu: 26.5 cho Air M5, Pro M5 và các mẫu M4, 27.0.1 cho Studio M5 Max/mini M6, 13.1 cho Studio M1 Max và mẫu `default`. API lưu/trả ID OS cụ thể. Request dùng alias và ID cụ thể tương đương khi kiểm tra idempotency. State đã lưu không bị đổi mặc định. - Các mẫu và phiên bản tùy chọn đang ở giai đoạn kiểm thử; trường `experimental` cho biết điều này. API không bảo đảm Apple hiển thị chính xác tên hoặc chấp nhận mọi cấu hình. - `device_name` được cắt khoảng trắng hai đầu, tối đa **128 byte UTF-8**, không nhận ký tự điều khiển. Để trống giữ tên mặc định. Đây là tên gửi khi đăng ký thiết bị, khác `label` chỉ dùng trên dashboard. - Phản hồi tài khoản có `device_model`, `device_os`, `device_name`. Đăng nhập lại dùng cấu hình đã lưu; không nhận các trường này ở `/accounts/{id}/login` hoặc `PATCH /accounts/{id}`. Tên và cấu hình được giữ trong backup; khôi phục không tạo danh tính mới. - Tài khoản cũ giữ nguyên danh tính và liên kết lấy mã. Chọn mẫu mới không thay đổi những tài khoản đã tồn tại. Không gửi lại Apple ID/mật khẩu để kiểm tra tiến trình. Dùng `job.id` ở bước tiếp theo. ### Bước 3: theo dõi tiến trình ```http GET /api/v1/workshop/jobs/JOB_ID ``` Các trạng thái job: `job.events` chứa các bước tiến trình theo thứ tự, mỗi bước gồm `time` (ISO 8601) và `message` để hiển thị cho người dùng. Nội dung đã được chuẩn hóa và không chứa chẩn đoán nội bộ. Dùng `status` để điều khiển chương trình, không phân tích chuỗi `message` vì cách diễn đạt có thể thay đổi. | `status` | Cách xử lý | |---|---| | `queued` | Đang chờ; tiếp tục poll | | `processing` | Đang xử lý; tiếp tục poll | | `awaiting_otp` | Cần mã xác thực, đọc `two_factor` | | `succeeded` | Hoàn tất; đọc tài khoản và lấy link/mã | | `failed` | Kết thúc thất bại; kiểm tra trước khi đăng nhập lại | | `cancelled` | Tác vụ đã dừng | `otp_expires_at` là Unix timestamp theo giây. Nhập OTP trước thời điểm này. Thời gian còn lại phụ thuộc phiên hiện tại; không hardcode số phút ở client. Khi `two_factor.code_sent` là `true`, có thể nhập mã đã nhận ngay. Nếu muốn chọn số nhận SMS, làm bước 4. ### Bước 4: chọn số điện thoại nhận SMS Yêu cầu danh sách cách nhận mã: ```http POST /api/v1/workshop/jobs/JOB_ID/factor-options Content-Type: application/json {} ``` HTTP `202` nghĩa là đã nhận yêu cầu. Tiếp tục GET job đến khi `two_factor.options_loading` là `false` và `options_loaded` là `true`. Ví dụ phần `two_factor`: ```json { "mode": "trusted_device", "phone_id": null, "trusted_device": true, "options_loaded": true, "options_loading": false, "phones": [ {"id": 1, "label": "•••• 01"}, {"id": 2, "label": "•••• 88"} ], "sending": false, "code_sent": true, "resend_available_at": null } ``` Chọn `id` do API trả về; không suy ra ID từ vị trí trong danh sách hoặc từ đuôi số. Gửi mã đến số đã chọn: ```http POST /api/v1/workshop/jobs/JOB_ID/send-code Content-Type: application/json {"mode":"sms","phone_id":1} ``` Hoặc gửi tới thiết bị tin cậy: ```json {"mode":"trusted_device"} ``` Tiếp tục GET job đến khi `two_factor.sending` là `false`. Chỉ nhập OTP khi `code_sent` là `true` và không có `send_error`. Tôn trọng `resend_available_at` trước khi yêu cầu gửi lại. Nếu danh sách không tải được hoặc có lỗi gửi mã, phản hồi chỉ đưa thông báo chung. ### Bước 5: gửi OTP ```http POST /api/v1/workshop/jobs/JOB_ID/otp Content-Type: application/json {"code":"123456"} ``` Mã phải là **chuỗi** sáu chữ số, giữ cả số `0` ở đầu. Sau khi gửi, tiếp tục GET job đến `succeeded` hoặc trạng thái kết thúc khác. Trường hợp tài khoản không yêu cầu OTP, job có thể đi thẳng đến `succeeded`. ### Bước 6: lấy link và mã hiện tại ```http GET /api/v1/workshop/accounts/ACCOUNT_ID ``` Khi hoàn tất, `account.status` là `ready` và `account.device_api` là link lấy mã. Nếu dịch vụ chưa cấu hình public origin, link có thể là đường dẫn `/d/...`; ghép với origin API đang dùng. Lấy mã qua endpoint có xác thực bằng API Key: ```http POST /api/v1/workshop/accounts/ACCOUNT_ID/code Content-Type: application/json {} ``` ```json { "code": "000007", "generated_at": "2026-10-16T10:30:00+00:00" } ``` `generated_at` là thời điểm trả mã, không phải thời hạn mã. Đọc mã khi cần dùng. Ngoài endpoint có API Key, có thể GET `device_api` để nhận mã dạng văn bản theo link riêng của tài khoản. Yêu cầu lấy mã có thể chờ trong hàng đợi dùng chung với web. `429 code_queue_full` nghĩa là hàng đợi đã đầy; `503 code_queue_timeout` nghĩa là yêu cầu hết thời gian chờ trước khi chạy. Cả hai trả `Retry-After` theo giây: chờ đủ thời gian rồi gọi lại endpoint `/code`, không tạo tài khoản hoặc đăng nhập lại chỉ vì các lỗi này. ## 4. Proxy và nguồn OTP tùy chọn ### Proxy khi tạo tài khoản hoặc đăng nhập lại Có thể thêm `proxy` vào body **`POST /accounts` hoặc `POST /accounts/{id}/login`**. Proxy áp dụng cho kết nối từ dịch vụ tới Apple trong tiến trình đăng nhập/binding, gồm bước chọn cách nhận mã, gửi mã và nhập OTP. Proxy đã chọn được giữ cố định trong cùng job; cấu hình mới không đổi đường kết nối của job đang chạy. Proxy này không áp dụng cho đăng nhập website, request từ ứng dụng của bạn tới API hoặc lấy mã hiện tại qua `/code` và `device_api`. - Bỏ `proxy` hoặc gửi `"proxy": null`: dùng cấu hình proxy đã lưu của chủ API Key. Nếu chưa lưu, mặc định không dùng proxy. - Gửi một đối tượng `proxy`: **cập nhật cấu hình đã lưu của user**, dùng chung với web và các lần tạo/đăng nhập tiếp theo, rồi chụp cấu hình cho job mới. Đây không phải tùy chọn chỉ dùng một lần. - Gửi `"proxy": {"mode":"none"}`: tắt proxy trong cấu hình đã lưu và cho job mới. `direct` có nghĩa là dùng một proxy cụ thể; kết nối không qua proxy là `none`. | `mode` | Ý nghĩa / `value` | `strategy` | |---|---|---| | `none` | Tắt proxy; bỏ `value` | Không ảnh hưởng khi tắt proxy | | `direct` | Một địa chỉ proxy | `per_account` hoặc `smart` | | `list` | Chuỗi các proxy, mỗi dòng một proxy; dùng `\n` trong JSON | `per_account` hoặc `smart` | | `api` | URL **HTTPS** của API cung cấp proxy | `per_account` hoặc `smart` | `strategy` mặc định là `per_account`: - `per_account` — **Gán proxy theo tài khoản**: trong Xưởng, tái sử dụng phân bổ đã lưu theo tài khoản. Nhiều tài khoản có thể dùng chung proxy khỏe qua các lượt chờ; không cần một endpoint độc quyền cho mỗi tài khoản. - `smart` — **Thông minh**: ưu tiên proxy khả dụng đang rảnh hoặc có ít lượt chờ nhất. Tái sử dụng proxy khỏe trong danh sách hoặc từ API; không gọi API để đổi IP cho mỗi tài khoản. Cả hai giữ nguyên proxy sau khi phiên bắt đầu, kể cả qua OTP. `direct` luôn dùng địa chỉ duy nhất đã nhập. Khi Apple trả HTTP 429 hoặc 503, đường proxy có thể tạm nghỉ cho lượt mới; lỗi mật khẩu, OTP hay tài khoản không phải lý do đổi proxy. Lượt đang chờ proxy chưa dùng worker đăng nhập. Các ví dụ dùng tên miền giả `.invalid`; thay bằng proxy/nhà cung cấp của bạn trước khi chạy. Thêm một trong các trường sau vào body tạo hoặc đăng nhập lại: ```json { "proxy": { "mode": "direct", "strategy": "per_account", "value": "http://PROXY_USER:PROXY_PASSWORD@proxy.example.invalid:8080" } } ``` ```json { "proxy": { "mode": "api", "strategy": "smart", "value": "https://proxy-provider.example.invalid/next?key=YOUR_PROVIDER_KEY" } } ``` ```json { "proxy": { "mode": "list", "strategy": "per_account", "value": "http://proxy-1.example.invalid:8080\nsocks5://PROXY_USER:PROXY_PASSWORD@proxy-2.example.invalid:1080" } } ``` ```json {"proxy": {"mode": "none"}} ``` Proxy hỗ trợ scheme `http`, `https`, `socks4`, `socks5`, `socks5h`; cần ghi rõ cổng. Dịch vụ kiểm tra proxy và URL nhà cung cấp dùng địa chỉ công khai. Khi giữ nguyên `mode`, `value` bị bỏ qua, `null` hoặc trống sẽ dùng lại giá trị đã lưu của chế độ đó; nếu chưa có giá trị phù hợp thì request bị từ chối. Muốn chỉ kế thừa toàn bộ cấu hình, bỏ `proxy` để giữ cả chiến lược đã lưu. API không trả thông tin xác thực proxy trong account/job. Giữ proxy có mật khẩu và URL nhà cung cấp có key trong file riêng hoặc prompt ẩn, không in chúng vào log. ### Nguồn OTP tự động Nếu có API SMS của riêng mình, thêm: ```json { "otp_source": { "url": "https://your-sms-provider.example/messages?key=YOUR_SMS_KEY", "phone": "+14092986901" } } ``` Dịch vụ ghép số với danh sách số tin cậy, gửi SMS và đọc mã từ nguồn đó theo cơ chế hiện có. `phone` có thể bỏ qua nếu tài khoản có duy nhất một số tin cậy phù hợp. Với nhiều số hoặc không xác định chắc số cần nhận, chuyển sang chọn thủ công. Trong khi tự động nhận mã, theo dõi `job.two_factor.automation.status`: `selecting`, `waiting`, `submitted`, hoặc `manual`. Nếu cần nhập thủ công, gọi: ```http POST /api/v1/workshop/jobs/JOB_ID/automation/cancel Content-Type: application/json {} ``` Sau đó dùng các endpoint chọn cách nhận mã/nhập OTP ở trên. URL nguồn SMS và thông tin đăng nhập không được trả trong phản hồi job. ## 5. Các endpoint còn lại Tất cả đường dẫn dưới đây bắt đầu bằng `/api/v1/workshop`. | Phương thức và đường dẫn | Quyền | Dữ liệu / tác dụng | |---|---|---| | `GET /me` | read | Chủ key và hạn mức | | `GET /accounts` | read | `page`, `page_size` (1–100), `search`, `status`, `visibility`, `folder_id`; trả `accounts`, `stats`, `pagination` | | `GET /folders` | read | Các thư mục của chủ API key, gồm số tài khoản đang hiện và đã gỡ | | `POST /folders` | write | `{"name":"Khách hàng A"}` → tạo thư mục; tối đa 200 thư mục/user | | `PATCH /folders/{id}` | write | `{"name":"Tên mới"}` → đổi tên thư mục | | `DELETE /folders/{id}` | write | Xóa thư mục, đưa tài khoản về chưa phân loại; giữ link và ghi chú | | `GET /accounts/{id}` | read | Một tài khoản của user | | `POST /accounts` | write | Tạo/login; bắt buộc Idempotency-Key | | `POST /accounts/{id}/login` | write | `password`, tùy chọn `proxy`, `otp_source`; bắt buộc Idempotency-Key mới cho lần login mới | | `PATCH /accounts/{id}` | write | Cập nhật `label`, `note`, `folder_id`, `hidden`, `enabled`; chỉ gửi trường cần đổi | | `DELETE /accounts/{id}` | write | Gỡ khỏi danh sách chính, giữ danh tính và link; trả `deleted:true,hidden:true,link_preserved:true` | | `POST /accounts/{id}/rotate-link` | write + code | Cấp link mới, link cũ không dùng được | | `POST /accounts/{id}/code` | code | Trả mã 6 số hiện tại | | `GET /jobs/{id}` | read | Trạng thái tiến trình | | `POST /jobs/{id}/factor-options` | write | Nạp lựa chọn xác thực | | `POST /jobs/{id}/send-code` | write | `mode`, tùy chọn `phone_id` | | `POST /jobs/{id}/otp` | write | `{"code":"123456"}` | | `POST /jobs/{id}/cancel` | write | Dừng tiến trình | | `POST /jobs/{id}/automation/cancel` | write | Dừng tự nhận mã SMS | Quyền `read`, `write`, `code` trong bảng tương ứng với `workshop:read`, `workshop:write`, `workshop:code`. ### Thư mục, ghi chú và gỡ khỏi danh sách `GET /accounts` mặc định chỉ liệt kê tài khoản đang hiện (`visibility=visible`). Dùng `visibility=hidden` để xem các tài khoản đã gỡ, hoặc `visibility=all` để xem cả hai. `folder_id` là ID thư mục của chủ key; `folder_id=unfiled` chỉ lấy tài khoản chưa phân loại; bỏ tham số để xem mọi thư mục. `search` cũng tìm trong ghi chú. Các bộ lọc áp dụng trước khi phân trang. Ví dụ sửa ghi chú, chuyển thư mục và đưa một tài khoản trở lại danh sách: ```http PATCH /api/v1/workshop/accounts/ACCOUNT_ID Authorization: Bearer xid_YOUR_API_KEY Content-Type: application/json {"note":"Khách hàng A\nGia hạn tháng sau","folder_id":"YOUR_FOLDER_ID","hidden":false} ``` `DELETE /accounts/{id}` giờ chỉ gỡ khỏi danh sách, giữ nguyên `device_api`, danh tính và trạng thái bật/tắt. Các tài khoản đã gỡ vẫn dùng được API chi tiết/lấy mã và vẫn tính vào hạn mức. Muốn chặn lấy mã, dùng `PATCH` với `{"enabled":false}`; muốn vô hiệu hóa link cũ, dùng `/rotate-link`. Không cần đăng nhập lại để đưa tài khoản đã gỡ trở lại danh sách. Link còn dùng được khi dịch vụ/state được giữ và Apple vẫn chấp nhận thiết bị; thao tác gỡ không tạo ra bảo đảm hiệu lực vĩnh viễn từ Apple. Các tài khoản đã bị xóa và thu hồi token ở phiên bản cũ không tự được khôi phục. ### Lỗi thường gặp | HTTP | Hướng xử lý | |---|---| | 400 / 422 | Kiểm tra JSON, trường dữ liệu, Idempotency-Key và proxy | | 401 | Key thiếu, sai, hết hạn hoặc đã mất hiệu lực | | 403 | Thiếu quyền scope hoặc user chưa được cấp quyền thao tác | | 404 | Tài khoản/job không tồn tại hoặc không thuộc user | | 409 | Trùng nội dung khác, tài khoản đang xử lý, vượt hạn mức hoặc phiên OTP không còn phù hợp | | 429 | Chờ theo `Retry-After`; `code_queue_full` là hàng đợi lấy mã đầy; hạn chế poll và số thao tác đồng thời | | 503 | Với `code_queue_timeout`, chờ `Retry-After` rồi lấy mã lại. Với `request_incomplete`, kiểm tra `account_id`/`job_id` trước khi tạo thao tác mới | Không dùng nội dung thông báo lỗi để quyết định luồng tự động. Ưu tiên HTTP status và trường `error`; giữ các tham chiếu account/job nếu API trả về. ## 6. Ví dụ cURL Ví dụ sau dành cho Bash/Zsh trên Terminal. Dùng prompt ẩn cho API Key thay vì viết key thật vào lịch sử lệnh: ```bash export XID_BASE_URL='https://workshop.example.com' read -rs 'XID_API_KEY?API Key: '; echo export XID_API_KEY ``` Với Bash, dòng prompt tương đương là `read -r -s -p 'API Key: ' XID_API_KEY; echo`. Tạo một thư mục làm việc riêng bằng `umask 077; mkdir -p xid-api-test`, rồi tạo `xid-api-test/account.json` với dữ liệu của bạn. Đây là **file chứa mật khẩu**, chỉ giữ trong lúc thử và xóa khi không cần. Nội dung mẫu: ```json { "apple_id": "YOUR_APPLE_ID", "password": "YOUR_PASSWORD", "label": "cURL test" } ``` Mẫu này kế thừa proxy đã lưu. Nếu cần đổi proxy, thêm một trường `proxy` ở mục 4 vào chính file `account.json`; lệnh cURL bên dưới gửi nguyên body đó. Không đặt URL chứa mật khẩu proxy hoặc key nhà cung cấp trực tiếp trong lệnh. Kiểm tra user: ```bash curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ "$XID_BASE_URL/api/v1/workshop/me" ``` Tạo tài khoản. Ghi lại giá trị `XID_OPERATION_ID` và giữ nguyên khi cần thử lại đúng request này: ```bash XID_OPERATION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')" printf 'Idempotency-Key: %s\n' "$XID_OPERATION_ID" curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: $XID_OPERATION_ID" \ --data-binary @xid-api-test/account.json \ "$XID_BASE_URL/api/v1/workshop/accounts" ``` Đặt `XID_JOB_ID` và `XID_ACCOUNT_ID` bằng `job.id`, `account.id` nhận được. Poll mỗi 3 giây khi cần: ```bash curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ "$XID_BASE_URL/api/v1/workshop/jobs/$XID_JOB_ID" ``` Nạp danh sách số SMS: ```bash curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ -H 'Content-Type: application/json' --data '{}' \ "$XID_BASE_URL/api/v1/workshop/jobs/$XID_JOB_ID/factor-options" ``` Sau khi GET job có danh sách số, tạo `xid-api-test/factor.json` dạng `{"mode":"sms","phone_id":1}` với **phone_id thật từ phản hồi**, rồi gửi: ```bash curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ -H 'Content-Type: application/json' --data-binary @xid-api-test/factor.json \ "$XID_BASE_URL/api/v1/workshop/jobs/$XID_JOB_ID/send-code" ``` Tạo `xid-api-test/otp.json` dạng `{"code":"YOUR_SIX_DIGIT_CODE"}` rồi gửi mã. Xóa file OTP sau khi dùng: ```bash curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ -H 'Content-Type: application/json' --data-binary @xid-api-test/otp.json \ "$XID_BASE_URL/api/v1/workshop/jobs/$XID_JOB_ID/otp" ``` Khi job hoàn tất, đọc tài khoản và lấy mã: ```bash curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ "$XID_BASE_URL/api/v1/workshop/accounts/$XID_ACCOUNT_ID" curl --silent --show-error --fail-with-body \ -H "Authorization: Bearer $XID_API_KEY" \ -H 'Content-Type: application/json' --data '{}' \ "$XID_BASE_URL/api/v1/workshop/accounts/$XID_ACCOUNT_ID/code" ``` Không thêm `-L`/`--location`: các ví dụ không tự chuyển API Key sang URL khác khi có redirect. Sau khi thử, xóa các file chứa thông tin tài khoản/OTP và `unset XID_API_KEY`. ## 7. Ví dụ Python hoàn chỉnh Tải `/api-docs/examples/workshop_api.py` hoặc lấy file `examples/workshop_api.py` trong source. Cài Python 3.10+ và thư viện `requests`: ```bash python3 -m pip install requests python3 workshop_api.py --base-url https://workshop.example.com ``` Mẫu sẽ hỏi API Key nếu biến `XID_API_KEY` chưa được đặt, sau đó hỏi Apple ID, mật khẩu và OTP bằng prompt phù hợp. Mật khẩu, API Key, OTP và proxy không được in ra. Khi cần, chọn SMS rồi chọn đúng số từ danh sách. Sau khi hoàn tất, chương trình in account ID, `device_api` và mã hiện tại. Không dùng `--proxy` thì mẫu kế thừa cấu hình proxy đã lưu. Thêm `--proxy` để nhập một proxy Apple bằng prompt ẩn; mẫu gửi `mode: direct`, `strategy: per_account` và cập nhật cấu hình đã lưu: ```bash python3 workshop_api.py --proxy ``` Để tích hợp `api`, `list`, `smart` hoặc tắt bằng `none`, đặt `proxy` trong body theo mục 4. Ví dụ Python dùng lớp `Client` trong file mẫu để gửi API cấp proxy với chiến lược `smart` (URL minh họa: `https://proxy-provider.example.invalid/next?key=YOUR_PROVIDER_KEY`): ```python from getpass import getpass from uuid import uuid4 from workshop_api import Client client = Client("https://workshop.example.com", getpass("API Key: ")) body = { "apple_id": input("Apple ID: ").strip(), "password": getpass("Mật khẩu Apple ID: "), "proxy": { "mode": "api", "strategy": "smart", "value": getpass("URL HTTPS của API cấp proxy: ").strip(), }, } operation_id = str(uuid4()) print("Idempotency-Key:", operation_id) try: result = client.call( "POST", "/api/v1/workshop/accounts", body, operation_id=operation_id, ) print("Job ID:", result["job"]["id"]) finally: body.clear() client.close() ``` Với `list`, `value` là chuỗi các proxy nối bằng `"\n"`; với `none`, dùng `body["proxy"] = {"mode": "none"}`. Để kế thừa, bỏ hoàn toàn khóa `proxy` hoặc gán `None`. Khi đăng nhập lại, bỏ `apple_id` và gọi `/api/v1/workshop/accounts/ACCOUNT_ID/login` với mật khẩu và cấu hình đã chọn. Giữ cùng Idempotency-Key và dữ liệu nếu thử lại request bị mất kết nối. Theo dõi job đang có, không tạo/login thêm: ```bash python3 workshop_api.py --resume-job JOB_ID ``` Đăng nhập lại account hiện có: ```bash python3 workshop_api.py --account-id ACCOUNT_ID ``` Thử lại một request tạo/login bị mất kết nối với cùng key đã in, nhập **đúng dữ liệu cũ**, giữ các tùy chọn `--label`, `--proxy`, `--account-id` giống lần đầu: ```bash python3 workshop_api.py --idempotency-key PREVIOUS_OPERATION_ID ``` Mẫu poll 3 giây/lần, dừng theo dõi sau 15 phút; dừng chương trình không tự hủy job trên server. Có thể tiếp tục bằng `--resume-job`. Mẫu không tự gửi lại các POST có khả năng tạo tác vụ và không theo redirect; khi gặp lỗi không rõ kết quả, kiểm tra dashboard trước. ## Nhập hàng loạt, xuất TXT và proxy đã lưu Các endpoint dưới đây dùng chung parser, hàng đợi, hạn mức và cấu hình proxy với Xưởng trên web. Mỗi API Key chỉ thao tác dữ liệu của chính chủ key, kể cả key của super admin. ### 1. Kiểm tra rồi gửi nhiều tài khoản Gửi POST /bulk/preview với text trước. Mỗi dòng gồm Apple ID và mật khẩu, phân cách bằng | hoặc khoảng trắng; mật khẩu có khoảng trắng phải đặt trong dấu nháy kép. Có thể thêm link OTP HTTPS hoặc số----link OTP. Đây là định dạng Xưởng; không nhận OTP có sẵn hoặc đáp án bảo mật. ```json {"text":"one@example.invalid|PASSWORD\ntwo@example.invalid|PASSWORD|14092986901----https://sms.example.invalid/read?key=YOUR_KEY"} ``` Preview chỉ trả số lượng, tình trạng từng dòng, has_otp_url và masked_phone; không đăng nhập, gửi SMS hay trả lại mật khẩu/link OTP. Sửa dòng không hợp lệ trước khi gửi. Tối đa 100 dòng không rỗng và 256 KiB UTF-8 mỗi lượt. Gửi cùng text đến POST /bulk với Idempotency-Key mới. Có thể thêm note, folder_id, profile_id, device_model, device_os, device_name và proxy như tạo một tài khoản. HTTP 202 trả batch, gồm id, status, counts và rows. Mỗi user chỉ có một batch hoạt động; API và web chia sẻ giới hạn này. ```bash curl "$BASE/api/v1/workshop/bulk" \ -H "Authorization: Bearer $XID_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $OPERATION_ID" \ --data-binary @accounts.json ``` GET /bulk trả 20 batch gần nhất. GET /bulk/{batch_id} trả tiến trình từng dòng; dùng job_id với các endpoint /jobs để chọn số, gửi SMS, nhập OTP hoặc dừng tự lấy mã như luồng một tài khoản. Poll khoảng 3 giây/lần, điều phối tổng request để không vượt 120 request/phút. POST /bulk/{batch_id}/cancel dừng dòng chờ và job đang chạy; không xóa tài khoản/link đã hoàn tất. Không tự gửi lại batch bằng key mới khi mất kết nối. Gửi lại cùng key và JSON sẽ trả batch hiện tại; request_in_progress/request_incomplete có thể kèm batch_id để tra cứu. Mật khẩu/link OTP chỉ ở bộ nhớ khi chạy; restart giữ lịch sử nhưng không tự khôi phục mật khẩu hay chạy lại dòng interrupted. Muốn thử lại, xem kết quả rồi gửi riêng các dòng lỗi với key mới; tài khoản mới/lỗi đủ điều kiện được dùng lại, không tạo bản sao. ### 2. Xuất các tài khoản đã chọn hoặc toàn bộ POST /accounts/export cần cả workshop:read và workshop:code vì nội dung chứa link nhận mã. Dùng scope=selected kèm account_ids (tối đa 10.000 ID), hoặc scope=all cùng visibility=visible/hidden/all và folder_id tùy chọn. ID tài khoản/thư mục của người khác làm yêu cầu thất bại, không xuất một phần. ```json {"scope":"selected","account_ids":["ACCOUNT_ID_1","ACCOUNT_ID_2"]} ``` Phản hồi JSON gồm text, filename, count, with_link, without_link. Lưu text thành TXT UTF-8: mỗi dòng apple_id|device_api, thêm |note khi có ghi chú. Ký tự gạch chéo ngược, | và xuống dòng trong trường dữ liệu được escape. Tài khoản chưa có link để trống cột link. Xuất chỉ đọc liên kết hiện có, không lấy mã, đăng nhập hay thay danh tính/link. ### 3. Lưu proxy dùng chung với web GET /proxy (workshop:read) chỉ trả tóm tắt, không có URL hoặc mật khẩu proxy. PUT /proxy (workshop:write) lưu mode, strategy và value cho các lượt mới; job đang chạy giữ tuyến đã chọn. GET /proxy/details cần workshop:write và trả value đầy đủ của chính user: không ghi log hoặc chia sẻ phản hồi này. ```json {"mode":"api","strategy":"per_account","value":"https://proxy-provider.example.invalid/next?key=YOUR_KEY"} ``` Các mode là none/direct/api/list, strategy là per_account/smart. Khi giữ nguyên mode, bỏ value hoặc gửi null/chuỗi trắng giữ giá trị đã lưu; mode=none xóa cấu hình. Tạo tài khoản hoặc batch mà bỏ proxy sẽ dùng cấu hình này. Lưu cấu hình không gọi Apple hoặc nhà cung cấp proxy. ### Các endpoint bổ sung | API | Scope | |---|---| | `POST /api/v1/workshop/bulk/preview` | `workshop:write` | | `POST /api/v1/workshop/bulk` | `workshop:write` | | `GET /api/v1/workshop/bulk` | `workshop:read` | | `GET /api/v1/workshop/bulk/{batch_id}` | `workshop:read` | | `POST /api/v1/workshop/bulk/{batch_id}/cancel` | `workshop:write` | | `POST /api/v1/workshop/accounts/export` | `workshop:read + workshop:code` | | `GET /api/v1/workshop/proxy` | `workshop:read` | | `PUT /api/v1/workshop/proxy` | `workshop:write` | | `GET /api/v1/workshop/proxy/details` | `workshop:write` | ### Python ```python from getpass import getpass from pathlib import Path from uuid import uuid4 from workshop_api import Client client = Client("https://workshop.example.com", getpass("API Key: ")) text = Path("accounts.txt").read_text(encoding="utf-8") try: preview = client.preview_bulk(text) if preview["can_submit"]: operation_id = str(uuid4()) print("operation_id:", operation_id) result = client.submit_bulk(text, operation_id=operation_id) print("batch_id:", result["batch"]["id"]) finally: text = "" client.close() ```