# Helpteo API v1

Protected calls accept `Authorization: Bearer <token>`. Operator web clients use a same-origin HttpOnly session cookie instead; never put account tokens in customer code. Native account tokens are returned by login/register and expire after seven days. Logout revokes the session, including its WebSocket access.

| Endpoint | Access | Purpose |
|---|---|---|
| `POST /api/v1/auth/register` | public, rate limited | Create company, owner and session |
| `POST /api/v1/auth/login` | public, rate limited | Email/password sign-in |
| `POST /api/v1/auth/logout` | account | Revoke current session |
| `GET /api/v1/projects` | account | Projects in current organization |
| `POST /api/v1/projects` | owner | Create project (name only) |
| `GET /health` | loopback server only | Database health |
| `POST /api/v1/sessions` | public | Create/resume a guest conversation |
| `GET /api/v1/me` | authenticated | Current identity |
| `GET /api/v1/conversations` | operator | Latest 100 conversations in the organization |
| `GET /api/v1/conversations/{id}/messages` | owner/operator | History; private notes excluded for guests |
| `POST /api/v1/conversations/{id}/messages` | owner/operator | Idempotent send |
| `PATCH /api/v1/conversations/{id}` | operator | Open/resolve |
| `GET /api/v1/events` | WebSocket | Authenticate in first frame and receive sync revisions |

Anonymous widgets first obtain a short-lived `/widget/admission` token for their configured embedding origin and include `admission_token` and `context.page_url` in `POST /sessions`. The loader handles this automatically; a bare public channel ID cannot start a session. See [widget security](widget-security.md). Generate and retain the random token before the first request. Response: `conversation_id`, `project`. The token is a secret credential, not a user-supplied predictable identifier.

Send: `{"client_id":"<stable unique ID for this message>","content":"Hello","internal":false}`. Keep the same ID and content when retrying after an ambiguous response. Private notes require an operator. New non-note messages reopen resolved conversations.

Status: `{"status":"resolved"}` or `{"status":"open"}`.

WebSocket first frame: `{"token":"<credential>"}` within five seconds. On `{"type":"sync", ...}`, refresh the inbox/history. Reconnect and fetch history after interruptions. Same-origin browser policy applies; native clients can authenticate directly. The website loader at https://widget.helpteo.com/loader.js embeds the widget from its own origin, avoiding a wildcard CORS policy.

This is an initial local API, not a released third-party compatibility contract. Signed identity for logged-in users, pagination, attachments and webhooks are planned.

## Working hours and visitor reply links

`GET/PATCH /api/v1/workspace/schedule` reads or updates the verified owner's workspace time zone and weekly working hours. `GET/PATCH /api/v1/me/availability` controls the signed-in operator's own time zone and notification hours. Time zones use IANA names such as `Europe/Moscow`; each weekday has `enabled`, `start` and `end` values. Existing workspaces and operators default to 24/7.

Push delivery requires both the workspace and the destination operator to be within their configured hours. Suppressed pushes are recorded as suppressed and are not delayed until the next shift.

Session and conversation-state responses expose `business_open` and `contact_required`. Outside workspace hours, a visitor must `PATCH /conversations/{id}/contact` with `name` and `email` before sending. The first public operator reply after each new visitor-message sequence queues one localized email with a private chat link; internal notes and subsequent operator messages do not create duplicates. Resume links expire after 30 days, carry no email address, keep their signed credential in the URL fragment and can be exchanged with `GET /api/v1/resume`.

Register: `{"name":"Alex","email":"alex@example.com","password":"<12+ characters>","company":"Acme","terms_version":"2026-10-27"}`. Password maximum is 72 UTF-8 bytes. Login uses email/password. Account responses include token, expiry, name, company, organization and role. Emails are normalized to lowercase. New accounts start with `email_verified=false`; registration queues a confirmation email. The operator UI requests confirmation before entering a workspace. Inviting operators additionally requires verification on the server.

Create project: `{"name":"Store"}`. Projects group website connections and have no URL. Add a website using `POST /projects/{id}/websites` with `{"name":"Main website","url":"https://example.com"}`; use its ID as `data-channel` in the loader. URL is descriptive metadata, not a verified domain or origin allowlist. Creates are not idempotent; inspect the list before retrying an ambiguous result.

Project management (owner, current organization): `PATCH /projects/{id}` accepts `name` and/or `archived`. Listing includes active and archived projects with website/conversation counts. `GET /projects/{id}/websites` lists connections; `PATCH /projects/{id}/websites/{channel}` updates name and URL while keeping the installation ID. Website edits require an active project.

Archived projects reject new conversations (410) and message/status writes (409); existing visitors can resume and read history. Session responses include boolean `read_only`, `/me` and inbox entries include `project_archived`. Restoring resumes existing widgets. Permanent `DELETE /projects/{id}` requires an archived project and `{"confirm_name":"<exact current name>"}`; it transactionally removes websites, conversations and messages.

New sessions use `channel_id`. Legacy `project_id` sessions and `data-project` snippets remain supported via a stable default website binding. A legacy `website_url` on project creation creates an initial connection for older API clients. Tokens cannot be reused across website connections.

Account cookies: HttpOnly, SameSite=Strict, Secure by default, path `/api/v1`. Local HTTP development explicitly disables Secure. State-changing API calls reject mismatched Origin hosts and cross-site Fetch Metadata. Login/register rate limits are persisted per direct peer IP; trusted reverse-proxy IP handling is still required before deployment.

## Account security and team

| Endpoint | Access | Payload/result |
|---|---|---|
| `POST /api/v1/auth/verification` | account | Resend verification email |
| `POST /api/v1/auth/verify` | one-time token | `{token}`; confirms email |
| `POST /api/v1/auth/forgot-password` | public, rate limited | `{email}`; same success response for existing and missing accounts |
| `POST /api/v1/auth/reset-password` | one-time token | `{token,password}`; revokes all sessions |
| `GET /api/v1/team` | verified owner | Members and pending invitations |
| `POST /api/v1/invitations` | verified owner | `{email}`; returns invitation ID |
| `POST /api/v1/invitations/{id}/resend` | verified owner in same company | Rotates link, renews expiry |
| `DELETE /api/v1/invitations/{id}` | verified owner in same company | Revokes pending invitation |
| `POST /api/v1/auth/accept-invitation` | invitation token | `{token,name,password}`; creates operator membership and session |
| `GET /api/v1/workspaces` | account | Memberships for this user |
| `POST /api/v1/auth/workspace` | account | `{org_id}`; replaces current session with a session for an existing membership |

Account responses and `/me` include `email` and `email_verified`. Verification links expire after 24 hours, reset links after 30 minutes, invitations after 72 hours. Tokens are in URL fragments, are never submitted on GET, and need explicit POST redemption. Existing invitees must use their existing password; invite acceptance never resets it. The invitation role is always operator. An existing owner is never downgraded by invitation acceptance.

## Conversation stages and history

`GET /conversations/{id}/state` is available to the owning visitor and company members. Returns `status` (open/resolved), `stage` (new/waiting/active/resolved), `agent_name`, `archived`, and ordered `events` with id, kind, actor_name and created_at. Internal notes do not change visitor state or produce public events.

`POST /conversations/{id}/take` requires a company operator/owner and active project/open conversation. It records a joined event once; repeated take by the same operator is idempotent; another operator gets 409. First fresh public operator reply also assigns an unassigned conversation. All team members retain reply access; individual reply author_name is recorded independently of assignment.

Resolve/reopen changes append events only on transitions. A fresh public visitor message after resolution clears assignment, records reopened and returns to waiting. A fresh public operator reply after resolution reopens and joins. Duplicate message retries do not alter stages or append events. The existing WebSocket sync mechanism notifies both sides of state changes even without new messages.

Migration 4 preserves prior messages, adds author_name and a conversation event table. Historical events/names are not fabricated: previously answered open conversations use a generic operator label until taken/replied to. Operator assignment indicates responsibility, not continuous online presence.

## Transient typing over WebSocket

After authenticating `/api/v1/events`, send `{"type":"typing","conversation_id":"...","active":true}`; send `active:false` on send, blur, pause or conversation switch. Operator internal-note clients must suppress active signals (`internal:true` also forces inactive server-side).

The counterpart receives `type`, `conversation_id`, opaque per-socket `source_id`, server-derived `author` (`visitor` or `operator`), `active`, and `ttl_ms:5000`. Track sources separately and expire locally. Signals are scoped to the visitor conversation and operators in its company. No draft text or typing history is stored. Disconnect clears that source. Clients refresh at most every 2 seconds and stop after 2.5 seconds idle; server limits active refreshes to once per second and disconnects above 60 frames per 10 seconds.

## Languages

Supported canonical tags: `en`, `ru`, `fr`, `de`, `it`, `es`, `pt-BR`. English is the fallback. API requests may send `Accept-Language`; errors include localized `error`, an English `error_key` for client-side lookup, and `Content-Language`. Never translate protocol values such as `open`, `resolved`, `joined`, or `operator`.

`GET /me` and authentication responses include `language`. An empty value on a preexisting account means no preference yet. Authenticated account users can `PATCH /me/language` with `{"language":"fr"}`. This changes only their own profile, across workspaces. Visitor tokens cannot set a profile language. New registrations use the negotiated request language. Migration 5 adds only `users.language`; history and existing memberships are untouched.

Widget installation defaults to browser language. Developers can force a language:

```html
<script src="https://helpteo.com/loader.js"
        data-channel="YOUR_CHANNEL_ID"
        data-language="pt-BR" async></script>
```

For direct iframe/test links append `&lang=fr` to the widget URL. Language changes do not create a new visitor session. Existing snippets need no changes to gain automatic localization.

## Account deletion and legal acceptance

`GET /api/v1/me/deletion` returns the signed-in account email and sole-owned workspaces `{id,name}`. `DELETE /api/v1/me` requires `{password,confirm_email,workspace_ids}` with the exact IDs reviewed by the user. It permanently removes the live account and associated sessions, action tokens, invitations, mail records and attributable operator messages; sole-owned workspaces and their conversations are deleted only with exact scope confirmation. Other accounts remain. Remaining members with no surviving workspace receive an empty workspace to keep access to their accounts. Concurrently changed scope returns 409. Invalid credentials return 403; account/visitor separation and per-IP limits apply. This works for unverified accounts as well. Session and WebSocket authorization is revoked.

New registration requires `terms_version: "2026-10-27"`; the accepted time and service provider (`ip-pendurov` for Russian registration, otherwise `opsoft-inc`) are recorded. Locale changes do not change that record. Web deletion entry: https://helpteo.com/delete-account/. Native apps expose Account → Delete account, with password confirmation.

### Operator profile

`GET /api/v1/me/profile` and `PATCH /api/v1/me/profile` require a registered operator session. PATCH accepts `name` (private team name) and `public_name` (customer-facing alias), trimmed, 1–80 Unicode characters, without controls or formatting characters. Email is read-only. Profile changes apply across the user's workspaces.

New and migrated accounts default to the public name `Support`. Visitor message and state responses resolve aliases by stable user ID, including historical replies and joined/resolved/reopened events. Unattributed legacy authors use `Support`; private names are never used as a public fallback. Changing a profile broadcasts the normal refresh notification. Internal notes retain their separate visibility rules.

### Conversation assignment

`GET /conversations` includes `agent_id` and `is_mine` for the authenticated workspace member. Mine and Queue filter open conversations; resolved history remains available separately.

`POST /conversations/{id}/assign` accepts `{ "user_id": "..." }`, requires a verified workspace owner, validates the target's membership, rejects archived/resolved conversations and serializes assignment with messages. Repeating an unchanged assignment does not duplicate the event. A changed assignment adds an `assigned` history event whose name is resolved through the target operator's public alias.

## Visitor context

`POST /api/v1/sessions` accepts an optional `context` object with `page_url`, `page_title`, `entry_url`, `referrer`, `language`, `timezone`. Visitor-authenticated `PATCH /api/v1/conversations/{id}/visitor` refreshes context. Operator-authenticated `GET` on the same path returns context including server-observed first/latest IP, timestamps and locally resolved GeoIP names. Visitors cannot read these details, and other workspaces receive 404.

Visitor message `POST` accepts optional `context` and stores its sanitized page URL/title as an immutable snapshot for that client message ID. Operator message responses include `page_url` and `page_title`; visitor responses omit these fields. Context is informational, never an authorization signal. URL credentials, fragments and all query parameters except the five standard UTM parameters are removed. Existing visitor tokens and old widget payloads remain valid.

Set `HELPTEO_GEOIP_DATABASE` to the local DB-IP City Lite MMDB. `HELPTEO_CHECK_GEOIP=/path/to/file helpteo` validates a database without starting the server. The monthly systemd updater validates before atomic replacement and restarts the API to reopen the database. Geography is approximate and displayed with DB-IP attribution.

## Operator notification center

`GET /support-alerts` remains an unread array for installed clients (up to 50,
last seven days). The new UI opts in with `view=new` or `view=all`, receiving
`{items, unread_count, latest_id, next_before}`. Each item adds boolean `read`;
all other fields retain their existing names. `before=<next_before>` fetches the
next page; totals apply to the full visible audience, not just the current page.
`view=summary` returns only `unread_count` and `latest_id` for the bell.

`POST /support-alerts/read` accepts either `{id}` (compatible with older clients)
or `{all:true, through_id:<latest_id from the UI>}`. The latter marks only
currently authorized records up to that snapshot; arrivals with larger IDs stay
unread. It returns `{ok:true, updated:<affected rows>}`. Opening/listing never
marks records read. Reads are per operator and cannot cross workspace, project,
assignment or operator-group access. The same current audience restriction
applies to unread counts, history, pagination and both read operations.
