REST API Reference
Base URL: http://app.localhost:3000/api/v1
All protected endpoints require Authorization: Bearer <token>.
JWT access tokens and API keys use the same header format:
Authorization: Bearer <access_token_or_easm_api_key>
Update Center endpoints are local hxEASM admin endpoints under /api/v1/admin/updates/*. The browser never calls the Central Update API directly, and the Central Update API key is backend-only.
Auth
| Method | Path | Description |
|---|---|---|
| POST | /auth/register |
Create account |
| POST | /auth/login |
Login, returns token pair |
| POST | /auth/refresh |
Refresh access token |
| POST | /auth/2fa/verify |
Verify a login 2FA challenge |
| POST | /auth/2fa/resend |
Resend email OTP for a login challenge |
| POST | /auth/2fa/email/enable |
Required-login email OTP enrollment |
| GET | /me/me |
Current user |
| GET | /me/avatar |
Download own avatar image |
| POST | /me/avatar |
Upload own avatar image |
| DELETE | /me/avatar |
Remove own avatar |
| GET | /me/2fa |
Own 2FA status |
| POST | /me/2fa/totp/setup |
Start authenticator app enrollment |
| POST | /me/2fa/totp/confirm |
Confirm authenticator app enrollment |
| POST | /me/2fa/email/enable |
Start or confirm email OTP enrollment |
| POST | /me/2fa/disable |
Disable own 2FA |
| GET | /me/users |
List users (admin) |
| GET | /me/api-keys |
List own API keys |
| POST | /me/api-keys |
Create API key |
| DELETE | /me/api-keys/{id} |
Revoke API key |
POST /auth/login
{ "email": "user@example.com", "password": "secret" }
→ { "tokens": { "access_token": "…", "refresh_token": "…" }, "user": { … } }
When 2FA is required, no tokens are returned until the challenge is verified:
{
"requires_2fa": true,
"challenge_id": "...",
"method": "totp"
}
Then call:
POST /api/v1/auth/2fa/verify
{ "challenge_id": "...", "code": "123456" }
Admin policy endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /admin/auth/2fa |
Read global 2FA policy |
| PUT | /admin/auth/2fa |
Update global 2FA policy |
POST /me/api-keys
Create an API key:
{
"name": "Assets read key",
"permissions": ["read:assets"]
}
Response:
{
"key": "easm_...",
"api_key": {
"id": "...",
"name": "Assets read key",
"key_prefix": "easm_...",
"permissions": ["read:assets"]
}
}
The raw key is only returned once when created.
User avatar
POST /me/avatar accepts multipart form data with a single avatar file field.
Allowed content types:
image/pngimage/jpegimage/webp
Maximum size: 2 MiB.
Avatar uploads and deletion are self-service only. Image bytes are stored in object storage; the users table stores private object metadata and does not store base64 image data. GET /me/avatar streams the current user's avatar through the authenticated API.
Organizations
| Method | Path | Description |
|---|---|---|
| GET | /organizations |
List orgs (admin: all; others: assigned) |
| POST | /organizations |
Create org |
| GET | /organizations/{id} |
Get org |
| PATCH | /organizations/{id} |
Update org |
| DELETE | /organizations/{id} |
Delete org |
| GET | /organizations/{id}/members |
List members |
| POST | /organizations/{id}/members |
Add member |
Scopes
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/scopes |
List scopes |
| POST | /organizations/{id}/scopes |
Create scope (→ pending) |
| PATCH | /scopes/{scope_id} |
Update scope |
| POST | /scopes/{scope_id}/approve |
Approve (admin) |
| POST | /scopes/{scope_id}/reject |
Reject (admin) |
POST /organizations/{id}/scopes
{ "type": "domain", "value": "example.com", "description": "Main domain" }
Scans
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/scans |
List scans |
| POST | /organizations/{id}/scans |
Create & queue scan |
| GET | /scans/{scan_id} |
Get scan |
| POST | /scans/{scan_id}/cancel |
Cancel scan |
| GET | /scans/{scan_id}/jobs |
List scan jobs |
POST /organizations/{id}/scans
{ "profile": "default", "scope_ids": ["<scope-uuid>"] }
Profiles are returned by GET /api/v1/scan-profiles and are defined in backend/configs/config.yaml.
Audit Log
| Method | Path | Description |
|---|---|---|
| GET | /admin/audit |
List append-only user-action audit events (admin only) |
Query params:
user_idactionevent_typeresource_typeresource_idorganization_idfromtopagepage_size
Results are sorted newest first. Default page size is 50; maximum page size is 200.
Audit Log records user/admin mutations and configuration changes. It does not audit ordinary GET/read requests and is separate from Exposure Changes.
System Backup
| Method | Path | Description |
|---|---|---|
| POST | /admin/backup/export |
Export an easm-backup-v3 ZIP bundle (admin only) |
| POST | /admin/backup/import/preview |
Validate a backup ZIP and return counts/warnings/errors (admin only) |
| POST | /admin/backup/import |
Import a valid backup ZIP (admin only) |
Export request:
{
"include_files": true,
"include_scan_history": true,
"include_settings": true
}
Import uses multipart form field file plus conflict_mode, one of skip_existing, merge, or overwrite_metadata_only.
Backup V3 preserves current non-secret product state, including Assets, Asset Graph, Asset System History, Threats, Threat History, optional Asset Scan History, files metadata, exposure changes, scan profiles, scheduled scans, plugin settings, and scan settings. Secrets, API keys, 2FA material, memberships, and Audit Log are intentionally not restored.
Fatal DB integrity errors are returned as import failures rather than ordinary warnings. Expected conflicts are reported in skipped_expected.
Scan Settings
| Method | Path | Description |
|---|---|---|
| GET | /admin/scan-settings |
Get global scan behavior settings (admin only) |
| PUT | /admin/scan-settings |
Update global scan behavior settings (admin only) |
Request/response:
{
"auto_approve_discovered_assets": false
}
Default is false. The setting applies only to newly inserted scan/plugin-discovered assets and never retroactively modifies existing assets.
Assets
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/assets |
List assets (paginated) |
| POST | /organizations/{id}/assets |
Manually create an inventory asset (admin/hacker) |
| PATCH | /organizations/{id}/assets/bulk |
Bulk approve selected inventory assets (admin/hacker) |
| GET | /organizations/{id}/assets/graph |
Get asset graph |
| GET | /organizations/{id}/assets/stats |
Count by type |
| GET | /assets/{asset_id} |
Get single asset |
| GET | /assets/{asset_id}/history |
Read dedicated per-Asset lifecycle history |
| PATCH | /assets/{asset_id}/criticality |
Manually update asset criticality (admin/hacker) |
| PATCH | /assets/{asset_id}/lifecycle |
Update approved/disabled/stale state (admin/hacker) |
| POST | /assets/{asset_id}/run-plugin |
Run one compatible plugin against one asset (admin/hacker) |
| GET | /plugins |
List registry-defined plugin catalog metadata (admin/hacker) |
| GET | /plugins/manual-capabilities |
List registry-defined manual plugin capabilities (admin/hacker) |
| GET | /admin/scan-profiles |
List built-in and custom scan profiles (admin) |
| POST | /admin/scan-profiles |
Create a database-backed custom scan profile (admin) |
| PATCH | /admin/scan-profiles/{id} |
Update a custom scan profile (admin) |
| DELETE | /admin/scan-profiles/{id} |
Delete a custom scan profile (admin) |
| GET | /admin/plugins/{plugin_id}/settings |
Read validated plugin settings (admin) |
| PUT | /admin/plugins/{plugin_id}/settings |
Save validated plugin settings (admin) |
Query params for list: type, criticality, approved, disabled, stale, sort_by, sort_order, page, page_size.
Allowed asset sort fields are type, value, criticality, lifecycle, source, confidence, first_discovered, and threat_count. sort_order accepts asc or desc. Criticality uses semantic ordering (unknown, low, medium, high, critical) instead of alphabetical ordering. threat_count is returned on each asset list item and counts Threat records associated through threats.asset_id.
POST /organizations/{id}/assets
Create an asset directly in inventory. Allowed roles: admin, hacker. Organization access is enforced by the route; clients are denied.
Request:
{
"type": "domain",
"value": "example.com",
"criticality": "unknown",
"approved": true,
"disabled": false,
"stale": false
}
Supported manual Asset types:
domain, subdomain, ip, service, webapp, certificate
The backend validates the value for the selected type and normalizes it using the asset inventory normalization rules. Manual assets are stored with source_plugin=manual. The system-managed organization Asset is not manually creatable. Legacy create requests using url are mapped to webapp; removed types such as cidr, ip_range, asn, port, path, parameter, and technology are rejected for new manual Asset creation. If an asset already exists with the same organization_id, type, and normalized_value, the API returns 409 Conflict.
Creating an Asset manually does not create or approve a Scan Scope and does not start a scan.
Asset responses include lifecycle state:
{
"approved": true,
"disabled": false,
"stale": false,
"criticality": "high"
}
Asset Lifecycle
approved means a user has confirmed the asset. Existing assets are approved during migration, while new assets created from approved scopes are approved at creation time. Automatically discovered assets are unapproved by default unless auto_approve_discovered_assets is enabled. Rediscovery never overwrites an existing asset's approval state.
disabled keeps the asset in inventory but prevents new asset-derived manual plugin execution and excludes the saved asset from target reconstruction. It does not delete the asset or suppress an independently approved scan scope.
stale is a manual persisted flag for assets considered outdated. Automatic stale rules are not implemented yet.
PATCH /api/v1/assets/{asset_id}/lifecycle
Request:
{ "approved": true, "disabled": false, "stale": true }
Allowed roles: admin, hacker. Clients can read lifecycle state but cannot modify it.
GET /assets/{asset_id}/history
Read the dedicated per-Asset history timeline. Access follows the existing asset RBAC rules.
Query params:
| Parameter | Meaning |
|---|---|
page |
Page number, default 1. |
page_size |
Page size, default 20, maximum 100. |
event_type |
Optional event type filter. |
source_type |
Optional source filter: user, scan, or system. |
Response:
{
"items": [
{
"id": "history-id",
"organization_id": "organization-id",
"asset_id": "asset-id",
"event_type": "asset.criticality.changed",
"field": "criticality",
"old_value": "medium",
"new_value": "high",
"source_type": "user",
"actor_user_id": "user-id",
"actor_username": "admin@example.com",
"metadata": {
"asset_type": "service"
},
"created_at": "2026-08-23T12:00:00Z"
}
],
"total": 1
}
Tracked V1 event types:
asset.discoveredasset.created.manualasset.approval.changedasset.disabled.changedasset.stale.changedasset.criticality.changed
Asset History is append-only through normal application APIs. There are no user-facing create, update, or delete history endpoints.
Asset History does not backfill existing assets and does not record noisy refresh fields such as last_seen_at, updated_at, or full metadata churn.
GET /assets/{asset_id}/scan-changes
Read field-level scanner-observed changes for an Asset. Access follows the existing asset RBAC rules. The endpoint returns asset_scan_changes only; snapshots are not exposed through this API.
Query params:
| Parameter | Meaning |
|---|---|
period |
Optional 7d, 30d, 90d, or all. |
limit |
Optional max rows, default 100, maximum 200. |
Response:
{
"items": [
{
"id": "change-id",
"asset_id": "asset-id",
"scan_id": "scan-id",
"scan_job_id": "scan-job-id",
"change_type": "changed",
"field_path": "service.version",
"old_value": "1.22",
"new_value": "1.24",
"source_plugin": "nmap",
"observed_at": "2026-09-12T10:00:00Z",
"created_at": "2026-09-12T10:00:01Z"
}
],
"total": 1
}
GET /assets/{asset_id}/change-summary
Read day-bucketed counts for the Asset Detail change chart. Access follows the existing asset RBAC rules.
Query params:
| Parameter | Meaning |
|---|---|
period |
Optional 7d, 30d, 90d, or all; default 30d. |
Response:
{
"period": "30d",
"buckets": [
{
"bucket_start": "2026-09-12T00:00:00Z",
"system_changes": 1,
"scan_changes": 3
}
],
"total": {
"system_changes": 1,
"scan_changes": 3
}
}
PATCH /organizations/{id}/assets/bulk
Bulk approve selected inventory assets. Allowed roles: admin, hacker. The route organization controls access; clients are denied.
Request:
{
"asset_ids": ["asset-uuid-1", "asset-uuid-2"],
"approved": true
}
Response:
{
"requested": 14,
"updated": 10,
"unchanged": 4
}
The endpoint is intentionally narrow and only supports approved=true. Batch size is limited to 500 IDs. The operation validates that all requested assets belong to the route organization before updating. Disabled, stale, and criticality values are not changed. In the frontend, the All Assets header checkbox selects only the currently loaded page/list response, not every matching asset across all pages.
Asset Criticality
Asset responses include criticality, criticality_source, criticality_updated_at, and criticality_updated_by. Values are unknown, low, medium, high, and critical; new assets default to unknown.
Criticality is business importance, not vulnerability severity. A low-severity issue on a critical VPN may be more important than a medium issue on a test host.
For this MVP, criticality is manual-only and criticality_source is always manual. hxEASM does not infer it from hostnames, banners, technologies, vulnerabilities, exposure changes, LLMs, agents, or heuristics. Suggested criticality and future risk scoring are planned future work.
PATCH /api/v1/assets/{asset_id}/criticality
Allowed roles: admin, hacker. Clients can read criticality but cannot modify it. The endpoint uses the same organization-scoped asset access checks as other asset endpoints.
Request:
{ "criticality": "high" }
Response: the updated asset.
POST /assets/{asset_id}/run-plugin
Launch a single plugin against a selected asset. This creates a normal queued scan with internal profile manual_scan and exactly one scan job. Allowed roles: admin, hacker. Clients are denied.
Request:
{ "plugin": "nuclei" }
Response:
{
"scan_id": "...",
"scan_job_id": "...",
"status": "queued"
}
If the plugin does not support the selected asset type, the API returns 400:
{ "error": "plugin katana does not support asset type ip" }
Manual scans appear in GET /organizations/{id}/scans like any other scan. Their scan metadata includes manual, asset_id, asset_type, asset_value, and plugin.
If the selected asset is disabled, the API returns 400 and does not queue a scan:
{ "error": "asset is disabled and cannot be used for plugin execution" }
GET /plugins/manual-capabilities
Returns enabled plugins that declare manual_scan in supported_execution_modes. The frontend uses this endpoint to build the Run Plugin menu dynamically; there is no frontend plugin allowlist.
[
{
"id": "httpx",
"name": "httpx",
"type": "http_probe",
"supported_asset_types": ["domain", "subdomain", "ip", "service"],
"supported_execution_modes": ["profile_scan", "manual_scan", "retry"],
"manual_execution": true
}
]
Disabled plugins and plugins without manual_scan are omitted.
API key example: read:assets
Use an API key as a Bearer token. For example, with:
easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b
List assets for an organization:
curl "http://app.localhost:3000/api/v1/organizations/<organization_id>/assets?page=1&page_size=20" \
-H "Authorization: Bearer easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b"
Filter by asset type:
curl "http://app.localhost:3000/api/v1/organizations/<organization_id>/assets?type=domain&page=1&page_size=20" \
-H "Authorization: Bearer easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b"
Get the asset graph:
curl "http://app.localhost:3000/api/v1/organizations/<organization_id>/assets/graph" \
-H "Authorization: Bearer easm_b3d070d84cf7c121927d0bcbed23fcc287aaa29575f8d5dffe54919588458e0b"
Replace <organization_id> with the UUID of the organization to query.
Threats
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/threats |
List Threats with affected asset context (paginated) |
| POST | /organizations/{id}/threats |
Create a manual Threat |
| GET | /organizations/{id}/threats/stats |
Count Threats by severity |
| GET | /organizations/{id}/threats/{threat_id}/detections |
List scan detections recorded for a Threat |
| POST | /organizations/{id}/threats/{threat_id}/exclusion |
Add the Threat finding to persistent exclusions |
| GET | /organizations/{id}/threat-exclusions |
List persistent Threat exclusions |
| DELETE | /organizations/{id}/threat-exclusions/{exclusion_id} |
Remove a persistent Threat exclusion |
| GET | /threats/{id} |
Get a single Threat with affected asset context |
| PATCH | /threats/{id} |
Update mutable Threat fields. has_exploit is vulnerability-only. |
| PATCH | /threats/{id}/status |
Update workflow status |
| POST | /threats/{id}/retest |
Request vulnerability retest |
Compatibility: existing /vulnerabilities routes remain available for legacy clients, but they expose only threat_type=vulnerability records. Legacy create requests always create vulnerability Threats, even if threat_type is omitted or set differently. New integrations should use /threats.
GET /organizations/{id}/threats
Supported query parameters:
| Parameter | Description |
|---|---|
threat_type |
Threat type: vulnerability, data_leak, misconfiguration, phishing, or employee. |
severity |
Existing severity filter. |
status |
Existing status filter. |
asset_id |
Exact affected asset UUID. |
asset_type |
Asset type, for example ip, service, or webapp. |
asset |
Case-insensitive search across asset value and common target fields. |
source_plugin |
Source plugin, for example nuclei or nmap_vulns. |
port |
Vulnerability metadata port filter. |
protocol |
Vulnerability metadata protocol filter. |
has_exploit |
Vulnerability-only filter. When set, results are narrowed to vulnerability Threats. |
page, page_size |
Pagination. |
Each item includes common Threat fields plus current asset context when available: asset_type, asset_value, asset_criticality, asset_source_plugin, scan_id, and scan_job_id. risk_score is returned on a canonical 0-100 scale.
threat_type=vulnerability responses also include vulnerability-specific fields such as affected_host, affected_port, affected_protocol, affected_service, affected_url, cvss_score, has_exploit, and has_exploit_source. cvss_score remains the standard 0-10 CVSS value and is not the same metric as risk_score. Non-vulnerability Threat responses do not emit misleading top-level vulnerability defaults; their type-specific values are returned in type_details and details.
Plugin-discovered vulnerability Threats are deduplicated into one current Threat row. Repeated detections in later scan jobs are exposed through the organization-scoped detections endpoint. Historical Threats created before detection history may return an empty detections list.
Threat exclusions suppress future plugin detections by exact identity: organization_id + asset_id + external_id. The server derives these values from the persisted Threat; clients may send only an optional reason. source_plugin is retained on the exclusion as informational metadata for the rule that was originally created, but it does not participate in uniqueness or suppression matching. Creating an exclusion does not delete, close, or modify the existing Threat. Threats with an empty external_id cannot be excluded in V1. Removing an exclusion deletes only the suppression rule; historical Threat and detection rows remain unchanged, and future matching detections flow through the normal Threat upsert and detection-history pipeline again.
Example:
curl "http://app.localhost:3000/api/v1/organizations/<organization_id>/threats?threat_type=vulnerability&asset_type=service&asset=192.0.2.10&source_plugin=nmap_vulns" \
-H "Authorization: Bearer <token>"
POST /organizations/{id}/threats
Creates a manual Threat in the organization. Allowed for administrators and hackers with organization access.
Required fields:
{
"threat_type": "vulnerability",
"asset_id": "<asset_uuid>",
"title": "Exposed admin panel",
"severity": "high",
"description": "The admin panel is reachable from the internet."
}
Allowed threat_type values are vulnerability, data_leak, misconfiguration, phishing, and employee. All manual Threats start with status=new and close_reason=null.
Optional common fields include remediation and type_details. Vulnerability Threats may also include cve, cwes, references, has_exploit, affected_port, affected_protocol, affected_service, affected_url, and cvss_score.
PATCH /threats/{id}
Allowed for administrators and hackers with access to the Threat organization. The initial mutable field is vulnerability-only has_exploit.
{ "has_exploit": true }
Manual updates set has_exploit_source=manual.
PATCH /threats/{id}/status
{ "status": "closed", "reason": "fp", "note": "Verified manually" }
Valid status transitions:
- new → active
- new → closed with reason=fp
- active → in_progress
- active → closed with reason=skipped
- in_progress → review
- review → closed with reason=fixed
- review → active
Closed Threats require reason to be one of fp, fixed, or skipped. Non-closed Threats must not include a close reason. Admin users may perform any valid workflow transition as an operational override; hacker users perform security-team transitions; client users perform customer-lane transitions for organizations they can access.
POST /threats/{id}/retest
Requests a retest for a vulnerability Threat in review. Allowed for administrators and hackers with access to the Threat organization. The action does not automatically mark the Threat fixed; an administrator or hacker must decide the final review → closed/fixed or review → active transition after retest evidence is reviewed.
Reports
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/reports |
List generated reports |
| POST | /organizations/{id}/reports |
Create report generation request |
| GET | /organizations/{id}/reports/{report_id} |
Get report metadata |
| GET | /organizations/{id}/reports/{report_id}/download |
Download generated report file |
Supported formats: json, csv, pdf, html.
Reports are organization-scoped. Admin, hacker, and client users can list, create, and download reports for organizations they are authorized to access. The server records the authenticated user as the report initiator; clients cannot supply or spoof another initiator.
Example request:
{
"format": "pdf",
"filters": {
"severity_min": "medium",
"scan_id": "<scan-uuid>"
}
}
Example response:
{
"id": "<report-uuid>",
"organization_id": "<organization-uuid>",
"created_by": "<user-uuid>",
"initiated_by": {
"id": "<user-uuid>",
"username": "demo-client",
"role": "client"
},
"format": "pdf",
"status": "ready",
"created_at": "2026-09-01T14:22:08Z"
}
Historical report records without created_by are returned without initiated_by; clients should display an unknown-user fallback. Downloads use dated filenames in Content-Disposition, for example hxeasm-report-2026-09-01_14-22-08-<report-prefix>.pdf.
PDF reports use the styled report renderer. HTML reports use the embedded template at backend/internal/reports/templates/easm_report.html.
Health check
GET /health
→ { "status": "ok", "time": "2026-04-29T…" }
Error format
All errors return JSON:
{ "error": "description" }
Exposure Changes
| Method | Path | Description |
|---|---|---|
| GET | /organizations/{id}/changes |
List exposure change timeline events |
| GET | /organizations/{id}/changes/summary |
Summary counters for recent changes |
| GET | /assets/{asset_id}/changes |
History for a single asset |
List query params: change_type, entity_type, severity, asset_id, threat_id, scan_id, date_from, date_to, limit, offset. vulnerability_id is accepted as a compatibility alias for vulnerability Threat events.
Summary query params: days defaults to 7.
Example summary response:
{
"days": 7,
"total": 21,
"asset_discovered": 10,
"service_discovered": 4,
"webapp_discovered": 5,
"url_discovered": 0,
"path_discovered": 0,
"certificate_discovered": 0,
"vulnerability_discovered": 2,
"file_collected": 0,
"critical": 1,
"high": 3
}
Exposure changes are read-only through the public API. They are generated internally when new assets, vulnerability Threats, vulnerability status transitions, and file artifacts are persisted.
Scan profile administration
GET /api/v1/admin/scan-profiles returns built-in config-backed profiles and DB-backed custom profiles with metadata such as source, built-in flag, enabled state, plugin count, and supported scope types.
POST /api/v1/admin/scan-profiles creates a global custom profile:
{
"id": "custom_web",
"name": "Custom Web",
"description": "HTTP discovery and nuclei",
"plugins": ["subfinder", "dnsx", "httpx", "nuclei"],
"enabled": true
}
Custom profile IDs cannot override config-backed profile IDs.
Plugin catalog and settings
GET /api/v1/plugins returns registry metadata for enabled and disabled plugins, including supported asset types, supported execution modes, profile usage, and option schema.
PUT /api/v1/admin/plugins/{plugin_id}/settings validates submitted JSON against the plugin option schema before saving. The MVP does not support arbitrary CLI arguments or secret storage.
Threat Discussions
| Method | Path | Description |
|---|---|---|
| GET | /threats/{id}/comments |
List comments for a Threat. Clients receive only client-visible comments. |
| POST | /threats/{id}/comments |
Create a Threat comment. |
| PATCH | /threats/{id}/comments/{comment_id} |
Edit your own Threat comment. |
| GET | /organizations/{id}/mentionable-users |
Search users that may be mentioned in an organization context. |
Compatibility: /vulnerabilities/{id}/comments routes remain available as aliases.
Create/update comment body:
{
"body": "Please review this with @alex.",
"visible_to_client": false,
"mentioned_user_ids": ["<user-uuid>"]
}
Admin and hacker users may choose visible_to_client. Client-created comments are forced client-visible by the backend.
Profile Notifications
| Method | Path | Description |
|---|---|---|
| GET | /me/notifications |
List the current user's notifications. |
| POST | /me/notifications/{id}/read |
Mark one notification read. |
| POST | /me/notifications/read-all |
Mark all current-user notifications read. |
Notification listing supports page and page_size with a default page size of 20 and maximum of 100.