# Xgodo v1 API Reference ## Conventions - Base path is shown per router under “Mount path”. - Auth: “Public” needs no token; “User” requires `Authorization: Bearer `. ## Routers - **Auth** (`/`) — 10 endpoints. Signup, login, account activation, password reset, captcha verification. - **Other** (`/api/v1/`) — 4 endpoints. Miscellaneous public endpoints: FCM token registration, admin notifications, bootstrap script, bug report upload. - **Account** (`/api/v1/user`) — 10 endpoints. User profile management: read/edit profile, upload picture, search workers, manage worker lists, referral counts. - **Jobs** (`/api/v1/job`) — 33 endpoints. Job lifecycle: CRUD, search, applicants, task submission, planned tasks, sharing, hiring, invitations. - **Automation Data** (`/api/v1/automation`) — 2 endpoints. Read and download aggregated automation execution data. - **Automation Project** (`/api/v1/automation-project`) — 43 endpoints. Automation project management: CRUD, file editor, git versioning, sharing, templates, performance and out-of-steps stats. - **Dashboard** (`/api/v1/dashboard`) — 5 endpoints. Dashboard data: stats, notifications, site configuration, account summary, landing page video. - **Payments** (`/api/v1/payments`) — 6 endpoints. Deposits, withdrawals, transactions, bonuses, account upgrades, payment gateway callbacks. - **Referrals** (`/api/v1/referrals`) — 2 endpoints. Read referral history for workers and employers. - **Support** (`/api/v1/support`) — 4 endpoints. Support tickets: list, view chat, reply, create. - **Chat** (`/api/v1/chat`) — 11 endpoints. Direct 1:1 messaging: conversations, send/edit/delete messages, emoji reactions, media & document attachments, read receipts, unread counts. Real-time over Socket.IO. - **Report** (`/api/v1/report`) — 1 endpoints. Submit a user report. - **Device** (`/api/v1/device`) — 32 endpoints. Device lifecycle and operations: register, list, control, market publish/rent, automation control, airplane triggers, proxy passwords. - **Notifications** (`/api/v1/notification`) — 2 endpoints. List and mark notifications as read. - **Labelling** (`/api/v1/labelling`) — 6 endpoints. Labelling task lifecycle: create, fetch, resolve, end iteration, history, app metadata. - **API Key** (`/api/v1/developer/api/token`) — 4 endpoints. Developer API token management: get, create, regenerate, delete. - **Job Bucket** (`/api/v1/bucket`) — 5 endpoints. Per-job, per-device key/value bucket store. - **Device Bucket** (`/api/v1/device-bucket`) — 3 endpoints. Per-device key/value bucket store (job-independent). ## Auth - Mount path: `/` - Default auth: Public - Endpoints: 10 ### Authentication Public endpoints used to bootstrap a session: signup with optional captcha + activation, login (user and admin), forgot/reset password, and account activation. Tokens are JWTs signed with `JWT_SECRET` (user) or `JWT_ADMIN_SECRET` (admin); password reset uses `JWT_PASSWORD_CHANGE_SECRET`. #### POST /signup — Sign up Create a new user account. Validates username/email format and uniqueness, captcha (when enabled), email domain blacklist, and IP duplicates (when enabled). On success either activates the user immediately or sends an activation email, depending on the `account_activation` site setting. Request body: - `usertype` (string, required): One of `"worker"`, `"employer"`, `"both"`. - `username` (string, required): 4–48 chars. Must start with a letter or digit; only letters, digits, `-`, `.`, `_` allowed. Case-insensitively unique. - `email` (string, required): Email address. Case-insensitively unique. Must not match a blacklisted domain. - `password` (string, required): Plain-text password (hashed server-side). - `fname` (string, required): First name. - `lname` (string, required): Last name. - `country_id` (string, required): Country reference id from the `countries` collection. - `phone` (string, required): Phone number. - `captcha` (string): Plain captcha text (required when `signup_captcha` is enabled site-wide). - `captchasolution` (string): Encrypted captcha value returned in the `Cache-Control` header by GET `/captcha` (required when `signup_captcha` is enabled). - `ref_id` (string): Referrer's `user_id`. Used to populate `refId_level1/2/3`. Responses: - **201** — User created. If site `account_activation` is enabled, status is `inactive` and an activation email is sent; otherwise the user is `active`. { "message": "user created" } - **400** — Validation failed. Returns an array of `{ field, message }` errors covering: invalid username, blocked IP duplicate, blacklisted email domain, email already in use, username already in use, missing/wrong captcha. - **500** — Unexpected error. { "error": "internal server error" } #### GET /signup — Signup form configuration Returns whether the signup form should show a captcha and whether new accounts require email activation. Responses: - **200** — Returns the site-wide flags the signup form needs. { "signup_captcha": true, "account_activation": false } Example response: ```json { "signup_captcha": true, "account_activation": false } ``` #### POST /activate — Activate account Activates a previously-created account using the activation key emailed at signup. Request body: - `activation_key` (string, required): The 32-char hex key emailed to the user at signup. Responses: - **200** — Account marked active. { "website_title": "...", "website_url": "...", "msg": "You have successfully activated your account" } - **401** — Account is already active. { "msg": "You have already activated your account" } - **404** — No user has the supplied activation key. { "msg": "User Not Found " } #### POST /resend_activate — Resend activation email Re-sends the activation email for an inactive account. Request body: - `email` (string, required): Email of an `inactive` account. Responses: - **200** — Activation email re-sent (best effort). { "msg": "activation sent ..." } - **404** — No inactive user with that email. { "msg": "not found ..." } #### POST /login — Log in Standard user login. Pass `Rememberme: "true"` for a 30-day token. The `admin` flag enables admin-impersonation: an admin can mint a user token by supplying their admin JWT and the target `user_id`. Request body: - `username` (string, required): Username (case-insensitive; trimmed). - `password` (string, required): Plain-text password (compared against the bcrypt hash). - `Rememberme` (string): `"true"` extends token expiry to 30 days; otherwise 2 days. - `admin` (string): When set, switches to admin-impersonation flow: requires `token` (admin JWT) + `user_id` to mint a regular-user token for the given account. - `token` (string): Admin JWT, used only when `admin` is set. - `user_id` (string): Target user's `user_id`, used only when `admin` is set. Responses: - **200** — Returns the user JWT plus profile fields. Side effect: updates `last_ip`, `last_login`, `last_activity`, increments `login_count`. { "msg": "Login successful", "token": "", "username": "alice", "email": "...", "usertype": "worker", "user_id": "...", "status": "active", "block_reason": null } - **401** — Missing username/password, invalid credentials, or invalid admin-impersonation params. { "incorrect password or username": "..." } - **500** — Unexpected error. #### POST /admin/login — Admin log in Authenticates the single admin user. If no admin exists yet, one is auto-created with `username: "admin"` and password `"000"` — change immediately. Request body: - `username` (string, required): Admin username (any value — only `usertype: "admin"` user is matched). - `password` (string, required): Admin password. - `Rememberme` (string): `"true"` → 30-day token, else 5-day. Responses: - **200** — Returns the admin JWT (signed with `JWT_ADMIN_SECRET`). On first ever call with no admin user present, a default admin (`username: "admin"`, password: `"000"`) is created automatically. { "msg": "Login successful", "token": "", "email": "...", "usertype": "admin", "user_id": "..." } - **401** — Missing fields or invalid credentials. #### GET /captcha — Captcha image Returns a JPEG captcha image. The encrypted answer is in the `Cache-Control` response header — send it back as `captchasolution` on signup. Responses: - **200** — Returns a JPEG captcha image. The encrypted plaintext is sent back in the `Cache-Control` response header — pass it to signup as `captchasolution`. #### POST /forgot — Send password-reset email Emails a reset link with a 1-hour reset JWT. Request body: - `email` (string, required): Email of the account requesting a reset link. Responses: - **200** — Email queued (1-hour reset token signed with `JWT_PASSWORD_CHANGE_SECRET`). { "msg": "password changed ..." } - **400** — `email` missing. { "msg": "Email is required" } - **404** — No user with that email. { "msg": "user not found ..." } #### POST /reset_password — Reset password Uses the bearer reset token to set a new password (≥ 6 chars). Request body: - `Authorization` (header, required): `Bearer ` — the 1-hour reset JWT from the email. - `password` (string, required): New password. Must be ≥ 6 characters. Responses: - **200** — Password updated. { "username": "alice", "msg": "password changed ..." } - **401** — Reset link missing/expired/invalid, or password < 6 chars. { "error": "invalidpassword", "message": "Password must be at least 6 characters" } - **500** — Hashing/save failure. { "error": "error insertion", "message": "somting went wrong" } #### POST /verify_link — Verify reset link Checks whether a password-reset token is still valid (and unexpired). Lets the UI decide whether to render the reset form or an expired-link message. Request body: - `Authorization` (header, required): `Bearer ` — the 1-hour reset JWT from the email. Responses: - **200** — Reset token is valid and unexpired. { "msg": "link is valid ..." } - **401** — Token missing, expired, or signature mismatch. { "error": "jwt expired", "message": "Access key is invalid. Please provide a valid link." } ## Other - Mount path: `/api/v1/` - Default auth: Public - Endpoints: 4 ### Other Miscellaneous public endpoints. None of these use bearer auth — instead each gates itself with a static shared secret or a global setting. #### POST /api/v1/fcm — Register an admin FCM token Saves a Firebase Cloud Messaging token to the `admin` group and subscribes it to the `admin` topic so the device receives admin-broadcast notifications. Request body: - `token` (string, required): Firebase Cloud Messaging device token to register. - `secret` (string, required): Static shared secret. Must equal `be83da549a144164b7227ffede8514a0` (gates this otherwise-unauthenticated endpoint). Responses: - **200** — Token saved and subscribed to the `admin` Firebase topic. { "success": true } - **400** — Missing `token` or wrong `secret`. { "success": false, "message": "Token is required" } Example response: ```json { "success": true } ``` #### POST /api/v1/notifyAdmin — Send a push notification to admins Fires a Firebase push to the `admin` topic. Used by external services (e.g. RMS, frontend error reporters) that have the shared secret. Request body: - `secret` (string, required): Static shared secret. Must equal `bede2eff-de8f-415c-b07e-070c42f140e5` (otherwise the request silently no-ops but still returns 200). - `title` (string, required): FCM notification title. - `body` (string, required): FCM notification body text. - `channel` (string): One of `DEFAULT`, `OUT_OF_STEPS`, `TIMEOUT`, `FAILED_TO_START_AGENT`, `FRONTEND_ERROR`, `SYSTEM_ALERT`. Anything unknown falls back to `DEFAULT`. Responses: - **200** — Always returns success. The push is sent only when the secret and required fields are valid; failures are swallowed. { "success": true } Example response: ```json { "success": true } ``` #### GET /api/v1/bootstrap.js — Fetch automation bootstrap JS Public endpoint used by automation clients (RemoteMobile WebView) to download the currently active compiled bootstrap script. The active version is set by an admin via the bootstrap admin endpoints. Responses: - **200** — Returns the compiled JavaScript of the currently active automation bootstrap (`Content-Type: application/javascript`). When no bootstrap is configured or the file is missing, returns a placeholder comment. - **500** — Unexpected error reading the bootstrap file. Returns `// Error loading bootstrap` as JS. #### POST /api/v1/bugReport — Upload a bug report file Accepts a single file upload (`multipart/form-data`, field name `file`) and stores it on disk under `bugReports/`. Disabled when the site-wide `accept_bug_report` setting is off. Request body: - `file` (file (multipart/form-data), required): Bug report file (logs, screenshots, etc.). Max size 512 MB. The socket timeout is bumped to 10 minutes for the upload. Responses: - **200** — File saved under `bugReports/` with a timestamp suffix. Returns the stored filename. { "message": "File uploaded successfully", "filename": "crash_1713170040000.log" } - **400** — Bug reporting is disabled site-wide (`accept_bug_report` setting), or no file was attached. { "error": "Bug reporting is not enabled" } - **500** — Disk write failed. { "error": "Error uploading file: " } Example response: ```json { "message": "File uploaded successfully", "filename": "crash_1713170040000.log" } ``` ## Account - Mount path: `/api/v1/user` - Default auth: User (both | employer | worker) - Endpoints: 10 ### My Account The authenticated user's profile and the worker-discovery surface (search workers, top workers, my-workers shortlist). #### GET /api/v1/user/ — Get my profile Returns the caller's profile (or another user's when `?id=` is provided), with worker skills joined in. Strips secrets (password, IPs, activation/unsubscribe keys, login_count). Query parameters: - `id` (string (ObjectId)): Optional `user_id` to look up. Defaults to the authenticated user. Must be a valid ObjectId. Responses: - **200** — Returns the user record split into `UserData_db` (form-bound profile fields like username, email, address, notification flags) plus the rest of the user document (timestamps, balance/rate as strings, etc.) and `workerSkill` (array of skill names). - **404** — Invalid `id` or no user with that id. { "error": "User with this Id not found " } #### PUT /api/v1/user/ — Edit my profile Multiplexed update endpoint. Pass exactly one of `Userdata` (profile fields), `ChangePassword` (rotate password), or `workerSkill` (replace skill set). Request body: - `Userdata` (object): When supplied, updates the profile. Allowed keys: `email`, `fname`, `lname`, `avatar`, `about`, `company`, `address`, `address2`, `city`, `state`, `zip`, `phone`, `newsletter`, `notification_Newsletter`, `notification_JobInvitation`, `notification_NewJob`, `notification_NewOrder`, `notification_Task_completed`. Empty/null values are dropped before writing. Email uniqueness is checked against other users. - `ChangePassword` (object): When supplied, changes the password. Shape: `{ current_password, new_password, confirm_password }`. `current_password` is verified against the bcrypt hash before update. - `workerSkill` (object): Map of `{ skillName: boolean }`. The selected skills (true values) replace the user's `worker_skills` document. Responses: - **201** — Update applied. Body shape varies by which sub-object was passed. - **401** — `ChangePassword` flow: current password incorrect. { "message": "The current password you provided is incorrect. Please try again." } - **409** — `Userdata` flow: email already in use by another user. { "error": "Email already exist" } - **500** — DB write failed. { "error": "" } #### POST /api/v1/user/details — Get a public profile Public profile lookup for any active user. Returns identity, recent jobs, task aggregates, and (for employer accounts) employer-side stats. Anonymous access depends on the `show_users_unreg` site setting. Request body: - `user_id` (string (ObjectId), required): Profile to look up. Responses: - **200** — Returns the public profile of an active user: identity fields, recent active jobs (with `taskdone` count), aggregated task stats (total, satisfied/not_satisfied, totalEarned), worker_skills, fevorites count, `is_myworker` (does the caller list this user in `my_workers`), and — for non-worker accounts — `employer_stat` (totalJobs, averageJobPrice, total_Spend, confirmed/declined/total task counts). - **401** — Anonymous caller and `show_users_unreg` site setting is off. { "error": "Unauthorized", "message": "Access token is missing. Please provide a valid token." } - **404** — Invalid or missing user. { "error": "user with this Id not found " } #### PUT /api/v1/user/upload_profile — Upload profile picture Multipart upload. Stores the file via `upload_profilePicture` and sets `avatar` to `.`. Request body: - `image` (file (multipart/form-data), required): Profile picture (single file under field name `image`). Stored using `upload_profilePicture`. The user's `avatar` is set to `.`. Responses: - **201** — Avatar updated. { "status": { "status": "Your profile image has been updated successfully!" } } - **500** — DB error. { "error": "" } #### POST /api/v1/user/workers — Search workers Worker discovery with rich filters (rate range, premium, online, skill, location). Anonymous access depends on the `show_workers_unreg` site setting. Request body: - `Query` (string): Free-text search across `fname`, `lname`, `username`. - `page` (string): 1-indexed page. Default `1`. - `limit` (string): Page size. Default `10`. - `SortBY` (string): Field name to sort by (in addition to `premium: -1`). - `Orantaition` (string): `asc` or `desc`. Default desc. - `MinRate` (string): Filter workers with `rate >= MinRate`. - `MaxRate` (string): Filter workers with `rate <= MaxRate`. - `activeWorker` (boolean): Restrict to `status: 'active'`. - `premiumWorker` (boolean): Restrict to `premium: true`. - `onlineWorker` (boolean): Restrict to users active in the last 20 minutes. - `skills` (string): Filter by a single skill name. - `location` (string): Case-insensitive regex match against `country_id`. - `getOptions` (boolean): When `true`, additionally returns `Options` with the available `skills` and `country` lists for filter UI. Responses: - **200** — Returns a paginated worker list. Each entry includes profile + per-worker task aggregates (total, done, pending, notdone). When `getOptions` is set, also returns `Options.skills` / `Options.country`. - **401** — Anonymous caller and `show_workers_unreg` site setting is off. { "error": "Unauthorized", "message": "Access token is missing. Please provide a valid token." } #### GET /api/v1/user/searchWorkerForJob — Look up a single worker by id or username Used by the hire-worker UI. Excludes the caller. Query parameters: - `query` (string, required): Either a `user_id` (ObjectId) or a `username` (case-insensitive exact match). The caller is excluded. Responses: - **200 (found)** — Found a matching active worker. { "success": true, "id": "...", "username": "..." } - **200 (not found)** — No match. { "success": false, "message": "User not found." } #### POST /api/v1/user/topworkers — Top workers (last 70 days) Top 100 active workers ranked by total `confirmed` task earnings over the last 70 days. **Public** — no auth required. Responses: - **200** — Returns the top 100 workers by total `confirmed` job-task earnings over the last 70 days. Each row carries identity, total earnings, and full task aggregates. #### POST /api/v1/user/update_myworkers — Add/remove a worker from my list Toggles a worker in the caller's `my_workers` array. `action: "add"` performs `$addToSet`; anything else performs `$pull`. Request body: - `worker_id` (string (ObjectId), required): Worker `_id` to add or remove. - `action` (string, required): `"add"` ($addToSet) or anything else ($pull) on the caller's `my_workers` array. Responses: - **200** — List updated. { "message": "Worker successfully added to your list." } - **400** — Missing `worker_id`. { "error": "Action and worker_id are required." } - **404** — Caller user record not found. { "message": "User not found or no changes made." } #### POST /api/v1/user/myworkers — Get my workers list Returns the caller's curated worker shortlist with each worker's identity and activity. Responses: - **200** — Returns `my_workers` (joined with the active user records — identity + rate + last_activity + premium) and `totalworkers`. - **404** — Caller has no `my_workers` list yet. { "msg": "you dont have any worker list " } #### POST /api/v1/user/ref_count — Track a referral click Public click-counter for referral landing pages. Increments the referrer's `ref_clicks` by 1. Request body: - `ref_id` (string (ObjectId), required): Referrer's `user_id`. Increments their `ref_clicks` by 1. Responses: - **200** — Counter incremented. { "status": "count updated " } - **404** — Invalid `ref_id`. { "error": "User with this Id not found " } ## Jobs - Mount path: `/api/v1/job` - Default auth: User (both | employer | worker) - Endpoints: 33 ### Jobs (CRUD + Status) Read, list, update, and delete jobs. The owner can change a job's active/inactive state; pending jobs can be deleted. Most reads net `job_price` against the platform-wide referral cut. #### GET /api/v1/job/ — Get a job by id (or all jobs) When `job_id` is supplied returns a single job; otherwise returns the full collection. Used by debugging tooling — public listings live under `POST /search`. Query parameters: - `job_id` (string (ObjectId)): Job `_id`. When omitted, returns all jobs (admin/debugging). Responses: - **200** — When `job_id` is supplied, returns `{ count: 1, data: }`. Otherwise returns all jobs (`count`, `data: Job[]`). Used by admin tooling. - **404** — Invalid or unknown `job_id`. { "error": "Job with this Id not found " } #### DELETE /api/v1/job/ — Delete a pending job Owner-only. Deletion is restricted to jobs in `pending` status. Request body: - `job_id` (string (ObjectId), required): Job to delete. Only the owner may delete, and only while the job is `pending`. Responses: - **200** — Deleted. { "message": "Job deleted successfully" } - **404** — Invalid `job_id` or no matching pending job owned by caller. { "message": "somting went wrong" } #### PUT /api/v1/job/ — Activate or deactivate a job Toggles a job between `active` and `inactive`. Requires owner or `shared_edit`. For `screenshot` jobs being activated, also queues a fresh labelling task on the attached device. Request body: - `job_id` (string (ObjectId), required): Job to update. Caller must be owner or have `shared_edit`. - `action_type` (string, required): `"active"` or `"inactive"`. Other values are rejected. Cannot be used on jobs in `complete | pending | declined` states. Responses: - **200** — Job status flipped. For `screenshot` jobs being activated, also kicks off a fresh labelling task on the attached device. - **400** — `action_type` invalid, or device for a `screenshot` job is missing. { "error": "Invalid Action or Action type is required" } - **401** — Job is in a state that cannot be toggled. { "error": "unauthorized to perform this action " } - **404** — Job not found / not owned. #### POST /api/v1/job/myjobs — My jobs (with shared) Returns the caller's jobs (paginated) plus jobs shared to them. Each row carries task counts (`job_done`, `pending_task`, `running_task`) and planned-task usage (`used_planned_tasks`, `added_planned_tasks`). Request body: - `page` (integer): 1-indexed page for the caller's own jobs (the `data` list). Defaults to `1`. - `limit` (integer): Page size for both lists. Capped at `200`. Defaults to `10`. - `shared_page` (integer): 1-indexed page for the jobs shared with the caller (the `shared_data` list). Paginates independently of `page`. Defaults to `1`. - `status` (string): Filter by job `status` (applies to the caller's own jobs only; shared jobs are always returned regardless of status). - `Query` (string): Free-text search across `title` and `description` (case-insensitive). - `SortBY` (string): Reserved (the pipeline currently always sorts by `added: -1`). - `orientation` (string): Reserved. Responses: - **200** — Returns the caller's jobs (paginated by `page`, with total `count`) plus `shared_data` (jobs shared to them, paginated independently by `shared_page`, with total `shared_count`; the current shared page is echoed back as `shared_page`) carrying `permission: "view" | "edit"`. Each job carries `job_done`, `pending_task`, `running_task`, `used_planned_tasks`, `added_planned_tasks`. Shared jobs also carry `owner_username`. The per-job task status counters (`job_done`, `pending_task`, `running_task`) are cached server-side per `job_id` for 2 minutes (in-flight requests deduped); the response carries a single `cached_before_seconds` (integer, computed at response time — clock-drift immune) reflecting the OLDEST cache snapshot across both lists, so the UI can render "counts: ~X ago". - **404** — No jobs. { "error": "No Jobs found for this user" } ### Create Create new jobs and load the create-job form configuration. #### POST /api/v1/job/create — Create a job Validated by the centralised `jobCreateService`. Throws `ValidationError` (Zod) → 400, `BusinessLogicError` → its declared status code (e.g. insufficient balance, category min-price), generic 500 otherwise. Request body: - `(see jobCreateService)` (object, required): Job creation payload (validated by the service in `controller/jobs/job_create.service.ts`). Common fields include `title`, `description`, `category`, `proof`, `is_proof_file`, `files`, `Target_Workers`, `positions`, `job_price`, `duration`, `job_type`, `payment_type`, `automation_id`, `featured`, `premium`, `custom_vars`. Validation errors are returned per-field. Responses: - **201** — Job created. The service returns the new job + any computed fields (price, fees). - **400** — Validation error. Returns `{ errors: [{ field, message, code? }] }`. - **402+** — Business-logic error (e.g. `INSUFFICIENT_BALANCE`, `CATEGORY_MIN_PRICE`). Status code is set by the thrown `BusinessLogicError`. - **500** — Unexpected error. { "errors": [{ "field": "general", "message": "An unexpected error occurred. Please try again.", "code": "INTERNAL_ERROR" }] } #### GET /api/v1/job/create — Create-job form configuration Returns the caller's `active_balance` and the site settings + categories + countries needed to render the create-job form. Responses: - **200** — Returns the data the create-job form needs: caller's `active_balance`, the relevant subset of site settings (currency, fees, limits, referral percentages, etc.), and the catalogues of `category` and `country`. ### Browse jobs The public job-search surface. Anonymous access is gated by the site `show_jobs_unreg` setting. #### POST /api/v1/job/search — Search jobs Returns a paginated job list with employer info and per-job task counts. Optionally returns latest confirmed tasks and category counts via `get_category_latestTask`. Request body: - `page` (integer): Defaults to `1`. - `limit` (integer): Defaults to `10`. - `Query` (string): Free-text search across `title` and `description`. - `SortBY` (string): Field name to sort by. Defaults to `added`. Always combined with `featured: -1`. - `orientation` (string): `asc` or `desc`. Defaults to `desc`. - `min_price` (string|number): Minimum `job_price`. - `location` (string): Substring match against `Target_Workers`. - `job_category` (string): Filter by category. - `Featured` (boolean): Restrict to `featured: true`. - `Premium` (boolean): Restrict to `premium: true`. - `get_category_latestTask` (boolean): Also return `latest_task` (10 most recent confirmed tasks), `category_filter`, and `device_owner_share`. Responses: - **200** — Returns `{ Jobs, totalCount }`. Jobs include user info, per-job task counts (total/done/pending/running/satisfied), and `job_price` net of total referral cuts. Visibility is gated by job filters (`minRegistrationDays`, `minUplineTaskCompletion`, `minUserCompletedTasks`) and by `allowed_workers`. - **401** — Anonymous caller and `show_jobs_unreg` site setting is off. { "error": "Unauthorized", "message": "Access token is missing. Please provide a valid token." } - **500** — `device_owner_share` site setting missing. { "error": "device owner share invalid" } #### GET /api/v1/job/job_details — Get a job's public details Public detail view used by the job page. Returns the job, task counts, and a summary of the employer (`total_spend`, `total_tasks`, `total_job`). Increments the job's view count. Query parameters: - `job_id` (string (ObjectId), required): Job `_id`. When omitted, returns all jobs (admin/debugging). Responses: - **200** — Returns the job (with sensitive employer fields stripped), task counts (`totla_tasks_count`/`tasks_done_count`/`tasks_confirmed_count`/`tasks_running_count`/`tasks_pending_count`), employer summary (total spend, total tasks, total jobs), and `is_jobcompleted | is_premimum | is_myjob | is_avialblecountry` flags. Increments the job's `views` counter. The task counts are cached server-side per `job_id` for 2 minutes (in-flight requests are deduped); the staleness is exposed as `cached_before_seconds` (integer, seconds since the cache snapshot, computed server-side at response time so client clock drift doesn't matter) — counts may lag reality by up to ~2 min. - **404** — Invalid or unknown `job_id`, or visibility filters block the caller. { "error": "job with this Id not found " } - **500** — `device_owner_share` site setting missing. #### GET /api/v1/job/availability — Job availability Live counts of devices/workers available for the given job — drives the worker queue UI. Query parameters: - `job_id` (string (ObjectId), required): Job `_id`. When omitted, returns all jobs (admin/debugging). Responses: - **200** — Returns availability metrics for the job: `availableDevices`, `busyDevices`, `devicesAvailableForWorker`, `devicesBannedForWorker`, `workersBusy`, `workersInQueue`. Used to decide whether the worker should queue. - **401** — Caller not authenticated. - **404** — Invalid `job_id`. #### GET /api/v1/job/top_jobs_success_rates — Top jobs success-rate matrix Returns the latest 10 jobs that have automation success-rate data, the top 10 devices on those jobs, and the (job × device) success matrix. Used for the worker landing dashboard. Responses: - **200** — Returns `{ jobs, devices, matrix }` for the 10 most-recent jobs that have automation success-rate data. The matrix is keyed by `:` and contains success/fail counts and rates. Devices include owner info. ### Worker submission flow Endpoints workers use to apply, submit proof, upload files, and resubmit declined tasks. #### GET /api/v1/job/job_apply — Pre-apply check Verifies the worker can apply (rating, country, premium gating), claims a `custom_var` for this run when the job has one, and returns the job + worker context. Query parameters: - `job_id` (string (ObjectId), required): Job to apply to. - `compact` (string): When `"true"`, the response is just `verify_job.custom_vars` (a single chosen variable for this run) — used by the apply form. Responses: - **200** — Returns `{ verify_job, verify_user }`. `verify_user.CanApply_again` is false when the worker already applied to a `few_times: false` job. When the job has `custom_vars`, a random eligible variable is selected (lastActive older than `duration` minutes) and its `lastActive` bumped. - **404** — Job not found or visibility filters block. #### POST /api/v1/job/submit_JobTask — Submit a job task Files (single or multi) can be uploaded inline as base64. Validated against the job's file schema and per-file/site size limits. Daily-application limit middleware applies. Request body: - `job_id` (string (ObjectId), required): Active job. Must target the worker's country (or `worldwide`). - `job_proof` (string, required): Free-text proof. Required even when files are also uploaded. - `proof_file` (string): Filename for a single primary proof file (when the job's `is_proof_file` is true). Pair with `proof_file_base64`. - `proof_file_base64` (string (base64)): Encoded primary proof file. Size capped by site `proof_max_size`. - `proof_files_base64` (object[]): Multiple proof files matching the job's `files` schema. Each entry: `{ fileName, fieldName, base64 }`. Each must match a `field_name` defined on the job; required ones must be present. Size capped per file by job spec and site setting. - `used_varaible` (object): When the job has `custom_vars`, the variable claimed via `GET /job_apply` (`{ _id, ... }`). Responses: - **201** — Task submitted. Pending review by the employer (or auto-approved per site config). Earnings net of the referral cut are reserved. - **400** — Missing proof, low rating, premium-only job, missing required files, oversized files, missing `used_varaible`, or duplicate apply on a non-`few_times` job. { "error": "Proof required" } - **404** — Job not active or not available in caller's country, or invalid `job_id`. { "error": "ethier Job is inactive or ths job is not avilabel in your country" } #### POST /api/v1/job/upload_proof — Upload a proof file Multipart file upload (field `image`). Stored via `upload_profileProof`. Daily-application limit middleware applies. Request body: - `image` (file (multipart/form-data), required): Single proof file under field name `image`. Stored via `upload_profileProof`. The handler simply confirms the upload — link it to a task by name via the subsequent submit call. Responses: - **201** — File saved. { "message": "Proof uploaded successfully" } #### POST /api/v1/job/task/resubmit — Resubmit a declined/failed task Updates `job_proof` and flips the task back to `pending`. Caller must own the task. Request body: - `job_task_id` (string (ObjectId), required): Worker's task to resubmit. Caller must be the original `worker_id`. - `job_proof` (string, required): Updated proof text. Sets the task back to `pending`. Responses: - **201** — Task updated. { "msg": "task has been updated successfully" } - **400** — `job_proof` is not a string. - **404** — Task not found for this worker. ### Finished tasks (worker history) Worker's view of finished tasks (and tasks they own as device owner of an automation iteration). #### POST /api/v1/job/jobs_finished — My finished tasks Paginated list (or single task) of the worker's finished tasks. Proof fields are stripped when the caller is not the job owner. Query parameters: - `task_id` (string (ObjectId)): When supplied, returns a single finished task. Without it, returns a paginated list. Request body: - `page` (integer): Defaults to `1`. - `limit` (integer): Defaults to `10` (server caps page size at 10). - `exclude_failed_automation_tasks` (boolean): Excludes `failed`/`declined` automation iterations from the list. Responses: - **200** — List or single task where the caller is the worker or the device owner of an automation iteration. Proof fields are stripped when the caller is not the job owner. `job_price` is net of total referral cuts. - **404** — Invalid `task_id` / no matching task. #### DELETE /api/v1/job/jobs_finished — Delete a pending task (worker) Worker can delete their own pending submission. The task is archived to `JobTaskDeleted` for audit. Returns refreshed counters and the worker's full task list. Request body: - `task_id` (string (ObjectId), required): Pending task owned by the caller. The task is archived to `JobTaskDeleted` and removed; counts are recomputed and returned. Responses: - **200** — Task deleted; returns refreshed counts and the worker's full task list. - **404** — Invalid id or task not pending / not owned. ### Job applicants (employer) Employer-side endpoints to review applicants, bulk-update statuses, and export the task table. #### POST /api/v1/job/job_applicants — List applicants for a job Returns the job (with embedded `job_tasks`). Sortable, searchable (worker username + proof + ip + title), paginated. Caller must be owner or `shared_view`/`shared_edit`. Request body: - `job_id` (string (ObjectId), required): Job. Caller must be owner, `shared_view`, or `shared_edit`. - `task_id` (string (ObjectId)): Filter to a specific job-task. - `page` (integer): Defaults to `1`. - `pagesize` (integer): Defaults to `10`. - `sortby` (string): Field on the embedded job_task. Defaults to `added`. - `order` (string): `asc` or `desc`. Defaults to `desc`. - `search` (string): Free-text search across worker username / `job_proof` / `job_title` / `worker_ip`. Responses: - **200** — Returns the job (with embedded `job_tasks`) joined with worker info. `job_price` and `price` are stringified. Per-job status counters (`total_task`, `job_done`, `pending_tasks`, `satisfied_tasks`, `declined_tasks`, `failed_tasks`) plus planned-task counters (`total_planned_tasks`, `used_planned_tasks`) are included. The status counters are cached server-side per `job_id` for 2 minutes (in-flight requests are deduped); the staleness is exposed as `cached_before_seconds` (integer, seconds since the cache snapshot, computed server-side at response time so client clock drift doesn't matter) — counts may lag reality by up to ~2 min. - **404** — Invalid `job_id` or job not accessible. #### PUT /api/v1/job/job_applicants — Bulk update applicants Bulk transition a set of `job_task_id`s to a new status. `confirmed` triggers payment + referral cuts; `planned` requeues declined automation tasks. Request body: - `job_id` (string (ObjectId), required): Job. Caller must be owner or `shared_edit`. - `JobTasks_Ids` (string[], required): Tasks to update (bulk). - `status` (string, required): One of `"planned"`, `"status"`, `"confirmed"`, `"processing"`, `"notcomplete"`, `"declined"`, `"pending"`. `"planned"` is a special transition that re-queues declined tasks for an automation job. - `comment` (string): Reviewer comment stored on each task. Responses: - **200** — Bulk update applied. For `confirmed`, payments and referral cuts are processed. - **400** — Invalid status, missing fields, or `planned` requested for a non-automation job. { "error": "Invalid status. Valid statuses are: ..." } - **404** — Missing required fields or job not found. #### GET /api/v1/job/job_applicants — Download tasks CSV Streams a CSV of the job's tasks. For automation jobs, the columns are derived from `job_proof` JSON keys plus `submitted_task_status` and `submitted_task_comment`. Query parameters: - `jobId` (string (ObjectId), required): Job to export. Caller must own it. - `filter` (string, required): `"all"` or `"pending"`. Responses: - **200** — Streams a CSV (`Content-Type: text/csv`). For automation jobs the columns are derived from `job_proof` JSON keys plus `submitted_task_status`, `submitted_task_comment`. Excludes `running` rows; if `filter: pending`, restricts to `pending`. - **400** — Validation error. { "success": false, "message": "..." } - **404** — Job not found / not owned. { "success": false, "message": "Job not found" } #### POST /api/v1/job/job_iteration — Iteration solved actions Returns the solved labelling tasks for an iteration plus the RMS action-name → action-id mapping. Caller must own the underlying job. Request body: - `iteration_id` (string (ObjectId), required): Iteration whose solved labelling tasks the employer wants to inspect. Caller must be the job owner. Responses: - **200** — Returns `{ tasks, actionMappings }` — the iteration's `solved` labelling tasks plus the RMS action-name → action-id mapping, so the employer can render the recorded action sequence. - **400** — Missing `iteration_id` or caller is not the job owner. { "message": "Permission denied" } - **404** — No solved tasks. { "message": "No tasks found for this iteration ID" } - **500** — RMS request failed. #### POST /api/v1/job/job_device_info — Device owner + country lookup Quick lookup used by the applicants UI to display the device owner's username and the device country/name for a row. Request body: - `device_owner_id` (string (ObjectId)): Looks up the owner's `username`. - `device_id` (string (UUID)): Looks up the device's `country` and `name`. Responses: - **200** — Returns `{ device_owner, device_country, device_name }` (any may be `null`). ### Planned tasks Pre-queued inputs for automation jobs. A planned task is consumed when an automation iteration claims it. #### GET /api/v1/job/planned_tasks — List planned tasks Returns unconsumed planned tasks for a job (paginated, searchable). Query parameters: - `job_id` (string (ObjectId), required): Job. Caller must own or have it shared. - `search` (string): Substring match against `input` (case-insensitive). - `page` (integer): Min `1`. Defaults to `1`. - `limit` (integer): Min `1`, max `100`. Defaults to `10`. - `sortOrder` (string): `"asc"` or `"desc"`. Defaults to `desc`. Responses: - **200** — Returns unconsumed planned tasks for the job (`job_task_id: null`) with the parent job and total count. { "success": true, "data": { "job": { ... }, "plannedTasks": [ ... ], "total": 42 } } - **400** — Validation error. - **404** — Job not found / not accessible. #### POST /api/v1/job/planned_tasks — Add planned tasks Bulk-add inputs (each ≤ 100 KB). Caller must have `edit` access on the job, or — for automation agents — must own the matching running task. Sub-task limit applies. Request body: - `job_id` (string (ObjectId), required): Job. Caller must have `edit` access. For automation agents, the parent task's job owner must own or have `shared_edit` on the target job, and the target job must have `allow_sub_tasks` enabled (the target job may differ from the parent's job). - `inputs` (string[], required): Array of input strings (each ≤ 100 000 chars). Each becomes a planned task. - `remote_device_id` (string (UUID)): Optional device hint. - `run_immediately` (boolean): When `true`, attempts to schedule immediately. Defaults to `false`. Responses: - **200** — Planned tasks created. - **400** — Validation error, sub-task limit reached, or sub-tasks not allowed for this job. { "success": false, "error": "Sub-tasks are not allowed for this job" } - **404** — Job not found. #### PATCH /api/v1/job/planned_tasks — Edit a planned task Updates a planned task's input. Cannot edit tasks that are already assigned to a job_task. Request body: - `planned_task_id` (string (ObjectId), required): Planned task to edit. Must not yet be claimed by a job task. - `input` (string, required): New input string (≤ 100 000 chars). Responses: - **200** — Updated. { "success": true } - **400** — Validation error or task already assigned. { "success": false, "message": "Cannot edit a planned task that is assigned to a pending / successful job task" } - **404** — Planned task or its job not found. #### DELETE /api/v1/job/planned_tasks — Delete planned tasks Bulk delete planned tasks the caller owns. Request body: - `planned_task_ids` (string[] (ObjectId[]), required): One or more planned task ids to delete. Responses: - **200** — Deleted. - **400** — Validation error. #### GET /api/v1/job/download_planned_tasks — Download planned tasks CSV Streams a CSV of unconsumed planned tasks. Columns are derived from `input` JSON keys. Query parameters: - `jobId` (string (ObjectId), required): Job. Caller must own / have shared. Responses: - **200** — Streams a CSV of unconsumed planned tasks. Header row is derived from `input` JSON keys. - **400** — Validation error. - **404** — Job not found / not accessible. ### Sharing Share a job with collaborators in `view` or `edit` mode. Owner-only operations. #### GET /api/v1/job/sharing — Get sharing list Returns the per-job `view` and `edit` user lists with usernames resolved. Query parameters: - `job_id` (string (ObjectId), required): Job. Caller must be owner. Responses: - **200** — Returns `{ sharing: { view: [{user_id, username}], edit: [{user_id, username}] } }`. - **400** — Invalid `job_id`. - **404** — Job not found / not owned. #### POST /api/v1/job/sharing — Add user to sharing list Adds a user to either the `view` or `edit` list. Idempotent (`$addToSet`). Cannot share with self. Request body: - `job_id` (string (ObjectId), required): Job (owned by caller). - `username` (string): Recipient by username (provide either this or `user_id`). - `user_id` (string (ObjectId)): Recipient by user_id (provide either this or `username`). - `type` (string, required): `"view"` or `"edit"`. Responses: - **200** — User added (idempotent — uses `$addToSet`). { "success": true, "user": { "user_id": "...", "username": "..." } } - **400** — Validation error or trying to add the owner to their own list. - **404** — Job not owned, or recipient user not found. #### DELETE /api/v1/job/sharing — Remove user from sharing list Pulls a user from the chosen list. Request body: - `job_id` (string (ObjectId), required): Job (owned by caller). - `user_id` (string (ObjectId), required): User to remove. - `type` (string, required): `"view"` or `"edit"`. Responses: - **200** — User removed. - **400** — Validation error. - **404** — Job not owned. ### Hiring & invitations Direct worker invitations: employer hires specific workers; workers see and respond to invitations. #### GET /api/v1/job/hire_worker — My active jobs (for hire) Returns the caller's currently active jobs — used by the hire-worker UI to pick a job. Responses: - **200** — Returns the caller's currently `active` jobs (id, title, price). Used by the hire-worker UI. [ { "user_id": "...", "title": "Like 10 posts", "job_id": "...", "job_price": 0.05 } ] #### POST /api/v1/job/hire_worker — Invite workers to a job Creates a `WorkerInvites` row per worker. Caller must own the job and the job must be `active`. Request body: - `job_id` (string (ObjectId), required): Active job owned by caller. - `workers` (string[], required): Worker `_id`s to invite. - `comment` (string): Optional message attached to each invite. Responses: - **200** — Worker invites created. { "message": "Invitation sent" } - **400** — Job not found or not owned/active. { "message": "Job not found" } #### GET /api/v1/job/invitations — My pending invitations (worker) Returns the worker's pending invitations enriched with job + employer info and a total count. Responses: - **200** — Returns the worker's pending invitations with job + employer info, plus a `total_invitation` count. #### POST /api/v1/job/decline_Invitation — Decline an invitation Marks a worker's invitation as declined. Request body: - `invite_id` (string (ObjectId), required): Worker invitation `_id` (the worker must own it). Responses: - **200** — Marked declined. { "msg": "Invitation declined" } ## Automation Data - Mount path: `/api/v1/automation` - Default auth: User (both | employer | worker) - Endpoints: 2 ### Automation Data Read and export the per-row data that automations push back during execution. Each row stores a JSON string under `data` plus the device, automation, and git commit (`automation_version_id`) it came from. Scoped to the authenticated user. #### GET /api/v1/automation/data — List collected automation data Returns up to 100 most recent data rows for the given automation (newest first) and the total count. Optionally clears all rows first. Query parameters: - `automationId` (string (ObjectId), required): MongoDB ObjectId of the automation whose collected data you want to read. - `clearData` (string, required): Either `"true"` or `"false"`. When `"true"`, deletes all data rows for this automation owned by the authenticated user **before** returning the (empty) result. Responses: - **200** — Returns up to 100 most recent data rows (newest first) and the total count for this user/automation. - **400** — Validation error — `automationId` is not a valid ObjectId, or `clearData` is not the string `"true"` / `"false"`. { "success": false, "message": "Invalid automation ID format" } - **401** — Missing or invalid bearer token. Example response: ```json { "success": true, "count": 2, "data": [ { "_id": "65f1...", "user_id": "64aa...", "device_id": "8b2f...uuid", "device_name": "Pixel 5 — Lab", "automation_id": "65aa...", "automation_version_id": "a1b2c3d4e5f6...40 hex", "data": "{\"username\":\"alice\",\"followers\":123}", "createdAt": "2026-04-15T08:14:00.000Z" } ] } ``` #### GET /api/v1/automation/data/download — Download collected automation data as CSV Streams every data row for this automation back as CSV. The header row is derived from the JSON keys of the most recent row; values that are objects are re-serialised with `JSON.stringify`. Query parameters: - `automationId` (string (ObjectId), required): MongoDB ObjectId of the automation. All data rows owned by the authenticated user for this automation are streamed back. Responses: - **200** — Streams a CSV file (`Content-Type: text/csv`, `Content-Disposition: attachment; filename=automation--.csv`). The first row is the header derived from the JSON keys of the most recent data row; subsequent rows are values for each stored data row, newest first. - **400** — `automationId` is not a valid ObjectId. { "success": false, "message": "Invalid automation ID format" } - **401** — Missing or invalid bearer token. ## Automation Project - Mount path: `/api/v1/automation-project` - Default auth: User (both | employer | worker) - Endpoints: 43 ### Projects CRUD for automation projects. An automation is backed by an on-disk git repository plus a Mongo document. Most write operations require owner or `shared_editor`. #### POST /api/v1/automation-project/create — Create a project Creates the Mongo doc, allocates a code directory, writes the default file set, and initialises a git repo with the initial commit. Request body: - `name` (string, required): 1–225 chars. - `description` (string, required): 1–10100 chars. Responses: - **201** — Project created. Allocates a code directory, writes default files (`.gitignore`, `tsconfig.json`, `main.ts`, `main.js`), initialises a git repo with the initial commit, and sets `local_path` to the new id. - **400** — Zod validation error. { "success": false, "message": "" } - **500** — Unexpected error. { "success": false, "message": "Unknown error" } #### GET /api/v1/automation-project/list — List projects Returns the caller's owned and shared projects. With `?include_stats=true`, also includes basic counters per row. Query parameters: - `include_stats` (string): `"true"` to include task/run statistics on each row. Responses: - **200** — Returns `{ owned: Automation[], shared: Automation[] }`. Shared = automations where the caller appears in any of `shared_execution | shared_editor | shared_logs`. Each row carries `has_icon` and (with `include_stats`) basic counters. #### GET /api/v1/automation-project/:id — Get a project Returns the project metadata, input schemas, requirements, ownership info, and template flag. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Returns the automation metadata: id, name, description, entry_file, local_path, timestamps, the three input schemas (`automation_parameters_schema`, `job_variables_schema`, `bucket_schema`), requirements (`min_android_version`, `min_app_version`), `has_icon`, `is_owner`, `owner_username` (when not owner), and `is_template`. - **400** — Zod validation error. { "success": false, "message": "" } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### PATCH /api/v1/automation-project/:id — Update project metadata Updates `name`, `description`, and/or `entry_file`. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `name` (string): 1–225 chars. - `description` (string, required): 1–10100 chars. - `entry_file` (string): ≤ 500 chars. Path to the entry file (e.g. `main.js`). Responses: - **200** — Updated. Caller must be owner or `shared_editor`. - **400** — Zod validation error. { "success": false, "message": "" } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### PATCH /api/v1/automation-project/:id/input-schemas — Update input schemas Updates one or more of `automation_parameters_schema`, `job_variables_schema`, `bucket_schema`. Each is a `{ fields: FieldSchema[] }` (≤ 50 fields, supports recursive nested fields). Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `automation_parameters_schema` (object | null): Schema describing automation parameters. Shape: `{ fields: FieldSchema[] }` with up to 50 fields. Each field supports `name`, `type` (`string|number|boolean|array|object|any`), `required`, validations (min/max length, regex, enum, range, items, properties). - `job_variables_schema` (object | null): Same shape as `automation_parameters_schema`. Defines per-job variable inputs. - `bucket_schema` (object | null): Same shape. Validates the per-(job, device) bucket payloads. Responses: - **200** — Updated. Returns the three current schemas. - **400** — Zod validation error. { "success": false, "message": "" } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### PATCH /api/v1/automation-project/:id/options — Update options (schemas + requirements + template) Update input schemas, version requirements (`min_android_version`, `min_app_version`), and (owner-only) the `is_template` flag. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `automation_parameters_schema` (object | null): Schema describing automation parameters. Shape: `{ fields: FieldSchema[] }` with up to 50 fields. Each field supports `name`, `type` (`string|number|boolean|array|object|any`), `required`, validations (min/max length, regex, enum, range, items, properties). - `job_variables_schema` (object | null): Same shape as `automation_parameters_schema`. Defines per-job variable inputs. - `bucket_schema` (object | null): Same shape. Validates the per-(job, device) bucket payloads. - `min_android_version` (integer | null): 1–100. Minimum Android SDK version required. - `min_app_version` (integer | null): ≥ 1. Minimum RemoteMobile app build number required. - `is_template` (boolean): Owner-only. Marks the automation as a template. Setting `false` also clears `is_template_public`. Responses: - **200** — Updated. Returns the three schemas, both `min_*_version` values, and `is_template`. - **400** — Zod validation error. { "success": false, "message": "" } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### DELETE /api/v1/automation-project/:id — Delete a project Owner-only. Removes both the on-disk directory and the Mongo doc. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Project deleted (db row + on-disk directory). { "success": true } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } ### Bootstrap types The TypeScript source the editor uses to type-check user automations against the runtime API. #### GET /api/v1/automation-project/bootstrap-types — Get active bootstrap types Returns the active automation bootstrap TypeScript source code, used by the in-editor IntelliSense. Returns `bootstrap: null` if none configured. Responses: - **200** — Returns the active automation bootstrap TypeScript source (used by the editor's IntelliSense). When no bootstrap is configured, returns `bootstrap: null`. { "success": true, "bootstrap": { "typedCode": "...", "version": 12 } } ### Templates Apply curated starter projects on top of an existing project. #### GET /api/v1/automation-project/templates — List available templates Returns the templates the caller can apply: their own `is_template` projects plus public ones (`is_template_public`). Responses: - **200** — Returns the templates the caller can apply: their own templates plus public ones (`is_template_public`). Each row carries `id`, `name`, `description`, `has_icon`, `is_own`, `is_public`, `owner_username`. #### POST /api/v1/automation-project/:id/apply-template — Apply a template Copies all files from the template's last commit into the target project's working directory. Requires editor access on the target. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `template_id` (string (ObjectId), required): Template id (must be marked `is_template` and either owned by the caller or public). Responses: - **200** — Files from the template's last commit are copied into the target project's working directory. Caller must have editor access to the target. - **400** — Zod validation error. { "success": false, "message": "" } - **404** — Either project or template not found / not accessible / legacy. { "success": false, "message": "Template not found or not accessible" } ### Icon Project icon stored as `icon.webp`. The GET is public. #### PUT /api/v1/automation-project/:id/icon — Upload icon Uploads the icon (max 2 MB). Resized to 512×512, converted to WebP, RGB/RGBA forced. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Icon uploaded. Resized to 512×512, converted to WebP (RGB/RGBA), saved as `icon.webp` in the automation directory. Max upload size 2 MB. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } - **500** — Unexpected error. { "success": false, "message": "Unknown error" } #### GET /api/v1/automation-project/:id/icon — Get icon (public) Public — returns the WebP bytes (`Content-Type: image/webp`, cached for 1h). Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Returns the WebP icon bytes (`Content-Type: image/webp`, cached for 1 hour). **Public** — no auth required. - **404** — Icon not found. #### DELETE /api/v1/automation-project/:id/icon — Delete icon Removes `icon.webp` (no-op if missing). Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Icon removed (no-op if it didn't exist). { "success": true, "message": "Icon deleted" } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } ### Sharing Three sharing tiers: `execution` (run the automation), `editor` (edit code + reads), `logs` (read logs). Owner-only operations. #### GET /api/v1/automation-project/:id/sharing — Get sharing lists Returns the three sharing lists with usernames resolved. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Owner-only. Returns `{ sharing: { execution: [{id, username}], editor: [...], logs: [...] } }`. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### POST /api/v1/automation-project/:id/sharing — Add a sharing entry Adds a user (looked up by username) to one of the three sharing lists. Idempotent; cannot share with self or duplicate. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `username` (string, required): Recipient username (case-sensitive). - `type` (string, required): `"execution"`, `"editor"`, or `"logs"`. Responses: - **200** — User added (idempotent). Cannot add yourself or an already-shared user. - **400** — Validation, self-share, or duplicate. { "success": false, "message": "User already has access" } - **404** — Automation not owned by caller, or recipient not found. #### DELETE /api/v1/automation-project/:id/sharing — Remove a sharing entry Removes a user (by id) from one of the three lists. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `userId` (string (ObjectId), required): User to remove. - `type` (string, required): `"execution"`, `"editor"`, or `"logs"`. Responses: - **200** — Removed. { "success": true } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } ### Files File-level CRUD inside the project's git working directory. Paths are validated to prevent traversal. TypeScript files are auto-compiled — paired `.js` files are managed implicitly and cannot be edited / renamed / deleted directly. #### GET /api/v1/automation-project/:id/files — List files Returns a recursive file tree of the project (excluding `.git`, `node_modules`, hidden files except `.gitignore`). Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Returns a recursive file tree of the project (excluding `.git`, `node_modules`, hidden files except `.gitignore`). - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### GET /api/v1/automation-project/:id/file — Read a file Returns the UTF-8 content of a file at `?path=`. Query parameters: - `path` (string, required): Project-relative file path. Path-traversal-safe (`isPathSafe`). Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Returns `{ content, path }` (UTF-8). - **400** — Invalid path. - **404** — File not found. { "success": false, "message": "File not found" } #### PUT /api/v1/automation-project/:id/file — Write a file Writes content (≤ 50 MB). For `.ts` files, the .js compile output is also written. Editing a paired `.js` is rejected. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `path` (string, required): Project-relative path. For `.ts` files, the .ts and the compiled .js are both written; writing to a paired `.js` is rejected. - `content` (string, required): ≤ 50 MB. Responses: - **200** — Written. { "success": true, "path": "main.ts" } - **400** — Validation, unsafe path, or attempt to edit a paired `.js`. { "success": false, "message": "Cannot edit compiled JavaScript file. Edit the TypeScript source instead." } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### DELETE /api/v1/automation-project/:id/file — Delete a file Deletes a file. For `.ts`, the paired `.js` is also removed; deleting a paired `.js` is rejected. Query parameters: - `path` (string, required): Project-relative path. Deleting a `.ts` also deletes the paired `.js`. Deleting a paired `.js` directly is rejected. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Deleted. { "success": true, "path": "src/old.ts" } - **400** — Invalid path or paired `.js`. { "success": false, "message": "Invalid file path" } - **404** — File not found. #### POST /api/v1/automation-project/:id/directory — Create a directory Creates a directory recursively. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `path` (string, required): Project-relative directory path. Created recursively. Responses: - **201** — Created. { "success": true, "path": "src/utils" } - **400** — Zod validation error. { "success": false, "message": "" } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### POST /api/v1/automation-project/:id/rename — Rename / move Renames a file or directory. For `.ts`, the paired `.js` is moved too. Updates `entry_file` if it pointed at the renamed file. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `oldPath` (string, required): Existing path. - `newPath` (string, required): Target path. For `.ts` source, the paired `.js` is also moved. The automation's `entry_file` is updated when it pointed at the renamed file. Responses: - **200** — Renamed. { "success": true, "oldPath": "...", "newPath": "..." } - **400** — Invalid path or attempt to rename a paired `.js`. - **404** — Source file not found. #### GET /api/v1/automation-project/:id/package — Get full project package Returns every project file as `{ files: { '': '' } }`. Used by device sync flows. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Returns every project file as `{ files: { '': '' } }`. Hidden files except `.gitignore`, `node_modules`, and `.git` are excluded. Used for sending the project to a device. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } ### Git versioning An `isomorphic-git` repository per project. Backs the editor's history, diff, revert, and discard flows. #### GET /api/v1/automation-project/:id/git/status — Working-tree status Returns `{ hasChanges, changes: [{ path, status }] }` (`new` | `modified` | `deleted`). Hidden files skipped. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Returns `{ hasChanges, changes: [{ path, status }] }`. `status` is `new`, `modified`, or `deleted`. Hidden files are skipped. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### POST /api/v1/automation-project/:id/git/commit — Create a commit Stages all changes (incl. deletions) and commits with the supplied message. Author is derived from the user. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `message` (string, required): 1–1000 chars. Responses: - **201** — Commit created. Returns `{ commit: { hash, message } }`. Stages all changes (including deletions). Author info is derived from the user. - **400** — Nothing to commit, or validation error. { "success": false, "message": "No changes to commit" } - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### GET /api/v1/automation-project/:id/git/history — Commit history Returns up to `limit` commits (default 50). Query parameters: - `limit` (integer): Defaults to `50`. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Returns the commit history. Each entry: `hash`, `message`, `author` (name), `authorEmail`, `timestamp` (ISO). - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### GET /api/v1/automation-project/:id/git/commit/:hash — Get commit details Returns the commit's metadata (`hash`, `message`, `author`, `timestamp`, `tree`, `parent`). Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `hash` (string, required): Git commit OID (path param). Responses: - **200** — Returns the commit's `hash`, `message`, `author`, `timestamp`, `tree`, and `parent` array. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### GET /api/v1/automation-project/:id/git/diff/:hash — Get diff for a commit Diff against the commit's first parent (or all-added for the initial commit). Returns `{ changes: [{ path, status }] }`. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `hash` (string, required): Git commit OID (path param). Responses: - **200** — Returns `{ diff: { hash, parentHash, changes: [{ path, status }] } }`. `status` is `added` | `modified` | `deleted`. For initial commits, `parentHash` is `null` and all entries are `added`. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### POST /api/v1/automation-project/:id/git/revert/:hash — Revert to commit Resets the working tree to the contents of the given commit. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `hash` (string, required): Git commit OID (path param). Responses: - **200** — Reverts the working directory to the state at the supplied commit. Returns success. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### GET /api/v1/automation-project/:id/git/file/:hash — Read file at commit Returns the UTF-8 content of `?path=` as it was at the given commit. Query parameters: - `path` (string, required): Project-relative file path. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `hash` (string, required): Git commit OID (path param). Responses: - **200** — Returns `{ content, path, hash }` for the file as it existed at the commit. - **404** — File not present at this commit. { "success": false, "message": "File not found at this commit" } #### GET /api/v1/automation-project/:id/git/diff-working/:hash — Diff commit vs working Returns full file diffs (`{ files: [{ path, status, original, modified }] }`) between the commit and the current working tree. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `hash` (string, required): Git commit OID (path param). Responses: - **200** — Diffs a commit against the current working directory and returns `{ files: [{ path, status, original, modified }] }`. - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### GET /api/v1/automation-project/:id/git/diff-commits/:hash1/:hash2 — Diff between commits Returns full file diffs between two commits. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `hash1` (string, required): From commit. - `hash2` (string, required): To commit. Responses: - **200** — Returns `{ fromHash, toHash, files: [{ path, status, original, modified }] }` (full content for each side). - **404** — Automation not found, is a legacy automation (no `local_path`), or you don't have access. { "success": false, "message": "Automation not found" } #### POST /api/v1/automation-project/:id/git/discard-file — Discard changes to a file If the file existed in HEAD, restore it. If it's new, delete it. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `path` (string, required): File to discard. New files are deleted; tracked files are restored from HEAD. Responses: - **200** — Discarded. `action` is `restored` or `deleted`. - **400** — No commits in repo. - **404** — File not found. #### POST /api/v1/automation-project/:id/git/discard-all — Discard all changes Reset the working tree to HEAD (deletes new files, restores tracked ones). Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. Responses: - **200** — Working tree reset to HEAD. New files deleted, tracked files restored. { "success": true, "message": "All changes discarded" } - **400** — No commits in repo. ### Out-of-Steps (editor view) Cursor-paginated view of `out_of_steps` records, scoped to automations the caller owns or can edit. Used by the editor to step through OOS occurrences and mark them solved/skipped. #### GET /api/v1/automation-project/out-of-steps — Get OOS record (cursor-paginated) Returns the current OOS record plus next/prev cursor info. Filterable by device name/id and automation name/id. Query parameters: - `cursor` (string): Cursor for the current record. - `before_cursor` (string): Cursor to fetch the previous record. - `after_cursor` (string): Cursor to fetch the next record. - `device_name` (string): Substring filter against device name (case-insensitive). - `remote_device_id` (string): Substring filter against `remote_device_id`. - `automation_name` (string): Filter by automation name. Resolved against accessible automations only. - `automation_id` (string): Filter by automation id. Must be accessible. Responses: - **200** — Returns the current OOS record (`result`), `total`, `currentPage`, cursors, and `hasNext` / `hasPrev`. Limited to automations the caller owns or has editor access to. #### GET /api/v1/automation-project/out-of-steps/page-number — Get page number for cursor Returns the 1-indexed page number for a given cursor. Called in the background to keep pagination UI in sync. Query parameters: - `cursor` (string, required): Cursor whose page index you want. Responses: - **200** — Returns the 1-indexed page number for the cursor — used to keep the editor's pagination UI in sync. #### PATCH /api/v1/automation-project/out-of-steps/:id/status — Mark OOS solved/skipped One-shot status set (cannot be changed once marked). Records a tracking entry attributing the decision to the caller. Request body: - `id` (string (ObjectId), required): OOS record id (path param). - `status` (string, required): `"solved"` or `"skipped"`. Responses: - **200** — Marked. The OOS record is updated and an `OutOfStepsStatusTracking` entry is created with `marked_by = caller`. - **400** — Invalid id, status already set (cannot change). - **403** — Caller is not owner / editor of the automation. { "success": false, "message": "You don't have permission to mark this out of steps record" } - **404** — OOS record not found. ### OOS progress Aggregate stats per automation for the OOS-resolution dashboard. #### GET /api/v1/automation-project/out-of-steps-progress/stats — OOS stats Returns total / solved / skipped counts for an automation in a date range, optionally filtered by who marked them. Query parameters: - `automation_id` (string (ObjectId), required): Must be in the caller's accessible set. - `start_date` (string): ISO date. Defaults to 7 days ago. - `end_date` (string): ISO date. Defaults to now. - `user_id` (string (ObjectId)): Restrict resolved counts to ones marked by this user. Responses: - **200** — Returns `{ stats: { total, solved, skipped }, dateRange }` for the automation. - **400** — Invalid `automation_id` or dates. - **403** — Automation not accessible. { "success": false, "message": "Access denied" } #### GET /api/v1/automation-project/out-of-steps-progress/users — Users who resolved OOS Returns per-user solved/skipped/total counts (and last activity) for the automation in the date range. Query parameters: - `automation_id` (string (ObjectId), required): Must be in the caller's accessible set. - `start_date` (string): ISO date. Defaults to 7 days ago. - `end_date` (string): ISO date. Defaults to now. Responses: - **200** — Returns the list of users who have marked OOS records for this automation in the date range, with per-user solved/skipped/total counts and last activity. ### Performance Time-series task-result data for the automations the caller has access to. #### GET /api/v1/automation-project/performance/filters — Performance filters Returns the automations and devices that have task-result data in the date range, plus the canonical result types. Query parameters: - `startDate` (string): ISO. Defaults to 1 day ago. - `endDate` (string): ISO. Defaults to now. Responses: - **200** — Returns the automations and devices that have task-result data in the date range (scoped to the caller's accessible automations), plus the canonical list of `resultTypes` and their colours. #### GET /api/v1/automation-project/performance/data — Performance time series Bucketed time series per automation, keyed by `TaskResultType`. Filter by automation ids and device id; choose bucket via `xAxis`. Query parameters: - `startDate` (string): ISO. Defaults to 1 day ago. - `endDate` (string): ISO. Defaults to now. - `xAxis` (string): Bucket size — e.g. `days`, `hours`. Defaults to `days`. - `automationIds` (string): Comma-separated automation ids, or `"all"`. Defaults to all accessible. - `deviceId` (string): Single device id or `"all"`. Responses: - **200** — Returns time-series data per automation, bucketed at `xAxis` granularity. Each entry has `timestamp`, `total`, and `results` keyed by `TaskResultType`. Sorted by total tasks descending. ### Device sync (patch / zip) Used by RemoteMobile devices to fetch the project. **Public** routes — auth is performed by checking that the supplied `remote_device_id` is authorised for this automation (owner / shared / iteration-linked). #### POST /api/v1/automation-project/:id/patch — Get sync patch (incremental or full) With no `commit_id`, returns a full snapshot of HEAD. With `commit_id`, returns the incremental patch from that commit to HEAD. Snapshots are cached on disk per commit. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `remote_device_id` (string, required): Calling device's `remote_device_id`. Authorisation passes if the device's owner (or current renter) has access to the automation, or there is an `AutomationIteration` linking them. - `commit_id` (string): Git OID the device currently has. When supplied the response is an incremental patch from `commit_id` to HEAD; otherwise it's a full snapshot. Responses: - **200** — Returns either a full snapshot or an incremental patch describing the file deltas needed to bring the device to HEAD. Cached to disk per commit. - **400** — Validation error or no commits in repo. - **403** — Device not authorised for this automation. { "success": false, "message": "Device not authorized to access this automation" } - **404** — Automation not found / legacy. #### POST /api/v1/automation-project/:id/zip — Get project ZIP Returns the project as a single ZIP. Backed by an LRU-cached bundle. Request body: - `id` (string (ObjectId), required): Path param. Automation `_id`. Most write endpoints require owner or `shared_editor`; reads also accept `shared_execution`, `shared_logs`, `visibility: public`, or a job-share to a job using this automation. - `remote_device_id` (string, required): Calling device. Same authorisation rules as `/patch`. - `commit_id` (string): Optional commit pin. Responses: - **200** — Returns the project as a single ZIP (`Content-Type: application/zip`). Backed by an LRU-cached bundle for efficient large-project serving. - **403** — Device not authorised. - **404** — Automation not found / legacy. ## Dashboard - Mount path: `/api/v1/dashboard` - Default auth: User (both | employer | worker) - Endpoints: 5 ### Dashboard Endpoints powering the home/landing dashboard, the navbar badge bar, and the My Account / Site Config screens. #### GET /api/v1/dashboard/ — Dashboard summary Aggregates the home-screen payload: user profile, job/task counts, recent activity, total earnings, and a status-bar chart. Responses: - **200** — Returns the user profile (with sensitive fields like password / IP / activation_key stripped), plus aggregated dashboard data: total job stats, latest 10 task applicants on owned jobs, latest 20 finished tasks (proof fields wiped, prices net of referral cuts), pending/running task counts on owned jobs, total earnings (sum of `job_payment` transactions), site `auto_confirm_days`, and a bar-chart `total_task_status` ({ task_status, applicants_task }). - **401** — Bearer token missing or `req.user.id` not present. { "error": "Unauthorized", "message": "User ID is missing" } - **404** — User document not found for the token's `id`. { "error": "Not Found", "message": "User not found" } #### GET /api/v1/dashboard/notification — Navbar badge counts Counts that drive the navbar badges (running/pending tasks, pending invitations, unread support replies, referral counts, balance). Responses: - **200** — Returns counts that drive the navbar badges: running/pending tasks on the user's own jobs, pending worker invitations, unread support replies addressed to the user, referral counts per level, and the user's wallet balance. { "running_tasks": 1, "pending_tasks": 4, "invitations": 0, "supports": 2, "my_referrals": { "level1": 3, "level2": 5, "level3": 1, "total_ref": 9 }, "balance": 12.34 } - **500** — Unexpected error. { "error": "Internal Server Error", "message": "..." } Example response: ```json { "running_tasks": 1, "pending_tasks": 4, "invitations": 0, "supports": 2, "my_referrals": { "level1": 3, "level2": 5, "level3": 1, "total_ref": 9 }, "balance": 12.34 } ``` #### GET /api/v1/dashboard/siteconfigration — Site configuration + categories + countries Returns whitelisted site-wide settings (currency, payouts, premium fees, referral %, etc.) along with the catalogues for job categories and countries. Responses: - **200** — Returns whitelisted site settings (currency, payouts, job rules, premium fees, referral percentages, homepage flags, etc.), plus the full list of job categories and countries. { "siteConf": { ... }, "category": [ ... ], "country": [ ... ] } #### GET /api/v1/dashboard/myaccount — My account Returns the user record (sensitive fields stripped) and a derived `active_balance` that subtracts pending withdrawals and active job funds from the wallet balance. Responses: - **200** — Returns the user document (sensitive fields stripped) plus a derived `active_balance = balance − pending_withdrawals − active_job_funds`. - **404** — User document not found. { "error": "Not Found", "message": "User not found" } #### GET /api/v1/dashboard/landing-page-video — Landing page video URL Public endpoint returning the configured landing-page video URL. No bearer token required. Responses: - **200** — Returns the configured landing page video URL (or `undefined` if not set). **Public** — no authentication required. { "video": "https://..." } Example response: ```json { "video": "https://example.com/intro.mp4" } ``` ## Payments - Mount path: `/api/v1/payments` - Default auth: User - Endpoints: 6 ### Payments Wallet operations: deposits via Cryptomus, withdrawals, in-app bonuses, account upgrades, and the gateway webhook. Most calls are gated by the user's `active_balance` (= balance − pending withdrawals − active job funds). #### POST /api/v1/payments/deposit — Open a Cryptomus deposit Creates a pending deposit transaction, opens a Cryptomus invoice, and returns the hosted-checkout URL. The webhook flow (POST `/callback`) finalises the transaction. Request body: - `amount` (number (USD), required): Amount in USD. The fee `amount × deposit_commisson%` is recorded as `transaction_fee`; the credited amount is `amount − fee`. - `payment_method` (string, required): Stored on the transaction record. Currently only Cryptomus is wired up — value is passed through to the provider call. - `payment_type` (string): Accepted in the body but always recorded as `"deposit"`. Reserved for future use. Responses: - **200** — A pending deposit transaction has been created and a Cryptomus invoice opened. Returns the hosted-checkout URL the client should redirect the user to. { "url": "https://pay.cryptomus.com/pay/" } - **500** — Cryptomus invoice creation failed. Returns the upstream JSON. #### POST /api/v1/payments/withdraw — Request a withdrawal Creates a pending withdrawal transaction. Validates against `active_balance` and `min_payout` inside a Mongo transaction. Currently only Cryptomus is supported. Request body: - `amount` (number (USD), required): Amount to withdraw. Must be ≤ `active_balance` and ≥ `min_payout`. - `wallet_address` (string, required): Destination wallet address. - `payment_method` (string): Defaults to `"cryptomus"`. Currently only Cryptomus is supported. Responses: - **201** — Pending withdrawal transaction created (negative amount). Funds are deducted from balance via `deposit_withdraw` inside a Mongo transaction. { "msg": "withdraw done" } - **400** — Insufficient `active_balance`, amount below `min_payout`, missing wallet address, missing fields, or unsupported `payment_method`. { "msg": "Insufficient balance" } - **500** — Unexpected error. { "error": "An error occurred while processing the withdrawal", "details": "..." } #### POST /api/v1/payments/transactions — List transactions Paginated list of the user's transactions (as sender or recipient) plus summary totals (balance, total_withdraw, total_deposit). Filter by `query` (array of `payment_type` values). Request body: - `query` (string[]): Filter by `payment_type`. Defaults to `["deposit", "Withdrawal", "referral_cut", "employer_referral_cut", "account_upgrade", "job_payment", "send_money"]` (all relevant types). - `page` (integer): Defaults to `1`. - `limit` (integer): Defaults to `10`. Responses: - **200** — Returns the paginated transactions where the user is sender or recipient (newest first), plus aggregated `total_count`, `total_withdraw`, `total_deposit`, and current `balance`. Recipient username is joined in. - **404** — No transactions match. { "message": "No transactions found" } #### POST /api/v1/payments/send_bonus — Send a bonus to another user Transfers money to another user as a bonus. Records a `send_money` transaction. Validates against `min_bonus`, `bonus_fee`, and the caller's `active_balance`. Request body: - `worker_id` (string (ObjectId | username), required): Recipient. Either the user's `_id` (ObjectId) or their `username`. Must not equal the caller. - `amount` (number (USD), required): Amount to send. Must be ≥ `min_bonus`, and `amount + bonus_fee` must be ≤ `active_balance`. Responses: - **201** — Bonus paid. Sender's balance debited by `amount + bonus_fee`, recipient credited by `amount`. A `send_money` transaction is recorded with status `paid`. { "message": "bounes of 5 USD successful sent to the worker" } - **400** — Missing fields, amount not numeric, amount below `min_bonus`, or insufficient balance. { "msg": "fill all required fields " } - **404** — Recipient not found, or caller tried to send money to themselves. { "error": "worker with this Id not found " } #### POST /api/v1/payments/upgrade_account — Upgrade to premium Decrements the user's balance by `premium_account_fee`, sets `premium: true`, and records an `account_upgrade` transaction. Responses: - **200** — User's balance is decremented by `premium_account_fee`, `premium` is set to true, and an `account_upgrade` transaction is recorded with status `paid`. **Note:** this endpoint does not check active balance. { "message": "Congratulations! Your account was upgraded to premium!" } #### POST /api/v1/payments/callback — Cryptomus webhook **Public** — guarded by `verifyPayment_Signature` (MD5 of base64 body + `payment_api_key`). Updates the originating deposit transaction, credits the user, and creates referral cuts. Idempotent on repeated calls (already-processed transactions are ignored). Request body: - `sign` (string, required): MD5 of `base64(JSON.stringify(body without sign)) + payment_api_key`. Validated by `verifyPayment_Signature` middleware. - `uuid` (string, required): Cryptomus invoice UUID. - `order_id` (string (ObjectId), required): Our internal `transaction_id` (from the original `/deposit` call). - `status` (string, required): `"paid"`, `"paid_over"`, `"wrong_amount_waiting"`, etc. Anything other than the paid pair is treated as declined when `is_final` is true. - `is_final` (boolean, required): Cryptomus marks the webhook as terminal. Non-final webhooks are largely ignored. - `from` (string): Payer wallet — stored in `payment_details`. - `network` (string): Crypto network — stored in `payment_details`. - `payer_currency` (string): Currency the payer used — stored in `payment_details`. - `txid` (string): On-chain tx id — stored in `payment_details`. Responses: - **200** — Webhook accepted. On `paid` / `paid_over`: marks the transaction `confirmed`, credits the user's balance, and creates `employer_referral_cut` invoices for upline levels 1–3. { "msg": "Webhook processed successfully." } - **403** — Signature did not match (`verifyPayment_Signature`). { "error": "Invalid signature" } - **500** — Payment gateway not configured (`payemnt_getway.payment_api_key` missing) or downstream error processing the webhook. ## Referrals - Mount path: `/api/v1/referrals` - Default auth: User (both | employer | worker) - Endpoints: 2 ### Referrals Three-level referral programme. Each user has `refId_level1/2/3` pointing at their upline. Worker earnings come from completed job tasks; employer earnings come from `employer_referral_cut` transactions. Level bonuses are configured site-wide. #### POST /api/v1/referrals/ — Worker referrals dashboard Returns the worker's referral dashboard. With no flags it returns the full payload (chart, summary, settings, my_refs, inactive_refs). Pass `filter_ref` or `get_inactive_ref` to fetch a single section for pagination/search. Request body: - `filter_ref` (boolean): If `true`, returns only the paginated/filtered downline list under `my_refs` (no chart, summary, or inactive_refs). Used by the table search/sort UI. - `get_inactive_ref` (boolean): If `true`, returns only the inactive-referrals list under `inactive_refs`. Used by the inactive tab. - `page` (integer): 1-indexed page for the `my_refs` and `inactive_refs` lists. Defaults to `1`. Page size is fixed at 10. - `sortBy` (string): Field name to sort the downline list by. Defaults to `sortOrder`. - `order` (integer): `1` ascending or `-1` descending. Defaults to `-1`. - `query` (string): Free-text search across downline `username`, `fname`, `lname` (case-insensitive). Responses: - **200** — Returns the worker's referral dashboard: chart (last 30 days, per-level paid/pending earnings), summary (per-level total/pending earnings + count), settings (level bonuses), my_refs (paginated downline workers with today/yesterday task counts), and inactive_refs (downline users inactive 72h–7d). - **401** — Missing or invalid bearer token. - **403** — User role not in `both | employer | worker`. Example response: ```json { "chart": [ { "date": "2026-04-15", "totalTasks": "12", "level1_paid_earnings": 0.45, "level1_pending_earnings": 0.12, "level2_paid_earnings": 0.05, "level2_pending_earnings": 0, "level3_paid_earnings": 0, "level3_pending_earnings": 0 } ], "ref_clicks": 27, "summary": { "level1": { "total_earnings": 12.34, "pending_earnings": 1.23, "totalCount": 8 }, "level2": { "total_earnings": 2.5, "pending_earnings": 0.4, "totalCount": 14 }, "level3": { "total_earnings": 0.7, "pending_earnings": 0, "totalCount": 22 } }, "settings": { "level1": 0.05, "level2": 0.02, "level3": 0.01 }, "my_refs": { "data": [ { "worker_id": "65a...", "user_id": "65a...", "username": "alice", "phone": "+1...", "referrer": "bob", "todayTasksCount": 3, "yesterdayTasksCount": 5, "today_tasks": [], "yesterday_tasks": [], "level": "level1" } ], "totalcount": 8 }, "inactive_refs": { "data": [ { "user_id": "65a...", "referrer": "bob", "referrer_id": "65b...", "username": "carol", "phone": "+1...", "last_activity": "2026-04-10T03:00:00.000Z" } ], "total": 2 } } ``` #### POST /api/v1/referrals/employerReferrals — Employer referrals dashboard Returns the employer-side referral overview. With no flags it returns summary, my_refs, and settings. Pass `filter_ref` to fetch only the `my_refs` table for pagination/search. Request body: - `filter_ref` (boolean): If `true`, returns only the paginated/filtered downline list (`my_refs`) without summary or settings. - `page` (integer): 1-indexed page for `my_refs`. Defaults to `1`. - `limit` (integer): Page size for `my_refs`. Defaults to `10`. - `sortBy` (string): Field name to sort by. Defaults to `sortOrder`. - `order` (integer): `1` ascending or `-1` descending. Defaults to `1`. - `query` (string): Free-text search across downline `username`, `fname`, `lname` (case-insensitive). Responses: - **200** — Returns the employer-side referral overview: per-level summary (count, totals from `employer_referral_cut` transactions, pending earnings), the paginated `my_refs` list with per-user transaction totals (all-time / today / 7d / 30d), and the level-bonus settings. - **404** — No referral data could be summarised (e.g. no downline users at any level). { "message": "No referrals found" } - **401** — Missing or invalid bearer token. Example response: ```json { "summary": { "level1": { "totalCount": 3, "total_earnings": "12.40", "pending_earnings": 0.5, "data": [ { "price": 4.5, "added": "2026-04-12T...", "status": "paid" } ] }, "level2": { "totalCount": 0, "total_earnings": 0, "pending_earnings": 0 }, "level3": { "totalCount": 0, "total_earnings": 0, "pending_earnings": 0 } }, "my_refs": { "data": [ { "user_id": "65a...", "username": "alice", "fname": "Alice", "lname": "L", "phone": "+1...", "ref_id": "abcd1234", "refId_level1": "65b...", "referrer": "bob", "level": "level1", "transactions": { "totalAllTime": "12.40", "totalToday": "0", "totalLast7Days": "4.50", "totalLast30Days": "12.40" } } ], "totalcount": 3 }, "settings": { "level1": 5, "level2": 2, "level3": 1 } } ``` ## Support - Mount path: `/api/v1/support` - Default auth: User (both | employer | worker) - Endpoints: 4 ### Support tickets Per-user support inbox. Tickets have a sender, a recipient (defaults to admin), a subject and an initial message, plus a `replies` array used for the chat-style thread. #### POST /api/v1/support/ — List my support tickets Returns paginated tickets where the authenticated user is sender or recipient. Each row carries metadata about the latest reply and the unviewed-reply count for the caller. Request body: - `page` (integer): 1-indexed page number. Defaults to `1`. - `limit` (integer): Page size. Defaults to `10`. - `SortBY` (string): Field name to sort by (e.g. `createdAt`). Omit to skip the sort stage. - `Orientation` (string): `"asc"` or `"desc"`. Defaults to descending. Only applies when `SortBY` is set. Responses: - **200** — Returns the user's tickets (where they are sender or recipient), each augmented with `lastReplyBy`, `repliesCount`, and `unviewedRepliesCount` (replies addressed to the caller and not yet viewed). Replies array is omitted from the list view. - **401** — Missing or invalid bearer token. Example response: ```json { "total_count": 1, "support": [ { "_id": "65f1...", "support_message_id": "65f1...", "sender_id": "64aa...", "recipient_id": "64bb...", "sender_username": "alice", "recipient_username": "support", "subject": "Cannot withdraw", "message": "Hi, my withdrawal is stuck.", "viewed": false, "is_admin": false, "status": "open", "createdAt": "2026-04-15T08:14:00.000Z", "lastReplyBy": "support", "repliesCount": 3, "unviewedRepliesCount": 1 } ] } ``` #### POST /api/v1/support/chat — Get ticket thread Returns the full ticket including replies (newest-first) and the sender's profile info. As a side effect, replies addressed to the caller are marked as viewed. Request body: - `support_message_id` (string (ObjectId), required): Ticket ID. The user must be sender or recipient of the ticket. - `page` (integer): Reserved for pagination (currently unused — full thread is returned). - `limit` (integer): Reserved for pagination (currently unused). Responses: - **200** — Returns the full ticket with replies sorted newest-first and the sender's profile fields joined in (`sender_fname`, `sender_last_activity`). Side effect: marks any replies addressed to the caller as `viewed: true`. - **401** — Missing or invalid bearer token. #### PUT /api/v1/support/chat — Reply to a ticket Appends a new reply to the thread. The recipient is inferred from the ticket (the other party). Returns the refreshed thread. Request body: - `support_message_id` (string (ObjectId), required): Ticket ID to reply to. The user must be sender or recipient. - `message` (string, required): Reply body. Responses: - **200** — Reply appended. Returns the same shape as `POST /chat` (full ticket + replies + sender info). - **404** — `support_message_id` did not match any ticket. { "error": "Support message not found" } #### POST /api/v1/support/add — Open a new support ticket Creates a ticket. If `recipient_id` is omitted, routes to the first admin user. Sends an email notification. Request body: - `subject` (string, required): Subject line of the new ticket. - `message` (string, required): Initial message body. Also stored as the first entry in `replies`. - `recipient_id` (string): Optional `user_id` of the recipient. When omitted, the ticket is routed to the first user with `usertype: "admin"` (default support inbox). Responses: - **200** — Ticket created and an email notification queued to support. Returns the new ticket document. - **404** — Recipient user not found (when `recipient_id` was supplied or no admin exists). { "error": "Recipient not found" } ## Chat - Mount path: `/api/v1/chat` - Default auth: User (both | employer | worker) - Endpoints: 11 ### Chat Direct 1:1 messaging between users. Conversations are deterministic per user pair (get-or-create). Real-time delivery, edits, deletes, emoji reactions, read receipts, and typing indicators are pushed over Socket.IO on the shared `/xgodo.socket.io` path with `chat=true` in the connection query. Attachments are uploaded first, then referenced when sending. Mutating endpoints are rate-limited per authenticated user (send ≤ 30/min, start ≤ 20/5min, upload ≤ 25/5min, edit/delete/react ≤ 60/min) and return `429` when exceeded; typing events are throttled at the socket layer. #### POST /api/v1/chat/conversations — List conversations Returns the caller's conversations, newest activity first, with the other participant, unread count, and online presence. Request body: - `page` (integer): 1-indexed page. Defaults to `1`. - `limit` (integer): Page size. Defaults to `30`. Responses: - **200** — Returns `{ page, limit, conversations }`. Each conversation carries `conversation_id`, `last_message`, `last_message_at`, `last_sender_id`, the caller's `unread_count`, and an `other_user` object (`user_id`, `username`, `avatar`, `last_activity`, `online`). Conversations the caller cleared are hidden unless a newer message arrived since. { "page": 1, "limit": 30, "conversations": [ { "conversation_id": "...", "last_message": "see you", "last_message_at": "2026-06-17T10:00:00.000Z", "last_sender_id": "...", "unread_count": 2, "other_user": { "user_id": "...", "username": "jane", "avatar": "jane.png", "online": true } } ] } - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/conversations/start — Start / get a conversation Resolves a target user by `user_id` or `username` and returns the existing or newly created conversation. Used by the profile-page “Message” button and username deep-links (`/chat?user_id=...` or `/chat?username=...`). Request body: - `user_id` (string (ObjectId)): Target user's `user_id`. Either this or `username` is required. - `username` (string): Target user's username (case-sensitive exact match). Responses: - **200** — Returns `{ conversation }` — the existing or newly created 1:1 conversation summary (same shape as a list item). Conversations are deterministic per user pair, so repeated calls return the same conversation. - **400** — Attempted to start a conversation with yourself. - **404** — Target user not found or inactive. - **429** — Rate limit exceeded. Returns `{ error }` with a slow-down message. Limits are keyed per authenticated user: send ≤ 30/min, start ≤ 20/5min, upload ≤ 25/5min, edit/delete/react ≤ 60/min. - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/conversations/clear — Clear a conversation Hides a conversation from the caller's list (per-user; the other side is unaffected). Request body: - `conversation_id` (string (ObjectId), required): Conversation to hide from the caller's list. The other participant is unaffected. Responses: - **200** — Returns `{ success: true }`. The conversation reappears if a newer message arrives. - **404** — Conversation not found for this user. - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/messages — Get messages Cursor-paginated message history for a conversation (newest page first, chronological within the page). Request body: - `conversation_id` (string (ObjectId), required): Conversation to read. The caller must be a participant. - `before` (string (ISO date)): Cursor for pagination — return messages created strictly before this timestamp. Use the `created_at` of the oldest loaded message to page backwards. - `limit` (integer): Page size. Defaults to `30`. Responses: - **200** — Returns `{ conversation_id, other_user, has_more, messages }`. `messages` are in chronological order (oldest first within the page). Deleted messages are returned with `deleted: true` and empty text/attachments. { "conversation_id": "...", "other_user": { ... }, "has_more": true, "messages": [ { "message_id": "...", "sender_id": "...", "recipient_id": "...", "text": "hi", "attachments": [], "read": true, "edited": false, "deleted": false, "created_at": "..." } ] } - **400** — Invalid `conversation_id`. - **403** — Caller is not a participant. - **404** — Conversation not found. - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/messages/send — Send a message Sends text and/or pre-uploaded attachments. Creates the conversation on demand when only a recipient is given. Request body: - `conversation_id` (string (ObjectId)): Existing conversation to post into. If omitted, a recipient (`recipient_id` or `username`) must be supplied and the conversation is created on demand. - `recipient_id` (string (ObjectId)): Recipient user_id (used when `conversation_id` is omitted). - `username` (string): Recipient username (alternative to `recipient_id`). - `text` (string): Message body. Either `text` or at least one attachment is required. - `attachments` (Attachment[]): Array of attachment metadata previously returned by `POST /api/v1/chat/upload`. Each item: { "type": "image|video|audio|document", "url": "uploads/chat/", "name": "photo.jpg", "mime": "image/jpeg", "size": 12345, "width": 1280, "height": 720, "compressed": true }. URLs must point to the chat upload directory. - `reply_to` (string (ObjectId)): Optional message_id this message is replying to (quoted reply). Responses: - **201** — Returns `{ message }` — the created message. Side effects: bumps the conversation preview/timestamp, increments the recipient's unread count, and emits `chat:message` over Socket.IO to both participants. - **400** — Message had neither text nor an attachment, or self-message attempt. - **403** — Conversation not found or caller is not a participant. - **404** — Recipient not found. - **429** — Rate limit exceeded. Returns `{ error }` with a slow-down message. Limits are keyed per authenticated user: send ≤ 30/min, start ≤ 20/5min, upload ≤ 25/5min, edit/delete/react ≤ 60/min. - **401** — Missing or invalid bearer token. #### PUT /api/v1/chat/messages/edit — Edit a message Edits the text of your own message and marks it edited. Request body: - `message_id` (string (ObjectId), required): Message to edit. Must be the caller's own message. - `text` (string, required): New text. Cannot be empty unless the message has attachments. Responses: - **200** — Returns `{ message }` with `edited: true` and a new `edited_at`. Emits `chat:message_edited` to both participants. Refreshes the conversation preview when the edited message was the latest. - **400** — Invalid id, empty edit, or message already deleted. - **403** — Not the caller's message. - **404** — Message not found. - **429** — Rate limit exceeded. Returns `{ error }` with a slow-down message. Limits are keyed per authenticated user: send ≤ 30/min, start ≤ 20/5min, upload ≤ 25/5min, edit/delete/react ≤ 60/min. - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/messages/delete — Delete a message Soft-deletes your own message, leaving a tombstone for the other participant. Request body: - `message_id` (string (ObjectId), required): Message to soft-delete. Must be the caller's own message. Responses: - **200** — Soft-deletes the message (tombstone): `deleted: true`, text/attachments cleared. Emits `chat:message_deleted` to both participants and adjusts unread/preview when needed. - **400** — Invalid `message_id`. - **403** — Not the caller's message. - **404** — Message not found. - **429** — Rate limit exceeded. Returns `{ error }` with a slow-down message. Limits are keyed per authenticated user: send ≤ 30/min, start ≤ 20/5min, upload ≤ 25/5min, edit/delete/react ≤ 60/min. - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/messages/react — React to a message Set, change, or clear your emoji reaction on a message. Either participant can react; reactions are pushed live via the `chat:reaction` socket event. Request body: - `message_id` (string (ObjectId), required): Message to react to. The caller must be a participant of the conversation. - `emoji` (string, required): Emoji to set (max 24 chars). Sending the emoji you've already set clears your reaction; a different emoji replaces it. Each participant holds at most one reaction per message. Responses: - **200** — Returns `{ message }` with the updated `reactions` array (`[{ user_id, emoji }]`). Emits `chat:reaction` to both participants with `{ message_id, conversation_id, user_id, emoji, reactions }` (`emoji` is `null` when cleared). - **400** — Invalid id/emoji, or the message is deleted. - **403** — Caller is not a participant. - **404** — Message not found. - **429** — Rate limit exceeded. Returns `{ error }` with a slow-down message. Limits are keyed per authenticated user: send ≤ 30/min, start ≤ 20/5min, upload ≤ 25/5min, edit/delete/react ≤ 60/min. - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/messages/read — Mark conversation read Marks incoming messages as read and notifies the sender (read receipts). Request body: - `conversation_id` (string (ObjectId), required): Conversation whose incoming messages should be marked read. Responses: - **200** — Marks the caller's unread incoming messages as read, zeroes their unread counter, and emits `chat:read` to the other participant (drives read receipts). - **403** — Conversation not found or caller is not a participant. - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/unread — Unread count Total unread messages for the caller across all conversations. Responses: - **200** — Returns `{ unread }` — the caller's total unread messages across all conversations (navbar badge). - **401** — Missing or invalid bearer token. #### POST /api/v1/chat/upload — Upload attachments Multipart upload for images, video, audio, and documents. Returns attachment metadata for use with the send endpoint. Request body: - `files` (file[] (multipart), required): One or more files under the `files` field (max 10 per request, 50 MB each). Stored under `public/uploads/chat/` with randomised names. - `compressed` (boolean (string)): Informational flag recorded on the returned attachments (the client performs any image compression before upload). Responses: - **201** — Returns `{ attachments }` — metadata for each uploaded file, ready to pass to `POST /api/v1/chat/messages/send`. Each item: { "type": "image|video|audio|document", "url": "uploads/chat/", "name": "photo.jpg", "mime": "image/jpeg", "size": 12345, "width": 1280, "height": 720, "compressed": true }. - **400** — No files uploaded. - **429** — Rate limit exceeded. Returns `{ error }` with a slow-down message. Limits are keyed per authenticated user: send ≤ 30/min, start ≤ 20/5min, upload ≤ 25/5min, edit/delete/react ≤ 60/min. - **401** — Missing or invalid bearer token. ## Report - Mount path: `/api/v1/report` - Default auth: User (both | employer | worker) - Endpoints: 1 ### Report Submit a report against a user or a job. The reporter is taken from the authenticated session — clients only need to supply the target and the reason. #### POST /api/v1/report — Create a report Creates a new report document. The reporter is set to the authenticated user. The report is persisted to the `reports` collection and used by admin moderation tooling. Request body: - `reported_id` (string (ObjectId), required): MongoDB ObjectId of the entity being reported (a user `_id` or a job `_id`). - `report_type` (string, required): Kind of entity being reported. One of `"user"` or `"job"`. - `reason` (string, required): Free-form reason describing why the entity is being reported. - `name` (string): Optional human-readable label for the reported entity (stored on the report document; not validated). Responses: - **200** — Report created successfully. { "msg": "Report created" } - **401** — Missing or invalid bearer token. - **403** — Authenticated user role is not in `both | worker | employer`. - **500** — Server failed to persist the report (e.g. validation error from Mongo such as missing `reason`/`report_type`/`reported_id`). { "msg": "Failed to create report" } Example response: ```json { "msg": "Report created" } ``` ## Device - Mount path: `/api/v1/device` - Default auth: User (both | employer | worker) - Endpoints: 32 ### Device management Add, list, search, inspect, and delete devices the caller owns or rents. RMS-internal endpoints (`/status`, `/version`, `/check-update`, `/automations`, `/usage`, `/interference`, `/next-ban-duration`, `/airplane-complete`) are not documented here — they require the `authenticateRms` shared secret and are only called by the RMS service. #### POST /api/v1/device/add_device — Add a device Pair a device using the hashcode shown in the RemoteMobile app. Verifies the hashcode against RMS, prevents duplicates, and records a `DeviceHistory.added` event. Request body: - `hashcode` (string, required): Pairing hashcode shown on the RemoteMobile Android app. The backend validates it against RMS, then claims the device for the caller. Responses: - **201** — Device claimed. Returns `{ success, message, device }`. A `DeviceHistory` `added` event is recorded. { "success": true, "message": "Device added successfully", "device": { ... } } - **400** — Hashcode invalid, device already registered, or upstream RMS responded non-2xx. { "success": false, "message": "Invalid device code!" } - **404** — Caller user record missing. { "success": false, "message": "User not found" } - **500** — Internal server error. { "success": false, "message": "Internal Server Error" } #### POST /api/v1/device/get_devices — List my devices Paginated list of devices the caller owns or currently rents (and, optionally, devices participating in the caller's active automation iterations). Request body: - `page` (integer): 1-indexed page. Defaults to `1`. - `limit` (integer): Page size. Defaults to `10`. - `includeIterations` (boolean): When truthy, also returns the caller's active automation jobs, their iterations, and the worker user records used by those iterations. Devices participating in those iterations are also included even if they're not owned/rented by the caller. Responses: - **200** — Returns `{ success, devices, page, totaldevice, jobs, iterations, workers, ppaDevicePresent }`. Each device row carries the latest solved labelling action (when one exists), last-30-days `lastMonthEarning` (for `payment_type: action` market devices), and `automation_job_banned_till` if there is an active ban. - **404** — Caller has no devices. { "success": false, "devices": [], "message": "No devices to display" } #### POST /api/v1/device/search_devices_for_automation — Search devices for automation Returns devices the caller can target with an automation run (own non-rented + live-rented). Without `search`, capped at 6. Request body: - `search` (string): Substring matched (case-insensitive) against `name`, `brand`, `model`, `processor`, and `country` (via country-name lookup). Empty/missing returns up to 6 devices owned/rented by the caller. Responses: - **200** — Returns devices the caller can run automation on (own non-rented, plus live-rented), sorted by `online: -1`. Without `search`, capped at 6 results. Also returns total eligible device count. { "devices": [ ... ], "total": 12 } #### POST /api/v1/device/get_automations_for_devices — Get devices by id Lookup devices by `remote_device_id` (lean documents). Filters to ones the caller owns or rents. Request body: - `deviceIds` (string[] (UUID[]), required): Device ids to look up. Each must be a valid UUID. Responses: - **200** — Returns the (lean) device documents owned or rented by the caller for each requested id. { "success": true, "devices": [ ... ] } - **400** — `deviceIds` validation failed. { "success": false, "message": "Invalid device IDs" } #### POST /api/v1/device/get_device — Get a single device Returns the device document if the caller owns it (and it is not rented out / is on market) or rents it. Request body: - `deviceId` (string (UUID — `remote_device_id`), required): Target device's `remote_device_id`. Responses: - **200** — Returns the device (or `null` if not accessible to the caller). #### POST /api/v1/device/get_screenshots — Get screenshots for devices Fetches a fresh screenshot from each device the caller owns or rents. Devices that fail are silently dropped. Request body: - `deviceIds` (string[] (UUID[]), required): Array of `remote_device_id` values. Responses: - **200** — Returns an object keyed by `remote_device_id` whose values are base64 screenshots. Devices that fail to return a screenshot are silently omitted. { "": "", ... } #### DELETE /api/v1/device/delete_device — Delete a device Deletes the device. Cascades: history event, removes automation iterations, declines running tasks, removes airplane trigger links, invalidates auto-trigger cache. Request body: - `deviceId` (string (UUID — `remote_device_id`), required): Target device's `remote_device_id`. Responses: - **200** — Device deleted. Side effects: history `removed` event written, automation iterations cleared, running job tasks marked declined, airplane trigger links removed, auto-trigger cache invalidated. { "success": true, "message": "Device deleted successfully" } - **400** — `deviceId` missing. { "success": false, "message": "Device ID is required" } - **404** — Device not found or not owned by the caller. { "success": false, "message": "Device not found or not authorized" } #### POST /api/v1/device/start — Start a device For `screenshot`-payment devices, starts the device by creating the next labelling task on its attached job. Request body: - `deviceId` (string (UUID — `remote_device_id`), required): Target device's `remote_device_id`. Responses: - **200** — Starts a `screenshot`-payment device by creating a fresh labelling task on the attached job. Returns `{ success, message }`. Returns `success: false` if the device is not attached to a job, or if RMS communication failed. - **400** — Missing `deviceId`, device not found, or device already started. { "success": false, "message": "Device is already started" } #### POST /api/v1/device/stop — Stop a device Marks the device `stopped` and clears outstanding labelling tasks (`unsolved`). Request body: - `deviceId` (string (UUID — `remote_device_id`), required): Target device's `remote_device_id`. Responses: - **200** — Device transitioned to `stopped`. Outstanding labelling tasks for the device are flipped to `unsolved`. { "success": true } - **400** — Missing id, not found, or already stopped. { "success": false, "message": "Device is already stopped" } ### Marketplace Publish your devices to the rental marketplace, list devices for rent, and unlist them. #### POST /api/v1/device/market/publish — Publish devices to market Lists owned devices on the rental market with the chosen prices and `payment_type`. `action` requires `isAssistant: true` and a phone number for devices in the rollout window. Request body: - `device_Ids` (string[], required): Devices to publish (must be owned by caller, not in use, not on market, not rented). - `rent_price_hourly` (number): At least one of the four price tiers must be supplied. Used only for `live` payment type. - `rent_price_daily` (number): - `rent_price_weekly` (number): - `rent_price_monthly` (number): - `uptime_guarantee` (integer, required): Whole number 80–99 (inclusive). - `payment_type` (string, required): `"action"`, `"screenshot"`, or `"live"`. `"action"` requires `isAssistant: true` and a phone number when the device falls into the rollout window. Responses: - **200** — Devices flipped to `is_onMarket: true` with the supplied prices. A `RentHistory.published` event is recorded for each. { "success": true } - **400** — Missing/invalid prices, `uptime_guarantee` out of range, invalid `payment_type`, no eligible devices found, or phone number required for selected devices. #### POST /api/v1/device/market/unpublish — Unlist devices Removes devices from the market and revokes any active rentals (with notifications and history). Request body: - `device_Ids` (string[], required): Devices to delist. Must be owned by the caller and currently `is_onMarket` or `is_rented`. Responses: - **200** — Devices delisted. Active rentals are revoked: notifications fire to both renter and owner, RentHistory event recorded, the device is unsubscribed, and any jobs referencing the device have `remote_device_id` cleared. { "success": true } - **400** — No matching device to delist. { "success": false, "message": "Device not found" } #### GET /api/v1/device/market — Browse the rental market Public endpoint. Returns currently rentable, online, `live`-payment devices. Test devices are filtered out for authenticated callers. Query parameters: - `page` (integer): Defaults to `1`. - `limit` (integer): Defaults to `9`. - `search` (string): Substring matched against `name`, `brand`, `model`, `processor`, and `country` (via country-name lookup). Responses: - **200** — Returns currently rentable, online, `live` payment-type devices. Test devices are filtered out for authenticated callers. Owner usernames and string-formatted `rent_price` are joined in. { "success": true, "totalDevices": 17, "devices": [ ... ] } - **404** — No devices match the filter. { "success": false, "devices": [], "message": "No devices to display" } ### Sharing Share your owned devices with another user as a free `live` daily rental. #### POST /api/v1/device/share — Share devices with a user Transfers eligible devices to the recipient as a `rent_price: 0` daily live rental. Request body: - `device_Ids` (string[], required): Devices to share. Must be owned by the caller, not on market, not rented, not in use. - `username` (string, required): Recipient username (case-sensitive). Cannot be the caller themselves. Responses: - **200** — Devices shared with the recipient as a free `live` daily rental (`rent_price: 0`). A `RentHistory.shared` event is recorded. { "success": true, "count": 2 } - **400** — Validation failure, sharing with self, or no eligible devices. { "success": false, "message": "No eligible devices found" } - **404** — Recipient username not found. { "success": false, "message": "User not found" } ### Renting Rent a device from the marketplace and end an active rental. #### POST /api/v1/device/rent — Rent a device Rents a marketplace device for the chosen duration. Validates `active_balance`, locks the device, and notifies both parties. Request body: - `device_id` (string (UUID), required): Device to rent. Must be `is_onMarket: true`, not rented, not in use. - `rent_duration` (string, required): One of `"hourly"`, `"daily"`, `"weekly"`, `"monthly"`. The matching `rent_price_` on the device is charged. Responses: - **200** — Device rented. Caller's `active_balance` is checked, device is flipped `is_rented` with rental window and price, a fresh DeviceUptime row is created, and notifications fire to both renter and owner. { "success": true } - **400** — Missing/invalid `rent_duration`, missing `device_id`, missing/invalid rent price for the chosen duration, or insufficient `active_balance`. - **404** — Device not found, or selected device is offline. { "success": false, "devices": [], "message": "No device found" } #### POST /api/v1/device/unsubscribe — End rentals Releases an array of currently-rented devices. For live rentals, partial rent is collected up to the moment of release; uptime is summarised in the rent history. Request body: - `device_id` (string[] (UUID[]), required): Array of `remote_device_id` values currently rented by the caller. (Despite the singular name, the controller always treats it as an array.) Responses: - **200** — Rentals released. For `live` rentals, partial rent is collected up to now, an uptime/`rentDeducted` summary is captured in RentHistory, both parties are notified, and devices are marked back `is_onMarket: true`. { "success": true } - **400** — No matching active rental for the caller. { "success": false, "message": "Device not found" } ### Automation execution Run automations on owned/rented devices and inspect per-job-per-device success rates. #### POST /api/v1/device/automation — Start/stop an automation on devices Validates access on the automation, that all target devices are online and free, mints (or fetches) the user's API token, then forwards to `automationOperation`. Request body: - `device_ids` (string[], required): At least one device id. Each must be owned/rented (`live`) by the caller, not in use, online. - `automationId` (string (ObjectId), required): Automation `_id`. Caller must be owner, shared executor/editor, or the automation must be `visibility: public`. - `command` (string, required): `"start"` or `"stop"`. - `automationParameters` (any): Forwarded to `automationOperation` as the automation parameters payload. - `jobVariables` (any): Forwarded as job-level variables. Responses: - **200** — Operation accepted by `automationOperation`. Returns `{ success, message }` with the underlying operation's status code. - **400** — Validation failure, some devices offline/missing, or unable to mint an API token for the user. { "success": false, "message": "Some devices offline or not found." } - **404** — Automation not found or no access. { "success": false, "message": "Automation not found or you don't have access to it." } #### GET /api/v1/device/automation-success-rates — My automation success rates Returns a (job × device) success-rate matrix for the caller's devices, with pagination on both axes and configurable sort. Query parameters: - `sortBy` (string): One of `"success_count"` (default), `"success_rate"`, `"total_count"`. - `sortDirection` (string): `"asc"` or `"desc"` (default). - `deviceOffset` (integer): Defaults to `0`. - `deviceLimit` (integer): Defaults to `10`. - `jobOffset` (integer): Defaults to `0`. - `jobLimit` (integer): Defaults to `10`. Responses: - **200** — Returns `{ data: { devices, jobs, matrix, pagination } }`. The matrix is keyed by `:` and contains success/fail counts and rates. When the caller has no devices, all collections are empty. #### GET /api/v1/device/load-stats — Device load stats Returns per-device and fleet-wide load/utilization statistics for pay-per-action on-market devices only. Aggregates task execution data from JobTask and uptime data from DeviceUptime over the chosen period. Useful for understanding how busy each device is and how much spare capacity remains. Query parameters: - `period` (string): One of `"24h"`, `"7d"` (default), `"30d"`. Determines the look-back window for task and uptime aggregation. Responses: - **200** — Returns `{ data: { devices, summary } }`. Each device entry includes current status (online, busy), task counts (total / successful / failed / running), uptime/busy/free durations in ms, utilization percentage, and average task duration. The `summary` object contains fleet-wide aggregates: total/online/busy/free/offline device counts, fleet utilization %, total tasks, and total uptime/busy/free time. { "success": true, "data": { "devices": [ { "remote_device_id": "...", "name": "...", "online": true, "is_busy": false, "utilization_pct": 42.5, "total_tasks": 120, ... } ], "summary": { "total_devices": 5, "online_devices": 3, "busy_devices": 1, "free_devices": 2, "fleet_utilization_pct": 38.2, "total_tasks": 540, ... } } } ### Airplane-mode triggers Per-device shareable URLs that toggle airplane mode (forcing an IP rotation), plus the rate-limit/auto-trigger configuration and trigger logs. The trigger endpoint itself is **public**. #### POST /api/v1/device/settings — Get device settings Returns the device's trigger links, rate-limit/auto-trigger intervals, and proxy password info. Lazy-migrates the legacy single trigger id into a `Default` labelled link. Request body: - `device_id` (string (UUID), required): Owned device id. Responses: - **200** — Returns the device's airplane-trigger config (links, min interval, auto-trigger interval, online status), the legacy single proxy password (default label), and the labelled proxy passwords list. Lazy-migrates a legacy `airplane_trigger_id` into a `Default` labelled link. - **400** — Missing `device_id`. - **404** — Device not owned by caller. #### POST /api/v1/device/airplane-trigger/logs — List airplane trigger logs Returns the most recent trigger attempts for a device. Request body: - `device_id` (string (UUID), required): Owned device id. - `skip` (integer): Defaults to `0`. - `limit` (integer): Defaults to `100`. Capped at `100`. Responses: - **200** — Returns trigger log entries (newest-first) — `triggered_at`, `success`, `error_message`, `trigger_ip`, `device_ip` — plus the total count. { "success": true, "logs": [ ... ], "total": 24 } #### POST /api/v1/device/airplane-trigger/generate — Generate a trigger link Creates a new shareable trigger link with the supplied label. Capped at 20 per device. Request body: - `device_id` (string (UUID), required): Owned device id. - `label` (string, required): Display label, ≤ 50 chars (after trim). Responses: - **200** — Trigger link created. Returns `{ trigger_link: { _id, label, created_at } }`. Capped at 20 active links per device. - **400** — Missing `device_id`, missing/oversize `label`, or 20-link cap reached. - **404** — Device not owned by caller. #### POST /api/v1/device/airplane-trigger/regenerate — Rotate a trigger link id Replaces the link's id (label preserved). Use to revoke a leaked link. Request body: - `device_id` (string (UUID), required): Owned device id. - `trigger_link_id` (string (ObjectId), required): Existing link id to rotate (deleted and replaced with a new id). Responses: - **200** — New link issued (label preserved). Old link id no longer works. - **400** — Missing fields. - **404** — Device or link not found. #### POST /api/v1/device/airplane-trigger/delete — Delete a trigger link Removes a trigger link. Request body: - `device_id` (string (UUID), required): Owned device id. - `trigger_link_id` (string (ObjectId), required): Link to delete. Responses: - **200** — Link removed. { "success": true, "message": "Trigger link deleted successfully" } - **404** — Link not found. #### POST /api/v1/device/airplane-trigger/update-label — Update a trigger link label Renames an existing link. Request body: - `device_id` (string (UUID), required): Owned device id. - `trigger_link_id` (string (ObjectId), required): Link id. - `label` (string, required): New label, ≤ 50 chars after trim. Responses: - **200** — Label updated. { "success": true, "label": "..." } - **404** — Link not found. #### POST /api/v1/device/airplane-trigger/interval — Set rate-limit interval Minimum seconds between manual triggers. Must be < auto-trigger interval (in seconds). Request body: - `device_id` (string (UUID), required): Owned device id. - `interval` (integer (seconds), required): Min seconds between manual triggers (rate-limit). Range `0` (disabled) – `86400`. Must be strictly less than `auto-trigger interval` in seconds. Responses: - **200** — Updated. { "success": true, "airplane_trigger_min_interval": 30 } - **400** — Out-of-range interval, or violates the relationship with the other interval. - **404** — Device not owned by caller. #### POST /api/v1/device/airplane-trigger/auto-trigger — Set auto-trigger interval Minutes between automatic triggers (`0` disables; otherwise `[3, 1440]`). Must be > rate-limit interval (in seconds). Request body: - `device_id` (string (UUID), required): Owned device id. - `interval` (integer (minutes), required): Auto-trigger interval. `0` disables; otherwise must be in `[3, 1440]`. Must be strictly greater than the rate-limit interval (in seconds). Responses: - **200** — Updated; `airplane_auto_trigger_last_run` is reset so the next tick fires promptly. { "success": true, "airplane_auto_trigger_interval": 15 } - **400** — Out-of-range interval or violates the relationship with the rate-limit interval. - **404** — Device not owned by caller. #### POST /api/v1/device/airplane-trigger/trigger — Trigger airplane mode (public) Public endpoint — no bearer token required. The shareable URL embeds the link's `_id` as `trigger_id`. Rate-limited per device. Request body: - `trigger_id` (string (ObjectId), required): Link id from a generated trigger URL. Public — no auth. Responses: - **200** — Airplane mode toggled on the device. Logged in `AirplaneTriggerLog`. { "success": true, "message": "Airplane mode triggered successfully" } - **400** — Invalid `trigger_id`. - **404** — Trigger link or device not found. - **429** — Rate-limit (`airplane_trigger_min_interval`) not yet elapsed. { "success": false, "message": "Please wait s before triggering again" } - **500** — Device action failed. ### Proxy connections Connections decouple proxy credentials from devices. Each connection is `{ connectionId, password }` (the SOCKS5 username/password) and has at most one **association** to a device that routes its traffic. Re-pointing the association is the IP-rotation primitive — new client sessions exit through the new device within ~100 ms. RMS owns the credentials in plaintext and pushes to the zarif-proxy orchestrator. The legacy `device_proxy_password` table is kept in sync via a mirror so the in-process port-3008 proxy still authenticates new credentials during the transition. #### POST /api/v1/device/connections/list — List my connections All connections owned by, or currently routing through, devices the caller owns. Responses: - **200** — Returns connections owned by, or currently routing through, devices the caller owns. The `password` is plaintext (RMS owns the credential). { "success": true, "connections": [ { "connectionId": "", "password": "", "ownerUserId": "<deviceId>", "name": "client-A", "createdAt": "...", "deviceId": "<deviceId>" } ] } #### POST /api/v1/device/connections/create — Create a connection Generates a fresh `connectionId` (4 letters + 8 digits) and 12-char password, and associates it with the chosen device. Returns the plaintext password — save it now; the orchestrator stores only a hash. Request body: - `device_id` (string (UUID), required): Owned device the new connection should route through (also becomes the connection's owner). - `name` (string): Optional human label (≤ 100 chars). Cosmetic only — the credential is the auto-generated `connectionId`. Responses: - **200** — Created. RMS generates a `connectionId` (4 letters + 8 digits) + 12-char password, writes the connection + association, and syncs to the orchestrator (and mirrors to the legacy `device_proxy_password` table for transitional in-process proxy auth). { "success": true, "connection": { "connectionId": "<id>", "password": "<plaintext>", "ownerUserId": "<deviceId>", "name": null, "createdAt": "...", "deviceId": "<deviceId>" } } - **400** — Missing `device_id`. - **404** — Device not owned by caller. #### POST /api/v1/device/connections/delete — Delete a connection Cascades on RMS, orchestrator, and the legacy mirror. Active sessions are terminated. Request body: - `connection_id` (string, required): Connection id (from list/create). Responses: - **200** — Deleted on RMS, orchestrator, and the legacy mirror. Cascades remove association + connection-blacklist rows. Active proxy sessions using this credential are terminated. { "success": true } - **403** — Connection not owned/routed through any device the caller owns. - **404** — Connection not found. #### POST /api/v1/device/connections/password/rotate — Rotate password Replaces the connection's password (same `connectionId`). Active sessions continue on the old credential until they close. Request body: - `connection_id` (string, required): Connection id (from list/create). Responses: - **200** — Generates a new 12-char password, updates RMS + orchestrator + legacy mirror. { "success": true, "password": "<newPlaintext>" } #### POST /api/v1/device/connections/association/set — Move connection to another device (IP rotation) Changes the exit device. Owner must own both the source connection and the target device. Request body: - `connection_id` (string, required): Connection to re-route. - `device_id` (string (UUID), required): Target device — **must also be owned by the caller**. Responses: - **200** — Hot-swap exit device. Effective for new SOCKS5/HTTP CONNECT sessions within ~100 ms (the orchestrator's connection-auth cache TTL). In-flight TCP sessions continue on the old device until they close naturally. { "success": true } - **403** — Source connection or target device not owned by caller. #### POST /api/v1/device/connections/association/delete — Unassociate Connection stays but cannot authenticate until re-associated. Request body: - `connection_id` (string, required): Connection id (from list/create). Responses: - **200** — Removes the association — connection stays alive but cannot route until a new association is set. Active sessions are terminated. { "success": true } #### POST /api/v1/device/connections/blacklist/list — List per-connection blacklist Per-connection block list (in addition to global + per-device). Request body: - `connection_id` (string, required): Connection id (from list/create). Responses: - **200** — Per-connection blacklist domains (in addition to the global blacklist). Suffix matching applies — blocking `example.com` also blocks `sub.example.com`. { "success": true, "domains": ["example.com", "tracker.io"] } #### POST /api/v1/device/connections/blacklist/add — Add domains to blacklist Adds one or more domains to the connection's blacklist. Suffix matching applies on the proxy side. Request body: - `connection_id` (string, required): Connection id. - `domains` (string[], required): Non-empty array of domains to add or remove. Responses: - **200** — Updated. { "success": true } #### POST /api/v1/device/connections/blacklist/remove — Remove domains from blacklist Removes one or more domains from the connection's blacklist. Request body: - `connection_id` (string, required): Connection id. - `domains` (string[], required): Non-empty array of domains to add or remove. Responses: - **200** — Updated. { "success": true } ### Proxy logs & bandwidth Read-only views on the orchestrator's request log (ClickHouse) and bandwidth aggregates (PostgreSQL). All queries are scoped to devices the caller owns. #### POST /api/v1/device/proxy-logs/list — Query request logs One log entry per SOCKS5 / HTTP CONNECT attempt. Date range required; max 10000 results per call. Request body: - `from` (string (ISO 8601), required): Inclusive lower bound on timestamp. - `to` (string (ISO 8601), required): Inclusive upper bound on timestamp. - `connection_id` (string): Filter to one connection. - `device_id` (string (UUID)): Filter to one device — must be owned by the caller. - `target` (string): Substring match on the request target (`host:port`). - `action` ("allowed" | "blocked"): Filter by outcome. - `limit` (integer): Page size (default 100, max 10000). - `offset` (integer): 0-indexed offset for paging. Responses: - **200** — Backed by ClickHouse. Each entry has `timestamp`, `connectionId`, `deviceId`, `serverNode`, `target`, `targetType`, `port`, `resolvedIp`, `action`, `blockReason`, `bytesIn`, `bytesOut`. Results are post-filtered to devices owned by the caller. { "success": true, "total": 12345, "entries": [ { "timestamp": "...", "connectionId": "<id>", "deviceId": "<uuid>", "action": "allowed", "target": "example.com:443", "bytesIn": 0, "bytesOut": 0 } ] } #### POST /api/v1/device/proxy-bandwidth — Query bandwidth aggregates Per-(device, connection) byte counts, optionally grouped by hour or day. Request body: - `device_id` (string (UUID)): Filter to one device — must be owned by the caller. Omitting fetches per-device rows for every device the caller owns. - `connection_id` (string): Filter to one connection. - `from` (string (ISO 8601)): Inclusive lower bound on the bucket time. - `to` (string (ISO 8601)): Inclusive upper bound on the bucket time. - `groupBy` ("hour" | "day"): Time-bucket granularity. Responses: - **200** — Aggregated bandwidth rows (PostgreSQL on the orchestrator). Each row has `bytesIn`, `bytesOut`, `avgLatencyMs`, `bucket` (when grouped), keyed by `(deviceId, connectionId)`. { "success": true, "rows": [ { "deviceId": "<uuid>", "connectionId": "<id>", "bytesIn": 0, "bytesOut": 0, "bucket": "2026-04-25T13:00:00Z" } ] } ## Notifications - Mount path: `/api/v1/notification` - Default auth: User (both | employer | worker) - Endpoints: 2 ### Notifications In-app notification feed for the authenticated user. Each notification has a `name` (event type) and a `params` map; the frontend renders the message from these. #### GET /api/v1/notification/notifications — List notifications Returns a paginated, newest-first slice of the authenticated user's notifications, plus the total unread count. Query parameters: - `page` (integer): 1-indexed page number. Defaults to `1`. - `limit` (integer): Items per page. Defaults to `9`. Responses: - **200** — Returns a page of notifications (newest first), the total unread count, and the page number that was served. - **401** — Missing or invalid bearer token. - **403** — User role not in `both | employer | worker`. Example response: ```json { "notifications": [ { "_id": "65f1c2b8a9b4e9c9d4a8f111", "notification_id": "65f1c2b8a9b4e9c9d4a8f111", "name": "job_completed", "params": { "job_id": "65f1c2b8a9b4e9c9d4a8f000", "title": "Like 10 posts" }, "user_id": "64aa1c2b8a9b4e9c9d4a8e22", "added": "2026-04-15T08:14:00.000Z", "read": null } ], "unread": 7, "page": "1" } ``` #### POST /api/v1/notification/read_notifications — Mark notifications as read Sets `read` to the current timestamp for the given notification IDs (scoped to the authenticated user). Request body: - `notification_ids` (string[] (ObjectId[]), required): Array of notification `_id` values to mark as read. Each entry must be a valid ObjectId. Only notifications owned by the authenticated user are updated. Responses: - **200** — Returns `success: true` and the user's remaining unread count. { "success": true, "unread": 4 } - **400** — `notification_ids` is missing, not an array, or contains an invalid ObjectId. { "success": false, "message": "Invalid notification IDs" } - **401** — Missing or invalid bearer token. Example response: ```json { "success": true, "unread": 4 } ``` ## Labelling - Mount path: `/api/v1/labelling` - Default auth: User (both | employer | worker) - Endpoints: 6 ### Labelling Crowd-tasking workflow used to label screens or perform device actions. An employer creates a labelling task with a screenshot and a reward; a worker claims the next available task (`/get_task`), then either solves it via `/task/resolve` (which performs the action on the real device) or — for an iteration — finishes via `/end-iteration`. #### POST /api/v1/labelling/task — Create a labelling task Create a new labelling task tied to a device (and optionally to a job and iteration). Uses a Mongo transaction; deducts `reward` against the employer's `active_balance`. If a duplicate task for the same device already exists in `created`/`solving`, the call is rejected. Request body: - `taskName` (string, required): Task title shown to the worker. - `deviceId` (string (UUID), required): Target device's `remote_device_id`. - `screen` (string, required): Base64-encoded screenshot the worker will label / act on. - `originalHeight` (number, required): Source screenshot pixel height. - `originalWidth` (number, required): Source screenshot pixel width. - `reward` (number, required): Amount paid to the solving worker. Must not exceed the employer's `active_balance`. - `duration` (number (seconds), required): Time limit for the worker to solve. After 2× this duration without progress, the iteration is auto-failed. - `iterationId` (string (ObjectId) | null, required): Iteration to attach this task to. When non-null, `jobId` is also required. - `jobId` (string (ObjectId)): Required when `iterationId` is set. Job must be `active`. Responses: - **200** — Task created. Returns the LabellingTask document. - **400** — Validation failure (Zod), insufficient `active_balance`, or a duplicate task already exists for this device. { "success": false, "message": "<reason>" } - **404** — Authenticated user not found, or the supplied `jobId` / `iterationId` does not exist. { "success": false, "message": "Job not found" } - **500** — Job is not active, or unknown error. { "success": false, "message": "Job not active" } #### POST /api/v1/labelling/get_task — Claim or fetch the worker's labelling task Returns the worker's currently in-progress task, or claims the oldest available `created` task and returns it. Pass `job_id` to scope to a specific job's iterations; omit it for free-floating tasks. Request body: - `taskId` (string): (Reserved.) Currently unused — assignment is by FIFO across `created` tasks. - `job_id` (string (ObjectId)): If supplied, only tasks attached to active iterations on this job (for the calling user) are considered. Otherwise free-floating tasks (`iteration_id: null`) are picked. Responses: - **200** — Either returns the worker's currently-assigned `solving` task (if any matches the requested job context) or claims and returns the next available `created` task. Each task is enriched with `device_name`. `retry: false` means there's nothing more to wait for. - **200 (no task)** — When no task is currently assigned and the job/active-iteration check returned no candidate, the call succeeds with `success: false, message: "Job not found or job is no longer active"` and the request ends. - **400** — No tasks are available to solve. Client should retry later (`retry: true`). { "success": false, "message": "No tasks available to solve", "retry": true } #### GET /api/v1/labelling/apps — List label-able apps Public catalogue of apps used by the labelling UI. Responses: - **200** — **Public** — returns the entire `apps` collection (label-able apps shown in the worker UI). #### POST /api/v1/labelling/history — Labelling history Paginated history of labelling tasks where the user is worker, employer, or both. Each row carries resolved usernames. Request body: - `type` (string, required): One of `"SolvedByMe"`, `"SolvedByWorkers"`, `"Both"`. Filters by `worker_id` (me), `employer_id` (me), or both. - `page` (integer): Defaults to `1`. - `limit` (integer): Defaults to `10`. Responses: - **200** — Returns the labelling history page with `worker_username` and `employer_username` resolved on each task. - **500** — Unknown error. { "success": false, "message": "Server error" } #### POST /api/v1/labelling/task/resolve — Resolve a labelling task with a device action Worker submits the chosen device action. The action is validated (Zod, per action type), executed against the device via the device-action utilities, and the task transitions `solving → processing → solved`. On success, a fresh follow-up labelling task is queued automatically. Request body: - `labelling_task_id` (string (ObjectId), required): Task currently in `solving` state. Will be transitioned to `processing` and then `solved`. - `action` (string, required): One of: `airplane`, `refresh`, `goHome`, `recents`, `goBack`, `hold`, `swipe`, `swipePoly`, `doubleTap`, `multiTap`, `tap`, `copyText`, `listApps`, `launchApp`, `showNotification`, `screenContent`, `reverseCopy`, `installApk`, `uploadMedia`. - `(action-specific)` (various, required): Each action carries its own params validated by Zod. Examples: `tap` → `{ x: number, y: number }`; `swipe` → `{ x1, y1, x2, y2, duration }`; `multiTap` → `{ sequence: {x,y}[], intervalFrom, intervalTo }`; `swipePoly` → `{ startX, startY, sequence: {x|null,y|null}[], duration, bezier }`; `hold` → `{ x, y, duration }`; `doubleTap` → `{ x, y, interval }`; `copyText` → `{ text }`; `launchApp` → `{ packageName }`; `installApk` → `{ app_file }`; `uploadMedia` → `{ url }`. Verb-only actions (`airplane`, `refresh`, `goHome`, `recents`, `goBack`, `listApps`, `showNotification`, `screenContent`, `reverseCopy`) need no extra params. Responses: - **200** — Action executed and the labelling task marked `solved`. If the parent job exists, a follow-up labelling task is created (so the worker keeps a fresh screen). Returns `{ success: true, newTask }`. - **400** — Bad action shape, task not found, device offline, or job not found after the action. { "success": false, "message": "device offline" } - **500** — Time out (task no longer in `solving`), action failed, or unknown error. Side-effects: when not part of an iteration, the device is `stopped` and outstanding tasks for that device flipped to `unsolved`. { "success": false, "message": "Time out" } #### POST /api/v1/labelling/end-iteration — End an iteration with proof text Closes an iteration owned by the worker, persisting the proof text as a JobTask and removing the iteration. If the job is now full, marks it `complete` and notifies. Request body: - `iteration_id` (string (ObjectId), required): Iteration to end. - `iteration_text` (string, required): Final proof text. Must be ≤ 5000 chars. Responses: - **200** — Iteration closed. A new `JobTask` is created with the worker's proof, the iteration is deleted, and outstanding labelling tasks are flipped to `unsolved`. If the job has reached its `positions` quota, it is marked `complete` and a completion email is sent. { "success": true, "message": "Iteration ended successfully" } - **400** — Invalid `iteration_id` or `iteration_text` (missing, not a string, or > 5000 chars). { "success": false, "message": "Valid iteration ID and text is required" } - **404** — Iteration or its job not found. { "success": false, "message": "Iteration not found" } - **500** — Server error during the transaction. The iteration is best-effort cleaned up. { "success": false, "message": "An error occurred while ending the iteration" } ## API Key - Mount path: `/api/v1/developer/api/token` - Default auth: User (both | employer | worker) - Endpoints: 4 ### Developer API Token Manage the long-lived (~99 year) JWT used to call the v2 client API. One active token per user. Tokens are signed with `JWT_CLIENT_API_SECRET` and include the user's `id` and `username` claims. #### GET /api/v1/developer/api/token/ — Get current API token Returns the user's currently-active API token along with its expiry and activity metadata. Returns 404 if the user has never created one (or it was deleted). Responses: - **200** — Returns the user's active API token plus expiry, last_activity, and recent activity_log entries. { "token": "<jwt>", "expireat": "2125-04-15T08:14:00.000Z", "activity_log": [], "last_activity": null } - **404** — User has no API key, or it was revoked / has a `null` token. { "msg": "no api key found for this user" } - **401** — Missing or invalid bearer token. Example response: ```json { "token": "<jwt>", "expireat": "2125-04-15T08:14:00.000Z", "activity_log": [], "last_activity": null } ``` #### POST /api/v1/developer/api/token/ — Create API token Issues a new API token if the user does not already have one active. If the user has a denied status, returns 403; if a token already exists, returns 401. Responses: - **200** — Token created (or re-issued onto an existing record that had no token). ApiResponse includes `status: 200`, the new `token`, `expireat` (~99 years in the future), and the existing `activity_log` / `last_activity`. - **401** — User already has an active token. Use PUT to rotate it. { "status": 401, "message": "Access denied. You already have an active API key." } - **403** — User's API access has been administratively denied (`status: "denied"`). { "status": 403, "message": "Access denied. You are not allowed to create an API key." } Example response: ```json { "status": 200, "token": "<jwt>", "expireat": "2125-04-15T08:14:00.000Z", "activity_log": [], "last_activity": null } ``` #### PUT /api/v1/developer/api/token/ — Rotate API token Issues a brand-new token, replacing whatever was previously stored for this user. Use this to revoke a leaked token. Responses: - **200** — Existing token rotated. Returns the new `token`, `expireat`, and the previous `activity_log` / `last_activity`. { "token": "<jwt>", "expireat": "2125-04-15T08:14:00.000Z", "activity_log": [], "last_activity": null } - **403** — User's API access has been administratively denied. { "message": "Access denied. You are not allowed to create an API key." } Example response: ```json { "token": "<jwt>", "expireat": "2125-04-15T08:14:00.000Z", "activity_log": [], "last_activity": null } ``` #### DELETE /api/v1/developer/api/token/ — Delete API token Removes the user's API token document entirely. Subsequent v2 calls using the old token will fail. Responses: - **200** — Token deleted. Returns an empty object. {} - **403** — User's API access has been administratively denied. { "message": "Access denied. You are not allowed to create an API key." } Example response: ```json {} ``` ## Job Bucket - Mount path: `/api/v1/bucket` - Default auth: User (both | employer | worker) - Endpoints: 5 ### Job Bucket (per-job, per-device) Per-(job, device) JSON object owned by the job. Used by automations to keep state isolated to a single hire — separate from the cross-job `device-bucket`. Validated against the automation's `bucket_schema` when the linked automation defines one. #### GET /api/v1/bucket/:job_id — List buckets for a job Returns a paginated list of every bucket attached to this job, with the device display name resolved for each. Query parameters: - `page` (integer): 1-indexed page. Defaults to `1`. - `limit` (integer): Page size. Defaults to `20`. Capped at `100`. Request body: - `job_id` (string (ObjectId), required): Path param. Job's `job_id`. The caller must own the job or have it shared to them. Responses: - **200** — Returns a paginated list of buckets for this job (newest first by `updated_at`), each enriched with the device's display name. - **404** — Job not found, or the caller does not own it / does not have the required share permission (`view` for reads, `edit` for writes). { "error": "Job not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } Example response: ```json { "buckets": [ { "_id": "65f1...", "job_id": "65aa...", "remote_device_id": "8b2f-uuid", "data": { "runs": 3 }, "updated_at": "2026-04-15T08:14:00.000Z", "device_name": "Pixel 5 — Lab" } ], "total": 1, "page": 1, "pages": 1 } ``` #### DELETE /api/v1/bucket/:job_id — Delete all buckets for a job Wipes every bucket associated with this job. Caller must have `edit` access. Request body: - `job_id` (string (ObjectId), required): Path param. Job's `job_id`. The caller must own the job or have it shared to them. Responses: - **200** — All buckets for this job removed. Returns the count deleted. { "success": true, "deleted": 12 } - **404** — Job not found, or the caller does not own it / does not have the required share permission (`view` for reads, `edit` for writes). { "error": "Job not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } Example response: ```json { "success": true, "deleted": 12 } ``` #### GET /api/v1/bucket/:job_id/:device_id — Read a single bucket Returns the bucket for this job/device pair. 404 when none exists. Request body: - `job_id` (string (ObjectId), required): Path param. Job's `job_id`. The caller must own the job or have it shared to them. - `device_id` (string (UUID — `remote_device_id`), required): Path param. Device's `remote_device_id`. Responses: - **200** — Returns the bucket document for this job/device pair. { "bucket": { "_id": "...", "job_id": "...", "remote_device_id": "...", "data": { ... }, "updated_at": "..." } } - **404** — Either the job is inaccessible or no bucket exists for this device. { "error": "Bucket not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } #### PUT /api/v1/bucket/:job_id/:device_id — Write a bucket Replaces the bucket payload for this job/device pair. Validated against the automation's `bucket_schema` when present. Request body: - `job_id` (string (ObjectId), required): Path param. Job's `job_id`. The caller must own the job or have it shared to them. - `device_id` (string (UUID — `remote_device_id`), required): Path param. Device's `remote_device_id`. - `data` (object, required): Bucket payload. If the linked automation has a `bucket_schema`, the data is validated against it (Zod-style). Caller must have `edit` access on the job. Responses: - **200** — Bucket created or replaced for this job/device pair. Returns the saved document. - **400** — `data` is missing or not an object, or schema validation against the automation's `bucket_schema` failed. { "error": "Invalid bucket data" } - **404** — Job not found, or the caller does not own it / does not have the required share permission (`view` for reads, `edit` for writes). { "error": "Job not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } #### DELETE /api/v1/bucket/:job_id/:device_id — Delete a bucket Removes the bucket for this job/device pair. Request body: - `job_id` (string (ObjectId), required): Path param. Job's `job_id`. The caller must own the job or have it shared to them. - `device_id` (string (UUID — `remote_device_id`), required): Path param. Device's `remote_device_id`. Responses: - **200** — Bucket for this job/device pair removed. { "success": true } - **404** — Job not found, or the caller does not own it / does not have the required share permission (`view` for reads, `edit` for writes). { "error": "Job not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } ## Device Bucket - Mount path: `/api/v1/device-bucket` - Default auth: User (both | employer | worker) - Endpoints: 3 ### Device Bucket (job-independent) A single per-device JSON object owned by the device's owner / current renter. Useful for cross-job state automations want to keep on a device (e.g. login cookies, last-run timestamps). Distinct from the per-job bucket under `/api/v1/bucket`. #### GET /api/v1/device-bucket/:device_id — Read device bucket Returns the device's bucket data, or `{}` if none has been written. Request body: - `device_id` (string (UUID — `remote_device_id`), required): Path param. The device's `remote_device_id` (uuidv4). The caller must own the device or currently rent it. Responses: - **200** — Returns the device-scoped bucket. Returns an empty object when no bucket has been written yet. { "deviceBucket": { ... } } - **404** — Device not found, or the authenticated user neither owns nor rents it. { "error": "Device not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } Example response: ```json { "deviceBucket": { "last_login": "2026-04-15T08:14:00.000Z" } } ``` #### PUT /api/v1/device-bucket/:device_id — Write device bucket Replaces the device's bucket data with the supplied object. Creates the bucket document on first write. Request body: - `device_id` (string (UUID — `remote_device_id`), required): Path param. The device's `remote_device_id` (uuidv4). The caller must own the device or currently rent it. - `data` (object, required): Bucket payload. Must be a non-null, non-array JSON object. Replaces the existing bucket entirely (no merge). Responses: - **200** — Bucket created or replaced. Returns the persisted data. { "success": true, "deviceBucket": { ... } } - **400** — `data` is missing, null, an array, or not an object. { "error": "Invalid data. Must be a non-null object." } - **404** — Device not found, or the authenticated user neither owns nor rents it. { "error": "Device not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } Example response: ```json { "success": true, "deviceBucket": { "last_login": "2026-04-15T08:14:00.000Z" } } ``` #### DELETE /api/v1/device-bucket/:device_id — Delete device bucket Removes the device's bucket document. Request body: - `device_id` (string (UUID — `remote_device_id`), required): Path param. The device's `remote_device_id` (uuidv4). The caller must own the device or currently rent it. Responses: - **200** — Bucket document deleted (no-op if it did not exist). { "success": true } - **404** — Device not found, or the authenticated user neither owns nor rents it. { "error": "Device not found" } - **500** — Unexpected DB error. { "error": "Internal server error" } Example response: ```json { "success": true } ```