CRM API — Довідник ендпоінтів

Arc OS — The Orchestration System for AI Teams

Загальна інформація

Параметр Значення
Base URL https://arc-os.co/api/crm
Авторизація Authorization: Bearer <JWT> або ?token=<JWT> (для SSE/WebSocket)
Content-Type application/json
JWT алгоритм HMAC-SHA256
JWT TTL 24 години

Автентифікація

Всі ендпоінти (окрім /docs/*) вимагають JWT токен у заголовку Authorization: Bearer <token>.

Для SSE та WebSocket з'єднань токен передається через query-параметр ?token=<JWT>.

Помилки авторизації

Код Опис
401 Відсутній або невалідний токен
403 Немає доступу до проєкту (multi-tenancy)

Ендпоінти по категоріях

Акаунт та налаштування

Метод Шлях Опис
GET /account/settings Отримати налаштування акаунту
PUT /account/settings Оновити налаштування акаунту

Онбординг + Trial Credits (Phase 50.1)

Метод Шлях Опис
POST /onboarding/setup Створити перший проект. Body multipart: config (JSON) + files. Поле anthropicKey тепер опціональне — якщо empty + user has email_verified + не отримував trial раніше, проект створюється у trial_mode=1 зі 100K free tokens. Response: { ok, project, trial_activated }. Phase 51: повертає 402 з {error:"plan_limit_reached", reason, current, limit, plan} коли користувач перевищив project limit для плану.
GET /account/trial-status Trial status для UI banner. Response: { email, email_verified, trial_granted, has_trial_active, total_remaining, total_granted, projects: [...] }
GET /account/usage Token usage history для авторизованого користувача (Phase 63, #148). Response: { rows: [ { project_name, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at } × up to 200 ], totals: { total, input, output } }. Читає token_usage_log по owner_id. Показується в UserDropdown (UsageCard) і BillingPage (Token Usage секція).
GET /account/billing-summary Зведений billing summary (#309). Response: { arc: { plan, status, tokens_this_month, tokens_input, tokens_output, renewal_date }, anthropic: { connected, key_prefix, credit_balance_usd, spend_month_usd, tokens_this_week } }. Anthropic-секція заповнюється server-side через Anthropic API з user's account_settings.anthropic_key (fallback → PLATFORM_ANTHROPIC_KEY). Показується в UserDropdown UsageCard.

Onboarding Checklist (Phase 54.1, issue #56)

Post-wizard 5-step engagement checklist. Кожен крок (workers, cli, skill, bot, issue) приймає статус completed або skipped. Мутації ідемпотентні: повторний ідентичний POST повертає той самий state, не пише дублікат в activity_log. Replay не скидає state, лише знімає dismissed_at — UI знову показує панель з тим самим прогресом.

Метод Шлях Опис
GET /onboarding/progress Поточний стан для авторизованого користувача. Response: { steps:["workers","cli","skill","bot","issue"], state:{<step>:<status>}, completed_count, total_steps:5, completed_at, dismissed_at, source, started_at, updated_at }. Untouched user → нулі/null без створення рядка.
POST /onboarding/event Записати step transition. Body: { step: "workers"|"cli"|"skill"|"bot"|"issue", status: "completed"|"skipped", source?: "web"|"cli" }. Whitelist validation → 400 на невідомий step/status. Response: той самий shape як GET. Емітує onboarding_step_completed/onboarding_step_skipped в activity_log лише при changed; при transition до 5/5 додатково емітує onboarding_completed з duration_ms.
POST /onboarding/dismiss Закрити панель (dismissed_at = now). Ідемпотентно. Емітує onboarding_dismissed при першому виклику з payload {completed_count}.
POST /onboarding/replay Знову відкрити закриту панель (dismissed_at = NULL). Step state не зачіпається. Емітує onboarding_replayed при clear-event.
POST /projects/:name/active-issue Issue #115. Bind current web session to an issue. Body: { issue_id: number, title?: string }. Writes activity_log event session_active_issue (source=web).
GET /projects/:name/active-issue Issue #115. Latest bound issue for this owner within 7d. Response: { active_issue_id, title, ts }.
GET /onboarding/cli-status Phase 54.3 (issue #58). Чи логінився user через arc login за останні 30 днів? Response: { installed: boolean, last_cli_at: string|null }. SSOT — рядки в activity_log з event_type='cli_invocation' і actor=chatId. Frontend onboarding-чекліст polls цей endpoint кожні 10s доки CLI крок pending; коли installed=true — автоматично марк cli step як completed.
GET /analytics/onboarding-funnel Phase 54.6 (issue #61). Aggregate funnel stats over rolling window. Query: hours=168 (1-720, default 7d). Response: { hours, total_steps:5, started_users, completed_users, completion_rate, per_step: [{step, completed, skipped}…], duration_p50_ms, duration_p90_ms, ttfc_p50_ms, ttfc_sample_size }. SSOT — activity_log events onboarding_step_* + onboarding_completed + cli_invocation. TTFC = time-to-first-arc (julianday delta з першого onboarding-step до першого cli_invocation per actor).

SSOT для funnel-метрик (Phase 54.6 / issue #61) — події в activity_log (event_type LIKE 'onboarding_%'). Таблиця onboarding_progress — derived cache: UI рендериться одним запитом замість агрегації по подіях.

Beta Feedback (Phase 53.3)

Метод Шлях Опис
POST /feedback Надіслати beta feedback. Body: {type: "bug"|"feature"|"other", title, description, project?, browser?}. Записує в activity_log (event_type=feedback_report) і пінгає CEO у Telegram.
GET /admin/feedback Список останніх submissions (admin only). Query: limit=50 (max 500). Response: {items: [...], count}.
POST /feedback/translation Надіслати переклад-issue (Phase 59.4). Body: {locale, msgid, suggestion, severity: "minor"|"major"|"wrong", current_translation?, page_url?}. Зберігає в translation_feedback.
GET /admin/translations Список translation feedback (admin). Query: locale, status=open|accepted|rejected|all, limit. Response: {items, count}.
GET /admin/translations/stats Per-locale health stats (admin). Response: {stats: [{locale, total, open_count, accepted, rejected, critical_open}]}.
POST /admin/translations/:id/accept Прийняти пропозицію — патчить .po файл на диску. Body: {note?}. Response: {ok, po_patched, glossary_suggestion}.
POST /admin/translations/:id/reject Відхилити пропозицію. Body: {note?}. Response: {ok}.

POST /feedback/translation — validates: locale ∈ {uk,de,es,fr,pl,pt-BR,ru}, msgid ≤1000, suggestion ≤2000, severity ∈ {minor,major,wrong}. After 3+ accepted suggestions for same msgid → glossary_suggestion: true in accept response.

Floating widget у FeedbackWidget.jsx тепер має 4-й тип «Translation» — auto-fills locale з i18n.locale, захоплює msgid + suggestion + severity.

Arc Help AI Chat (Phase 61, #147)

Метод Шлях Опис
POST /help/chat In-app AI Q&A. Body: {message, history: [{role,text}]}. Response: {reply, sources: string[], remaining, limit}. Rate limit: 30/day/user.
GET /help/usage Поточний ліміт. Response: {remaining, limit, used}.

POST /help/chat — pipeline: (1) rate-limit check (429 if exceeded), (2) RAG via shared/rag.ts (Cohere + sqlite-vec, Phase 71) merging project + _global_ skill hits → fallback keyword search of docs/public/, (3) Claude Haiku with system prompt + doc context + history. message ≤2000 chars. Відповідає мовою запиту.

Beta Invites (Phase 52.1, admin-only)

Метод Шлях Опис
GET /admin/dashboard System Dashboard (Phase 60.9, #145). Admin-only. Returns: CPU/RAM/Disk from /proc, users by plan, container fleet, last-50 activity events, waitlist + project + issue stats.
GET /admin/wipe-metrics WIP-E telemetry dashboard (#308). Admin-only. Returns: {render: {count, avg_ms, p50_ms, p95_ms, max_ms}, interaction: {count, avg_per_session, p95_per_session, max_per_session, sessions_zero}, by_worker: [{worker_id, render_count, avg_render_ms, session_count, avg_interactions}], daily: [{date, renders, interactions, avg_render_ms}], recent: [...]}.
GET /admin/waitlist Список усіх waitlist заявок. Лише admin. Response: {entries: [{id, email, message, status, created_at}]}.
POST /admin/waitlist/:id/approve Апрувнути заявку — генерує invite code (arc-XXXX-XXXX), відсилає email з кодом, оновлює status→approved. Response: {ok, invite_code, email_sent}.
POST /admin/waitlist/:id/reject Відхилити заявку. Response: {ok}.
GET /admin/invites Список всіх invite codes + counts (total_active, total_used). Лише admin.
POST /admin/invites Згенерувати N кодів. Body: {count: N, note?: string}. Лише admin. Response: {ok, codes, count}.
DELETE /admin/invites/:code Revoke unused invite code.
/admin/notebooklm/* Видалені у Phase 71.8 разом із NotebookLM Bridge. Семантичний пошук тепер працює через self-hosted RAG (rag-architecture.md).

Auth flow update: POST /api/auth/register тепер вимагає поле invite_code (Phase 52.1 closed beta). Без коду → 403 {error: "invite_required"}. Невалідний/used код → 403 {error: "invalid_invite"}.

Standard Cloud — WebSocket Terminal + SSE Logs (Phase 60 #139)

Протокол Шлях Опис
WS /ws/cloud/:userId/terminal?token=<JWT> Проксі до docker exec -i <containerId> /bin/bash. IDOR: userId мусить збігатись з chatId з JWT. Paused container авто-відновлюється. Вхідні WS frames → container stdin; stdout+stderr → WS frames.
SSE /api/sse/cloud/:userId/logs docker logs -f --tail 50 для container user. Auth: Bearer JWT. IDOR: userId === chatId. Events: data: {"line": "..."} per рядок, data: {"closed": true} при виході.

Standard Cloud (Phase 60)

Метод Шлях Опис
POST /cloud/claude-verify Перевіряє claude --version в container (transport-safe shell-quoted через SSH у remote-host режимі, #329). Встановлює claude_authed=true. Response: { ok, output }
POST /cloud/ssh-keygen Генерує ed25519 ключ в container (idempotent). Response: { public_key }
POST /cloud/ssh-verify ssh -T [email protected] в container. Встановлює github_authed=true при успіху. Response: { ok, output }
POST /cloud/provision Провіжнінг Docker container для user. Вимагає план cloud, 402 інакше. Idempotent: якщо container вже існує — повертає поточний стан. Response: { container_id, status, server_ip, port, claude_authed, github_authed }
GET /cloud/status Стан container + live docker inspect reconciliation. Response: { container_id, status, server_ip, internal_port, claude_authed, github_authed, docker_running, last_active, created_at } або { status: "none" }
POST /cloud/deprovision Зупинити + видалити container (docker stop + docker rm -f + docker network rm arc-net-{id}). Оновлює status=deleted у DB. Response: { ok: true, container_id }

Статуси container: provisioningreadypausedsuspended / deleted.

Security (SEC-60 #152, #154, #155, #156): кожен container ізольований у власній мережі arc-net-{id} (lateral movement prevention). SSH-з'єднання Contabo→Hetzner через dedicated arcapi user (docker group, no root) з docker-only wrapper — non-docker команди заблоковані на рівні authorized_keys. ARC_TOKEN ін'єктується через docker exec після старту (не видно у docker inspect). git clone обмежений timeout 60. WebSocket idle timeout: 120s. SSE docker logs обмежений --since 1h. IDOR prevention: всі endpoints звіряють container.user_id === req.userId. Security flags при docker run: --cap-drop=ALL --security-opt=no-new-privileges --cpus=1.5 --memory=2g --pids-limit=200. Volumes: arc-{id}-workspace:/workspace, arc-{id}-claude:/home/arcuser/.claude, arc-{id}-ssh:/home/arcuser/.ssh. Lifecycle (#141): GET /cloud/status завжди оновлює last_active. Idle 30 хв → docker pause (cron кожні 5 хв, scripts/cloud-lifecycle-cron.ts). Wake: CRM message, TG message, WS upgrade → docker unpause автоматично.

Waitlist (#134):

Метод Шлях Опис
POST /cloud/waitlist Приєднатись до черги. Idempotent. Response: { position, status, joined_at, message }. 409 якщо вже на cloud плані або вже є container.
GET /cloud/waitlist/status Власний статус у черзі. Response: { position, status, joined_at, invited_at } або { status: "not_joined" }.
GET /cloud/waitlist Admin only. Повний список + stats. Response: { stats: { total, waiting, invited, activated }, list: [...] }.
POST /cloud/waitlist/invite Admin only. Запросити user. Body: { user_id }. Встановлює status=invited + автоматично апгрейдить план до cloud. Response: { ok, user_id, position }.

Billing (Phase 51 → #202 Plata by mono)

Phase #202: Stripe замінено на Plata by mono (monobank internet acquiring). Recurring підписки через tokenization (картка зберігається при першому платежі).

Метод Шлях Опис
GET /billing/status Поточний план, ліміти, usage, features. Response: { plan, status, current_period_end, next_billing_date, plata_masked_pan, limits, usage, features, pricing, can_upgrade, plata_ready }
POST /billing/checkout-session Створює Plata invoice з tokenization. Body: { plan: "min"|"cloud", success_url?, cancel_url? }. Response: { url, invoice_id, plan, amount_uah }. 503 якщо PLATA_MERCHANT_TOKEN не в vault.
POST /billing/webhook Plata callback (NO CRM auth — verified by X-Token header). Статуси: success (активує план + зберігає cardToken), failure/expired (інкрементує billing_failures, 3+ → downgrade to free). Idempotent via plata_events table.
POST /billing/cancel Скасувати підписку (downgrade to free). Паузить Docker container для cloud плану. Response: { ok, plan: "free" }.

#205 (2026-05-26): Legacy /billing/portal-session route was removed alongside Stripe dead code. Use /billing/cancel to cancel a subscription.

Plan limits (OR-semantic):

402 response на POST /onboarding/setup або POST /projects/:name/workers коли ліміт перевищено: { error: "plan_limit_reached", reason: "projects_limit"|"workers_limit", current, limit, plan, message }

Admin-користувачі (role=admin) обходять plan-limit перевірку повністю — вони оператори, не платні тенанти.

Beta-тестери (subscriptions.plan='beta', Phase 52 F&F) також обходять — необмежена кількість проєктів/воркерів плюс усі Max-фічі. Призначається вручну: UPDATE subscriptions SET plan='beta' WHERE user_id=?.

Bugfix (issue #25): POST /projects/create (Quick Start, Phase 50.2) раніше падав із ownerChatId is not defined через typo — виправлено, audit-actor тепер коректно записується.

Bugfix (issue #26): allocatePort() для нових проєктів тепер пробить реальні TCP-bindings (ss -tln), а не лише registry. Раніше міг видати порт зайнятий не-registry сервісом (NotebookLM bridge :19213, internal bridges) → workspace bot падав на EADDRINUSE.

Auth flow (Phase 50.1): /api/auth/register і /api/auth/login тепер повертають JWT навіть для unverified email + flag needs_verification: true. Сенситивні дії (trial grant, billing, invites) перевіряють email_verified окремо. Rate limit на signup: 3 / IP / 24h.


Проєкти (9 ендпоінтів)

Метод Шлях Опис
GET /projects Список проєктів користувача
POST /projects/create Створити проєкт — body: {displayName, projectName, niche?, teamPreset?}; для trial-юзерів автоматично встановлює trial_mode=1 та інжектить PLATFORM_ANTHROPIC_KEY
POST /projects/create-with-team Атомарне створення проєкту + воркерів + (опц.) TG бота за один запит — body: {project, workers[], telegram?}; rollback при помилці
GET /projects/suggest-preset Підказка пресету по ніші — query: niche=<text>; повертає {preset_id} на основі keyword map
GET /projects/:name Деталі проєкту
GET /projects/:name/config Конфігурація проєкту
PUT /projects/:name/config Оновити конфігурацію
POST /projects/:name/upload-icon Завантажити PNG/GIF іконку проєкту
POST /projects/:name/workers/:id/upload-icon Завантажити PNG/GIF іконку воркера
GET /projects/:name/protocol Протокол проєкту
PUT /projects/:name/protocol Оновити протокол
GET /projects/:name/logs Логи проєкту
GET /projects/:name/metrics Метрики проєкту

POST /projects/create — body:

{
  "technical_name": "string",
  "displayName": "string",
  "description": "string",
  "icon": "string",
  "color": "string"
}

GET /projects/:name/logs — query: category, lines

GET /projects/:name/metrics — query: since, until


Воркери (11 ендпоінтів)

Метод Шлях Опис
GET /workers #304 Phase A — всі воркери всіх проєктів поточного користувача. Response: { workers: [{ id, label, icon, type, model, tools, context_assets, project_name }] }. Фільтрація за owner_id (multi-tenancy). CEO бачить усі проєкти.
GET /workers/presets #228 — глобальний preset library (project-agnostic). Повертає 13 воркерів з канонічного config/workers_registry.json: { presets: [{ id, label, icon, type, model, max_turns, tools, system_prompt, context_assets, focus_dirs, prompt_style }] }. Використовується WorkerCreationWizard для Step 1.
GET /workers/templates #304 Phase I — шаблони поточного користувача. Response: { templates: [{ id, name, description, config, is_public, created_at }] }.
POST /workers/templates #304 Phase I — зберегти/оновити шаблон. Body: { name, description?, config }. Response: { ok, id }.
DELETE /workers/templates/:id #304 Phase I — видалити шаблон (лише власник). Response: { ok }.
GET /projects/:name/workers Список воркерів
POST /projects/:name/workers Створити воркера
POST /projects/:name/workers/reorder Phase 53.8 — переставити порядок воркерів. Body: {order: [id1, id2, ...]}. Атомарно перезаписує workers_registry.json. Воркери відсутні в order додаються в кінець (захист від втрати). Response: {ok, count, order}.
PUT /projects/:name/workers/:id Оновити воркера
DELETE /projects/:name/workers/:id Видалити воркера
POST /projects/:name/workers/generate-prompt Згенерувати системний промпт
GET /projects/:name/workers/:id/telegram-token Отримати Telegram токен
POST /projects/:name/workers/:id/telegram-token Phase 53.4 — валідує токен через Telegram getMe, зберігає bot_username у vault, відмовляє якщо той самий бот уже привʼязаний до іншого воркера (409). Response: {ok, started, bot_username}.
DELETE /projects/:name/workers/:id/telegram-token Видалити Telegram токен
POST /projects/:name/workers/:id/avatar #304 Phase D — завантажити аватар (multipart file, JPEG/PNG/WebP, max 2 MB). Magic-byte перевірка. Зберігає у data/worker-avatars/, записує в worker_avatars (migration 043). Response: { ok, url }.
GET /projects/:name/workers/:id/avatar #304 Phase D — отримати аватар бінарно (Content-Type відповідно до MIME). 404 якщо аватар не завантажено.
DELETE /projects/:name/workers/:id/avatar #304 Phase D — видалити аватар, скинути avatar_pack='role' у worker JSON.
GET /projects/:name/workers/:id/activity #306 — activity feed воркера (останні 50 подій). Merged: activity_log (actor=workerId) + project_issues.activity (author=workerId) + token_usage_log (daily snapshots). Response: { events: [{ type, title, detail, when }] }. Types: git_commit, skill_loaded, skill_unloaded, issue_pick, issue_close, issue_log, token_budget, session_start.
GET /projects/:name/workers/:id/runtime #306 — runtime state воркера. Response: { status: 'working'|'idle', status_started_at, tokens_today, tokens_pct, tokens_cap, current_skill }. Reads from workers_runtime_state (migration 045) first; staleness fallback: status='working' + tmux dead + updated_at > 10 min → idle (crash detection). Plan-based daily cap via subscriptions.plan lookup: free=100K, starter=400K, starter_cloud=2M, beta=unmetered (returns tokens_cap: null, tokens_pct: 0). Poll interval 15s.
POST /projects/:name/workers/:id/notify Phase 53.2 — надіслати TG event ping ({event?, text, buttons?}). Silent no-op якщо токен не привязано чи CRM_DISABLE_TG_NOTIFY=1.
POST /projects/:name/workers/:id/suggest-bot-username 53.11.1 (issue #48) — повертає 5 кандидатів TG username для bot-creation wizard у форматі <project>_<worker>_bot + numbered fallbacks. Slugify зрізає hyphens, truncate до 32 chars (worker-частина зрізається першою). Response: {candidates: string[]}.
POST /metrics/wizard 53.11.1 (issue #48) — telemetry sink для bot-creation wizard. Body: {action, duration_ms?, attempts?, success?, project?, worker_id?, locale?} (#124: locale_active/locale_switch events). Пише в activity_log (event_type=wizard_metric), best-effort.
GET /analytics/wizard-metrics?hours=168 53.11.1 (issue #48) — funnel summary: {starts, completions, abandons, success_rate, avg_duration_ms_completed, avg_attempts_completed, by_action}. Default 7 днів, clamp 1-720h.
POST /projects/:name/restart Перезапустити воркера
GET /projects/:name/active-role Поточна активна роль
POST /projects/:name/active-role Змінити активну роль

POST /projects/:name/workers — body:

{
  "label": "string",
  "icon": "string",
  "type": "terminal | telegram",
  "model": "string",
  "max_turns": 20,
  "tools": ["Read", "Write", "Bash"],
  "system_prompt": "string",
  "focus_dirs": ["src/", "docs/"]
}

max_turns за замовчуванням 20 (раніше було 5, спричиняло помилку "Reached max turns" у багатоступеневих діалогах із tool calls).

POST /projects/:name/restart — query: worker_id


Файли та сховище (8 ендпоінтів)

Метод Шлях Опис
GET /projects/:name/files Дерево файлів
POST /projects/:name/files/upload Завантажити файл (multipart, max 100MB)
POST /projects/:name/files/mkdir Створити директорію
POST /projects/:name/files/create Створити файл
GET /projects/:name/files/read Прочитати файл
PUT /projects/:name/files/save Зберегти файл
DELETE /projects/:name/files/delete Видалити файл
POST /projects/:name/files/clone Git clone репозиторію

GET /projects/:name/files — query: path

GET /projects/:name/files/read — query: path, raw


Навички / Skills (18 ендпоінтів)

Навички проєкту

Метод Шлях Опис
GET /projects/:name/skills Список навичок проєкту. Повертає глобальні (owner_project=NULL) + навички цього проєкту (owner_project=name). Чужі проєктні навички не включаються (#157).
POST /projects/:name/skills Створити навичку. Зберігається з owner_project=name, видима тільки цьому проєкту.
PUT /projects/:name/skills/:id Оновити навичку
DELETE /projects/:name/skills/:id Видалити навичку

#210 (2026-05-26): DB (skills_global) is now the SSOT writer. UI saves go to DB first; .claude/skills/<name>/SKILL.md is written through as an artifact so Claude Code CLI auto-discovers skills. Legacy skills/<name>.md writes were removed — existing files are no longer read or maintained. Migration helper: scripts/migrate-skills-to-db.ts.

Глобальний маркетплейс

Метод Шлях Опис
GET /skills Список глобальних навичок
POST /skills Опублікувати навичку
GET /skills/:id Деталі навички
PUT /skills/:id Оновити навичку
DELETE /skills/:id Видалити навичку

Еволюція та оновлення

Метод Шлях Опис
GET /skills/:id/evolution Історія еволюції навички
GET /skill-updates Список доступних оновлень
POST /skill-updates/:id/approve Прийняти оновлення
POST /skill-updates/:id/reject Відхилити оновлення

Форки навичок

Метод Шлях Опис
GET /projects/:name/skill-forks Список форків
POST /projects/:name/skill-forks Створити форк
PUT /projects/:name/skill-forks/:id Оновити форк
DELETE /projects/:name/skill-forks/:id Видалити форк

Чат та повідомлення

Метод Шлях Опис
POST /projects/:name/chat Відправити повідомлення в чат
GET /projects/:name/chat/history Історія чату
POST /projects/:name/message Надіслати повідомлення воркеру (Phase 48.6: автоматично wake-up idle-killed воркера, ~2-4с cold start; Phase 48.6.1: wake-up тепер працює і в single-mode проєктах, не лише parallel)
GET /projects/:name/pins Список нотаток (pins)
POST /projects/:name/pins Створити нотатку
DELETE /projects/:name/pins/:id Видалити нотатку

Wiki (4 ендпоінти)

Метод Шлях Опис
GET /projects/:name/wiki/tree Дерево wiki-сторінок
GET /projects/:name/wiki/file Прочитати wiki-сторінку
PUT /projects/:name/wiki/save Зберегти wiki-сторінку. Phase 71.5: fires syncWiki → re-embed Cohere (fire-and-forget; failures логуються, write не падає).
GET /projects/:name/wiki/download Завантажити wiki як ZIP архів

Аналітика (4 ендпоінти)

Метод Шлях Опис
GET /analytics/activity Стрічка активності
GET /analytics/sidebar Дані для бічної панелі
GET /analytics/phases Список фаз проєкту
POST /analytics/phases Оновити фази проєкту

Marketplace та Sage (8 ендпоінтів)

Метод Шлях Опис
GET /sage/scout/categories Категорії маркетплейсу
POST /sage/scout Пошук навичок
POST /sage/scout/quick-scan Швидке сканування
POST /sage/scout/analyze Глибокий аналіз навички
POST /sage/scout/install Встановити навичку
POST /sage/analyze Sage аналіз
GET /sage/status Статус Sage сервісу
POST /sage/benchmark Запустити бенчмарк

Пам'ять та Knowledge

Метод Шлях Опис
GET /projects/:name/rag/search?q=...&k=6&include_global=true&doc_types=wiki,issue,skill,transcript Phase 71.7 (#364): semantic search над embeddings + embeddings_vec (Cohere + sqlite-vec). Параметри: q (текст запиту), k (1-25, default 6), include_global (default true — merge з _global_ skill namespace), doc_types (subset через кому; Phase 73.6 additional type: transcript). Response: `{ query, project, hits: [{rank, doc_type, doc_id, chunk_ix, distance, scope: 'project'
POST /projects/:name/memory/refresh Phase 71.8 (#365): re-embed MANIFEST + ROADMAP + ключові файли у RAG store (раніше — sync до NotebookLM). Той самий endpoint, нова семантика.
POST /projects/:name/memory/fetch-artifact Видалено у Phase 71.8 (audio overview не має RAG-еквівалента) — повертає 410 Gone.
GET /projects/:name/learnings Список learnings
POST /projects/:name/learnings Додати learning
GET /projects/:name/knowledge-graph Граф знань проєкту

Документація (глобальна, без auth)

Метод Шлях Опис
GET /docs/tree?lang=<lang> Дерево документації; lang опціональний (en/uk), default en
GET /docs/file?path=<p>&lang=<lang> Прочитати файл документації з language fallback

GET /docs/tree — query: lang (опціональний)

GET /docs/file — query: path (обовʼязковий), lang (опціональний)


Система

Метод Шлях Опис
GET /system/configs Отримати системні конфігурації
PUT /system/configs Оновити системні конфігурації

Коди помилок

Код Значення
200 Успіх
201 Створено
400 Невалідний запит
401 Не авторизовано
403 Заборонено (multi-tenancy)
404 Не знайдено
409 Конфлікт (дублікат)
429 Занадто багато запитів
500 Серверна помилка

GitHub Integration (Phase 49.3)

Endpoint Method Опис
/api/crm/projects/:name/github GET Список GitHub repos прив'язаних до проекту
/api/crm/projects/:name/github POST Прив'язати repo (body: {owner, repo}) — повертає webhook URL + secret + setup instructions
/api/crm/projects/:name/github/:id DELETE Відв'язати repo
/api/crm/projects/:name/github/events GET Список останніх GitHub events (Phase 49.3.1, query: ?limit=50)
/api/webhooks/github POST Public webhook receiver (HMAC-SHA256 validated, rate-limit 100/min)

Supported events: push, pull_request, workflow_run, issues. Notifications routed to project owner's Telegram.

Account Security (Phase 45.4)

Endpoint Method Опис
/api/crm/account/recovery GET Список активних recovery keys
/api/crm/account/recovery POST Створити recovery key (body: encryptedKey, keyHint)
/api/crm/account/recovery DELETE Відкликати recovery key(s) (body: { id } або {} для всіх)
/api/crm/account/recovery/restore GET Отримати encrypted master key для відновлення

Безпека


Phase 53.13 — type-safety baseline (2026-05-10)

Не зміна поведінки ендпоінтів — лише внутрішні типи. tsc --noEmit тепер блокує push/CI:


Sentinel Pentest Remediation (2026-06-10, #433–#444)

White-box pentest sprint — зміни поведінки endpoints після фіксу 3×P1 + 4×P2 + 3×P3:


Phase 53.15 — Sentinel Sprint 1 (2026-05-10)

Зміни поведінки auth + admin endpoints (Sentinel audit P0 fixes):


Phase 53.21 — Sentinel P2 batch 2 (2026-05-12)

Phase 63 — UI/UX Consolidation + Token Usage Tracking (2026-05-21, #148)

Новий ендпоінт:

Зміни в claude-runner.ts:

UI зміни (не API):

Phase 53.18 — tmux secret-leak fix (2026-05-11)

Не зміна поведінки endpoints — лише refactor internal spawn paths.

Phase 53.16 — Sentinel Sprint 2 (2026-05-10)

Зміни поведінки endpoints після hardening 13 × P1:

Phase 55 — Cosmic Editorial login (2026-05-13)

Нові endpoints для magic-link sign-in:

EphemeralTokenType union розширено: тепер містить "magic_link" поряд з існуючими oauth_state / password_reset / email_verification / tfa_challenge.

Frontend (CosmicCard.jsx) handle'ить magic state (60-s resend countdown) та ?magic_token= URL-параметр (auto-consume → login → success animation).

Phase 56 — AI Interop / Project Context Export (2026-05-13)

Owner-only експорт sanitized snapshot проєкту як .md для передачі зовнішньому AI (Gemini / ChatGPT / Perplexity / Claude.ai).

Alert: коли власник перевищує 3 експорти за 24h AND prefs.notify_on_export = true (default OFF) — logActivity("export_alert", ...) йде через існуючий Phase 53.10 TG notify pipeline (alertFired: true у response body).

Multi-tier scanner (shared/secret-scanner.ts) — Tier 1 regex (PATTERN_REGISTRY з PII sanitizer), Tier 2 Shannon entropy ≥4.5 bits/char на ≥20-char runs, Tier 3 context heuristics (key=/token:/secret=/password=). Whitelist: UUID / git SHA / SHA-256 / repeated chars / short hex / low-entropy base58. Severity tiers (critical/high/medium/low). Перформанс: <500 ms / 1 MB.

DB migration 024 — таблиці export_audit_log + export_preferences.


Phase 57 — Platform Settings (Sentinel #103 follow-up, 2026-05-15)

Super-admin secret management через CRM UI замість ssh/edit-.env/paste-in-chat. Backend MVP (Stage 1 з 4 stages). Усі endpoints гейтнуті requireAdmin (Phase 53.15) — повертають 403 Forbidden — admin only для не-admin, 401 Unauthorized без JWT.

Hard exclusion list NEVER_EXPOSE: CRM_SECRET (JWT signing) + SECRET_ENCRYPTION_KEY (vault meta-key) — навіть admin запит з валідним токеном повертає 400 "not managed". Audit log append-only (no UPDATE/DELETE handler), кожна дія (incl. failed) пише row з IP + UA + email.

DB migration 026 — таблиця platform_audit_log. Stage 2 (frontend PlatformSettings.jsx) — shipped 2026-05-15 (cbc8bac): admin-only card grid + rotate modal (<input type="password"> + retype-confirm) + audit drawer; sidebar entry filtered by userRole === "admin" fetched from /api/auth/me.

Polish (2026-05-15, commit 56191b0) — Platform Settings UI restructure. GET /api/crm/platform/settings response items gain 5 new fields: category (anthropic|oauth|telegram|email), usedIn (string[] — files/flows that consume the key), getFromUrl (where to fetch a fresh value), effectAfterRotate, riskIfLeaked. Used by frontend to render 4 sectioned card groups + per-card collapsible help panel with structured context (Used in / Get from / Effect / Risk). No behavioral change to mutator endpoints (PUT/POST/restart/test).

Refactor (2026-05-16)shared/routes/platform.ts internal cleanup. 39 lines removed (16 added), no public API surface change. PUT/POST/restart/test/audit endpoint signatures and responses unchanged. Documented here only because the doc-coverage pre-push gate triggers on any shared/routes/*.ts diff.

Backdated activity (#117, 2026-05-16)POST /api/mcp/issues/:project/:id/log now accepts optional ts field (ISO-8601 string). Used by arc retro reconstruction so historical entries land at their original timestamps. Future-dated values are silently clamped to now inside addActivity() (defense against fat-finger backdates). Invalid ISO → 400.

Stage 3 (2026-05-15) — hot-reload OAuth + Resend secrets без restart. shared/auth.ts loadOAuthConfig() тепер читає getSecret("GITHUB_CLIENT_ID/SECRET" | "GOOGLE_CLIENT_ID/SECRET") per call замість process.env. Callsites у master-bot/routes/auth.ts уже викликали getOAuthConfig() per request → 0 callsite changes. RESEND_API_KEY уже hot-reload через shared/email.ts:47. Поведінкова зміна: PUT /api/crm/platform/settings/{GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GOOGLE_CLIENT_ID|GOOGLE_CLIENT_SECRET|RESEND_API_KEY} тепер набуває чинності з наступного запиту, не вимагає restart. restartTargets для цих 5 ключів пустий → кнопка Restart у UI прихована. Edge case: OAuth flow з state-token виданим до rotation може отримати 400 на callback при code-exchange — user retry resolves. ANTHROPIC_API_KEY, PLATFORM_ANTHROPIC_KEY, MASTER_BOT_TOKEN, CITADEL_BOT_TOKEN залишаються restart-required (читаються при child-bot spawn / TG long-poll init).

Phase 57.3.5 cleanup (2026-05-16)MANAGED_KEYS allowlist trimmed 9 → 6. Removed: ANTHROPIC_API_KEY (operators now use single PLATFORM_ANTHROPIC_KEY for both trial-credits and platform inference; .env fallback still works for legacy code paths until Sage/Karpathy migrate), CITADEL_BOT_TOKEN (per-project bot belongs under child:<name>:token vault entries, managed by worker onboarding flow — not Platform Settings). MASTER_BOT_TOKEN repurposed: label → "Telegram — System Monitor Bot", description → "Server health alerts + on-demand status probes (admin-only, not a chat bot)". Phase 58 will add the monitoring loop (push alerts for worker crash / disk / RAM / SSH brute-force / CF bypass + /status, /health, /errors, /restart commands). Final set: PLATFORM_ANTHROPIC_KEY + GITHUB×2 + GOOGLE×2 + MASTER_BOT_TOKEN + RESEND_API_KEY (refs #103).

Arc Help (Phase 61 / #147)

History (Phase 61 / #153):

GDPR / Compliance (Sprint 1+2, #161–#174, 2026-05-22)

Right to Erasure — DELETE /api/auth/account (#162)

Permanently deletes the authenticated user and all their data (GDPR Art. 17).

Password Version / Token Invalidation (#174)

Migration 035 adds password_version INTEGER NOT NULL DEFAULT 0 to users. On password change, password_version is incremented. JWT payload includes pv field. crmAuthMiddleware validates pv against DB on each request, rejecting tokens issued before the last password change (401 "Token invalidated — please log in again"). Fails open if DB is unavailable.

Data Retention Cron (#168)

Master bot runs a daily purge at startup + every 24h. Retention limits: chat_messages 180 days (by timestamp), activity_log 365 days (by created_at), auth_events 90 days (by ts), token_usage_log 730 days (by created_at unixepoch), export_audit_log 365 days (by exported_at). Non-fatal — erasure does not block startup.

Email Compliance (#167)

All outbound transactional emails (password reset, verification, magic-link) now include:

Security — HIBP Breached Password Check (#171)

On POST /api/auth/register and POST /api/auth/reset-password, the submitted password is checked against the HaveIBeenPwned k-anonymity API before being stored. Only the first 5 hex chars of the SHA-1 hash are sent to HIBP — the full password never leaves the server. If the password appears in any breach database with count > 0, the request is rejected with HTTP 400: "This password was found in a known data breach. Please choose a different password." Fails open on HIBP timeout/error (4s timeout) — a down HIBP does not block registration.

Data Portability — GET /api/auth/export (#163)

GDPR Art. 20 — Right to Data Portability. Returns a structured JSON file containing all personal data Arc OS holds about the authenticated user.

Arc Help — Hardened System Prompt + Anti-Injection (#151)

POST /api/crm/help/chat behavior changes (no API surface change):

Worker Discipline Hardening (#187, #188, #189, 2026-05-23)

Issue Status Expansion (#187)

PUT /api/mcp/issues/:project/:id now accepts extended status values:

Status Meaning
open Not yet started
in_progress Actively being worked (set by arc issue take)
blocked Waiting on external dependency
deferred Postponed (was previously stored as text only)
closed Done

New assignee field: issues now have assignee: string | null. Set via arc issue take <id> or --assignee <worker_id> in arc issue update.

Migration 036: ALTER TABLE project_issues ADD COLUMN assignee TEXT (nullable, auto-applied on server start).

arc issue take <id> CLI Command (#187)

Shortcut to claim an issue: sets assignee = current_worker_id, status = in_progress, logs activity, writes session state. Equivalent to:

arc issue update <id> --status in_progress --assignee developer
arc issue log <id> "Taken by developer — status set to in_progress"

commit-msg Hook Validation (#187)

.githooks/commit-msg now validates referenced #N issues against local issues/issues.json:

PROJECT_MANIFEST.md Bridge Injection (#188)

handleCliInit (shared/cli-routes.ts) now reads PROJECT_MANIFEST.md from the project root and injects it into the CITADEL block under ## Project Context. Limit: 8000 chars. This gives bridge workers (running on client machines via arc) access to compact architecture, security patterns, file structure, and key learnings from the full CLAUDE.md.

Placement: after PROJECT_RULES.md, before skills list.

context_assets Worker Config Field (#189)

Worker config in workers_registry.json supports optional context_assets: string[] — list of skill names that are automatically injected into every bridge session for that worker (without requiring arc skill <name>):

{
  "id": "developer",
  "context_assets": ["crm-api-reference", "archivist_system"]
}

Each skill content is injected under ### Auto-Loaded Skills → #### Skill: <name>, truncated at 3000 chars each.

Phase 62 — Voice Input (#373, 2026-06-05)

Real-time voice transcription proxied through the self-hosted whisper.cpp server (arc-whisper.service, port 19214, ggml-base model preloaded).

POST /api/crm/voice/transcribe (#373, Phase 62.4)

Transcribes short voice clips (chat dictation). Proxies audio to the local whisper-server and returns text.

Auth: Bearer token (or ?token= query).

Body: multipart/form-data

Field Type Notes
audio Blob webm / ogg / wav. Max 25 MB.
locale string BCP-47, e.g. uk-UA, en-US. Passed as language param to whisper.

Response 200:

{ "transcript": "Що ти зробив вчора?" }

Error codes:

Code Meaning
400 Missing audio or locale field
413 Audio over 25 MB
429 Daily quota reached (60 min/user/day) OR server busy (max 2 concurrent transcriptions)
502 whisper-server returned non-200
500 Unexpected failure

Rate limit: voice_usage_log (migration 051) tracks approximate seconds per (user, day) using upload byte size as a proxy (assumes ~32 kbps voice codec, ±30% accuracy). Hard cap: 3600 s / day. Requests that would exceed the cap return 429 before forwarding to whisper.

Architecture note: whisper runs only on Contabo (not inside per-user Hetzner containers). Audio bytes never leave Contabo; the resulting text is what Phase 70 cloud-chat routing sees. arc-whisper.service keeps the ggml-base model preloaded so per-call cost is pure inference (~3.4 s warm for 11 s audio, 3.1× realtime on the current 6-vCPU EPYC box).


Phase 73 — Meeting Transcription + Analysis (#377-#384, 2026-06-05)

Upload meeting audio/video to a project, get whisper transcription + Claude summary, optionally embedded into RAG. All routes are gated by canAccessProject (owner or admin).

POST /api/crm/projects/:name/transcripts/upload (#377, Phase 73.1)

Multipart upload, returns 202 with transcript_id + job_id + status:'queued'. Job is picked up by the in-process queue (max 1 concurrent).

Body fields:

Limits: 1 GB max upload, MIME allow-list (mp3/wav/m4a/aac/ogg/opus/flac + mp4/mov/webm/mkv).

Errors: 400 (missing field / bad MIME), 401, 413 (over cap), 500 (disk write).

GET /api/crm/projects/:name/transcripts (#379, Phase 73.3)

List transcripts for the project, cursor-paginated. Query: ?limit=20&cursor=<id>. Returns {items: TranscriptSummary[], next_cursor: number|null}.

GET /api/crm/projects/:name/transcripts/:id (#379)

Full row including transcript_text, summary_json (parsed to object), and frames_json (parsed when Phase 73.4 ships).

GET /api/crm/projects/:name/transcripts/job/:jobId/progress (#379)

SSE stream of job progress. Pushes event: progress with {status, progress_pct, step_label, error} whenever any field changes, plus : keep-alive comment heartbeats every 1s so Bun's 10s idleTimeout doesn't kill long whisper runs. Closes with event: end once status is terminal.

Auth: browser EventSource appends ?token=<bearer> (can't set Authorization header).

Terminal statuses: done (post-Phase 73.6 RAG embed + file cleanup), failed. Note: summarized is a transient step — SSE stays open through embeddingdone. The frontend send-button ungates at summarized (doesn't wait for RAG).

State machine (Phases 73.1-73.6)

queued
  → extracting_audio    (ffmpeg → 16 kHz mono WAV)
  → transcribing        (whisper-cli -t 4)
  → (video) extracting_frames → frames_extracted   (ffmpeg scene-change)
            → vision_analyzing → vision_analyzed   (Phase 73.4 Claude vision per frame)
  → (audio) transcribed
  → summarizing         (Claude Sonnet → summary_json, receives vision frames as context)
  → summarized
  → embedding           (Phase 73.6 Cohere upsert via shared/rag.ts, skipped if embed_to_rag=0)
  → done                (source file + frames dir deleted — CEO decision D4)

Vision frames JSON shape (Phase 73.4, #380)

Stored as JSON string in transcripts.frames_json (parsed back to object by GET /transcripts/:id).

[
  { "ts_ms": 3000, "description": "Slide titled 'Q3 Revenue' with bar chart showing 30% growth." },
  { "ts_ms": 6000, "description": "Architecture diagram with three boxes labeled API/Worker/DB." }
]

Hard cap MAX_FRAMES=50 per transcript (~$0.15 worst case at typical Sonnet vision pricing). Frames over cap are dropped silently, last kept description gets a [+N more frames dropped] suffix. Per-frame failures become [vision failed: <msg>] strings — they don't abort the pass. Frames described as "No informational content" are webcam-only or decorative.

Summary JSON shape (Phase 73.5, #381)

Stored as JSON string in transcripts.summary_json. Parsed back to object by GET /transcripts/:id.

{
  "tldr": "1-2 sentence executive summary",
  "key_points": ["..."],
  "action_items": [{"task": "...", "owner": "name or null"}],
  "decisions": ["..."],
  "topics": ["..."],
  "model": "claude-sonnet-4-5",
  "generated_at": "2026-06-05T20:04:15.573Z"
}

Anthropic key resolution mirrors shared/worker-spawn.ts: BYOK account_settings.anthropic_key (decrypted if encrypted), fallback PLATFORM_ANTHROPIC_KEY for trial-mode owners. Summary failures are non-fatal — transcript_text stays intact, status rolls back to transcribed/frames_extracted so the user can retry after fixing their key.

Phase 78 — Notes: Knowledge Collections (#394–#404, 2026-06-08)

NotebookLM-style per-project notes. Each note is a collection of sources (video, audio, YouTube, web, PDF, DOCX, TXT, image) with a shared RAG index and chat.

GET /api/crm/projects/:name/notes

Returns all notes for project. Auth required + canAccessProject.

Response 200:

[{ "id": 1, "title": "Sprint planning", "description": null, "created_at": "...", "source_count": 3 }]

POST /api/crm/projects/:name/notes

Create a new note.

Body: { "title": "string", "description": "string?" }
Response 201: { "id": 1, "title": "Sprint planning" }

GET /api/crm/projects/:name/notes/:id

Get note detail with sources, issue links, and chat history.

Response 200:

{
  "id": 1, "title": "Sprint planning",
  "sources": [{ "id": 1, "source_type": "youtube", "title": "My video", "url": "...", "status": "done", "duration_seconds": 3600 }],
  "issue_links": [{ "issue_id": 42, "title": "Issue title" }],
  "chats": [{ "role": "user", "content": "Summarize", "created_at": "..." }]
}

DELETE /api/crm/projects/:name/notes/:id

Delete note and all sources/chats. Cascades to note_sources, note_chats, note_issue_links.

POST /api/crm/projects/:name/notes/:id/sources

Add a source (file upload or URL).

Content-Type: multipart/form-data OR application/json

Response 201: { "source_id": 5, "status": "queued" }

Processing is async. Poll GET /notes/:id until source.status === "done".

PATCH /api/crm/projects/:name/notes/:id/sources/:sourceId

Rename a source (inline title edit).

Body: { "title": "New name" }
Response 200: {}
Pass empty string or null to reset to filename/URL default.

DELETE /api/crm/projects/:name/notes/:id/sources/:sourceId

Remove a source and its content.

GET /api/crm/projects/:name/notes/:id/sources/:sourceId/progress

SSE stream of source processing progress.

Events: progress { "status": "processing"|"done"|"error", "message": "..." }

POST /api/crm/projects/:name/notes/:id/chat

Send a message to the note's chat. SSE stream response.

Body:

{
  "message": "Summarize all sources",
  "selectedSourceIds": [1, 3]
}

selectedSourceIds is optional — omit to include all sources.

SSE events:

RAG strategy: sqlite-vec search on note_source embeddings → fallback direct content_text injection (80 K chars max) when vector search unavailable or no results. Anti-hallucination system prompt guard injected when unprocessed sources are included.

Tool use — create_issue: Claude can create project issues from chat. Multi-turn: turn 1 streams until tool call, backend executes (issueQueries.nextId + issueQueries.insert), turn 2 resumes streaming with tool result injected.

Source status state machine

queued → processing → done
                    ↘ error

Source status field values:

YouTube transcript strategy (Phase 78.3)

  1. youtube-transcript npm: language cascade ["en", "en-US", "en-GB"] → fallback any
  2. Supadata.ai API: GET https://api.supadata.ai/v1/youtube/transcript?url=...&text=true&lang=en → fallback without lang param
  3. yt-dlp + Whisper: final fallback for videos without captions

Priority: prefer English captions to avoid auto-translated Arabic/other language transcripts.